Create thumbnails with AWS Lambda

Andy Chiang
6 min readJun 22, 2020

well~ long story

We need a S3 to put thumbnails, so create S3 bucket first. HOW

1. Create Lambda function

Function name: Thumbnail-stageRuntime: Python 3.8Execution role: Create a new role with basic Lambda permissions
Create lambda function

2. Add role

In function, click Permissions, there is a role name in Execution role block, click the name, it will open AWS Roles editor, click Attach policies.

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::YOUR_ORIGIN_IMAGE_BUCKET_NAME/*"
]
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::YOUR_THUMB_IMAGE_BUCKET_NAME/*"
]
}
]
}

Save it and remember the role name.

Make sure in Permission block, there is your role just added.

3. Add trigger

Click Add trigger button in Designer block.

Trigger configuration: Choose API GatewayAPI: Choose Create an APIAPI type: HTTP APISecurity: OpenDeployment stage: default
Add trigger

4. Python code

Make python3.8 env first tho.

mkdir lambda-projects
cd lambda-projects
python3.8 -m venv thumbnail-stage
cd thumbnail-stage
source bin/activate
python --version
Python 3.8.3
pip install --target ./package Pillow

Add lambda_function.py

# coding=UTF-8import PIL
from PIL import Image
from io import BytesIO
import json
import boto3, botocore, os, re, base64
s3 = boto3.resource('s3')# Lambda Environment Variablessource_bucket = os.environ['SOURCE_BUCKET']
source_prefix_key = os.environ['SOURCE_PREFIX']
target_bucket = os.environ['TARGET_BUCKET']
target_prefix_key = os.environ['TARGET_PREFIX']
source_replace = os.environ['SOURCE_REPLACE']
sizechart = os.environ['SIZECHART']
def check_object_exists(bucket, key):
try:
s3.Object(bucket, key).load()
except botocore.exceptions.ClientError as e:
return False
else:
return True
def lambda_handler(event, context):
# s3 307 answer
try:
key = event['queryStringParameters']['key']
except NameError as e:
return {
"statucCode": "404",
"body": "Can not find the key."
}
except Exception as e:
return {
"statucCode": "408",
"body": str(e)
}
match = re.search('((\d+)[a-zA-Z](\d+))\/(.*)', key) # Check value
if match == None:
return {
'statusCode': '404',
'body': 'Key is invalid.'
}
# check in size chart
if match.group(1) not in sizechart.split(','):
print("size chart: " + match.group(1))
return {
'statusCode': '404',
'body': 'Sizechart not found.'
}
width = int(match.group(2))
height = int(match.group(3))
extension = os.path.splitext(key)[1].lower()
r_key = str(match.group(4))
if source_replace != '':
r_key = re.sub(r'(http|https)://' + source_replace + source_prefix_key, '', r_key)
# path: prefix/v/WxH/W_H_key
# v/1000x1000/path1/path2/test_image.png
key_list = key.split("/")
prefix = key_list[0].lower()
# path: prefix/WxH/W_H_key
if prefix == "v":
source_sub_path = "item/v_image/"
elif prefix == "c":
source_sub_path = "item/c_image/"
else:
prefix = ""
source_sub_path = ""
if prefix > "":
putkey = target_prefix_key + prefix + "/" + str(match.group(1)) + "/" + r_key
else:
putkey = target_prefix_key + str(match.group(1)) + "/" + r_key
key = source_prefix_key + source_sub_path + r_key if extension in ['.jpeg', '.jpg']:
format = 'JPEG'
if extension in ['.png']:
format = 'PNG'
# check object exists
if check_object_exists(target_bucket, putkey) == False:
if check_object_exists(source_bucket, key) == False:
return {
'statusCode': '404',
'body': 'Image name not found.'
}
# get original's images
obj = s3.Object(
bucket_name=source_bucket,
key=key
)
obj_body = obj.get()['Body'].read() img = Image.open(BytesIO(obj_body))
SourceWidth, SourceHeight = img.size
# Make image to not distortion
# Based on width
if SourceWidth >= SourceHeight:
if SourceWidth > width:
# width:x = SourceWidth:SourceHeight
ResizeWidth = width
ResizeHeight = int(width * SourceHeight / SourceWidth)
# Based on height
else:
if SourceHeight >= height:
# x:height = SourceWidth:SourceHeight
ResizeHeight = height
ResizeWidth = int(height * SourceWidth / SourceHeight)
if 'ResizeWidth' not in locals() and 'ResizeHeight' not in locals():
ResizeWidth = SourceWidth
ResizeHeight = SourceHeight
img = img.resize((ResizeWidth, ResizeHeight), Image.ANTIALIAS) # Past white background
ResultImage = Image.new("RGB", [width, height], (255, 255, 255, 255))
ResultImage.paste(img, (int((width - img.size[0]) / 2), int((height - img.size[1]) / 2)))
# distortion
buffer = BytesIO()
ResultImage.save(buffer, format)
buffer.seek(0)
obj = s3.Object(
bucket_name=target_bucket,
key=putkey
)
#object_acl = s3.ObjectAcl(
bucket_name=target_bucket,putkey
)
obj.put(Body=buffer)
hex_data = buffer.getvalue()
else:
obj = s3.Object(
bucket_name=target_bucket,
key = putkey
)
hex_data = obj.get()['Body'].read()
return {
"isBase64Encoded": True,
"statusCode": 200,
"headers": {"content-type": "image/jpg"},
"body": base64.b64encode(hex_data).decode("utf-8")
}

zip all packages and upload to lambda

cd package
zip -r9 ${OLDPWD}/function.zip .
adding: PIL/ (stored 0%)
adding: PIL/FtexImagePlugin.py (deflated 60%)
adding: PIL/SunImagePlugin.py (deflated 63%)
adding: PIL/ImageDraw.py (deflated 76%)
adding: PIL/DcxImagePlugin.py (deflated 56%)
......
cd $OLDPWD
zip -g function.zip lambda_function.py
adding: lambda_function.py (deflated 66%)

Upload zip file to lambda with AWS CLI version 2.

aws lambda update-function-code --function-name my-function --zip-file fileb://function.zip

(Replace my-function with your lambda function name, ex: Thumbnail-stage)

After upload zip, reload your lambda function page, you’ll see packages and code.

5. Lambda settings

Add Environment variables

SIZECHART: required, split with ','
ex: 1000x1000,800x800,600x600,550x550,500x500,300x300,120x120
SOURCE_BUCKET: required, source image bucket
ex: my-storages-source
TARGET_BUCKET: required, thumbnail bucket
ex: my-thumbnail
SOURCE_PREFIX: source prefix path
ex: images/
SOURCE_REPLACE: If your source is a url, You can paste a domain & ‘/’ to replace it without ‘http://’ or ‘https://’.
TARGET_PREFIX: target prefix path
ex: thumb/

Basic settings block

Handler: lambda_function.lambda_handler
Memory: 128MB
Timeout: 0 min 10 sec
Execution role: choose Existing role
pick the role created in step 2
click Save

6. S3 settings

Step 1, check your source S3 bucket, you can keep private settings to prevent access from outside.

Step 2, check the thumbnail S3 bucket, in Properties tab, click the Static website hosting, then click Use this bucket to host a website, copy and paste redirect rules below. You need to replace YOUR_API_GATEWAY_HOST_NAME with your api gateway host, ex:0123456789.execute-api.us-west-1.amazonaws.com, and YOUR_API_PATH value with your api path ex: default/Thumbnail-stage. This can be found in API Gateway block in your lambda function.

<RoutingRules>
<RoutingRule>
<Condition>
<KeyPrefixEquals/>
<HttpErrorCodeReturnedEquals>403</HttpErrorCodeReturnedEquals>
</Condition>
<Redirect>
<Protocol>https</Protocol>
<HostName>YOUR_API_GATEWAY_HOST_NAME</HostName>
<ReplaceKeyPrefixWith>YOUR_API_PATH?key=</ReplaceKeyPrefixWith>
<HttpRedirectCode>307</HttpRedirectCode>
</Redirect>
</RoutingRule>
</RoutingRules>
Enable website hosting
Enable website hosting
Find API endpoint.
Find API endpoint.

7. Add cloudfront point to static_website_endpoint of thumbnail bucket

In Create Distribution, origin domain name must point to the website endpoint in Step 6. This can trigger 307 http code (Temporary Redirect) when file is unaccessible (403 http code here).

Edit behavior in cloudfront, update these fields.

Cache Based on Selected Request Headers: Whitelist
Whitelist Headers: Accept, Authorization
Query String Forwarding and Caching: choose Forward allm cache based on all

Now, you can put an image in SOURCE_BUCKET, and visit https://your_thumbnail.cloudfront.net/120x120/image.jpg

8. How to debug

CloudWatch > CloudWatch Logs > Log groups

find your lambda function name, that is.

Photo by Aniket Bhattacharya on Unsplash

--

--

Andy Chiang

不多話,愛兩個寶貝,寫Python,愛騎車,聽音樂