Using Python and AWS Lambda to Resize Images
Image resizing is a common task in web development, especially when dealing with user-uploaded images. AWS Lambda provides a serverless computing service that allows you to run your code without managing any infrastructure.
In this blog post, we will explore how to use Python and AWS Lambda to resize images dynamically.
Table of Contents
Theory
There are primarily two ways to run Python code in AWS Lambda:
Inline Code: In this method, you can write your Python code directly in the AWS Lambda console. This is suitable for simple or short-lived functions where the code logic is not extensive. You can quickly prototype and test your code without the need to package and upload any files.
Function Package: In this method, you package your Python code, along with any dependencies or libraries, into a deployment package. The deployment package is then uploaded to AWS Lambda. This method is suitable for more complex functions that require external dependencies or have a larger codebase.
When using the Function Package approach, you have two options to package your Python code:
Zip Package: You can create a ZIP file containing your Python code and any required dependencies. The ZIP file is then uploaded to AWS Lambda using the AWS Management Console, AWS CLI, or an AWS SDK. AWS Lambda will extract and execute the code from the ZIP file.
Container Image: AWS Lambda also supports running Python code using container images. You can build a Docker container image that contains your Python code and dependencies, and then push the image to a container registry such as Amazon Elastic Container Registry (ECR). AWS Lambda will pull the container image and execute the code within the container.
Both methods have their advantages and considerations. Inline Code is more suitable for simple functions with minimal dependencies, while the Function Package approach offers more flexibility for complex applications.
It's essential to choose the method that aligns with your specific requirements and the complexity of your Python code. We will use the "Container Image" approach in this blog post.
The advantages of using the "Container Image" approach in AWS Lambda:
Flexible runtime environment customization for Python code
Simplified deployment of larger codebases or multiple files
Efficient dependency management within the container image
Reproducibility and consistent runtime environments across deployments
Enhanced portability for running Python code on different platforms
Seamless integration with container-related tools and services
Enables leveraging CI/CD pipelines, container orchestration platforms, and monitoring solutions
And the final but most important reason is that I love containers. I think they have made life very simple for us DevOps people.
Enough theory let's get our hands dirty.
Prerequisites
Before we begin, make sure you have the following:
An AWS account
AWS CLI.
Docker installed and running.
Basic knowledge of Python programming language
Basic understanding of AWS Lambda and Amazon S3
Note:
You will find the number 112233445566 in the commands below.
You will have to replace it with your AWS account id.
Setting up the AWS environment
To get started, follow these steps to set up your AWS environment:
1. Create an S3 bucket
Let's create the bucket that will be used to upload user images. For this example, we will use the same bucket to store the resized images but ideally, you should use another bucket. The reason is that the Lambda function will be invoked on
$ aws s3api create-bucket \
--bucket awslambda-imageresizer-test-002 \
--create-bucket-configuration "LocationConstraint=us-west-2"
2. Create an IAM role
We are creating a role and providing permission to the Lambda service assume this role.
$ aws iam create-role \
--role-name awslambda-imageresizer-role \
--assume-role-policy-document \
'{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}'
Let's add the AWSLambdaBasicExecutionRole policy. It provides write permissions to CloudWatch Logs.
$ aws iam attach-role-policy \
--role-name awslambda-imageresizer-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Now let's allow the role to get and put objects to our S3 bucket
$ aws iam put-role-policy \
--role-name awslambda-imageresizer-role \
--policy-name S3AccessPolicy --policy-document \
'{
"Version": "2012-10-17",
"Statement":[
{
"Effect": "Allow",
"Action": [ "s3:PutObject", "s3:GetObject" ],
"Resource": "arn:aws:s3:::awslambda-imageresizer-test-002/**"
}
]
}'
Setting up the code
Get the code from this GitHub repo; phashish/aws-lambda-image-resizer
$ git clone git@github.com:phashish/aws-lambda-image-resizer.git
$ cd aws-lambda-image-resizer/
Let's go through the important files here.
First is the Dockerfile.
$ cat Dockerfile
FROM public.ecr.aws/lambda/python:3.10
COPY requirements.txt ${LAMBDA_TASK_ROOT}
COPY lambda_function.py ${LAMBDA_TASK_ROOT}
RUN pip install -r requirements.txt
CMD [ "lambda_function.handler" ]
Here we are using AWS's Python 3.10 image.
Then copying our script and the requirements.txt.
Then using pip to install the boto library for connecting to S3 and Pillow to manipulate the image.
The second file is our code, where we are parsing the uploaded image details from the event.
Using boto3, we are open the image and resize it using the Pillow library.
Finally using boto3 to upload the resized image with a 'resized-' prefix attached.
$ cat lambda_function.py
import boto3
from PIL import Image
import io
def handler(event, context):
# Get the S3 bucket and key from the event
bucket = event['Records'][0]['s3']['bucket']['name']
key = event['Records'][0]['s3']['object']['key']
print("Got new image: " + key + " from the bucket: " + bucket)
# Set the desired width and height for resizing
width = 800
height = 600
# Load the image from S3
s3 = boto3.client('s3')
response = s3.get_object(Bucket=bucket, Key=key)
image = Image.open(io.BytesIO(response['Body'].read()))
# Resize the image
resized_image = image.resize((width, height))
print("Image resized.")
# Save the resized image to the same S3 bucket with a different name
resized_key = 'resized-' + key
with io.BytesIO() as output:
resized_image.save(output, format='JPEG')
output.seek(0)
s3.put_object(Body=output, Bucket=bucket, Key=resized_key)
print("Image " + resized_key + " uploaded.")
return {
'statusCode': 200,
'body': 'Image resized successfully!'
}
The third important file is the requirements.txt
$ cat requirements.txt
boto3
Pillow
Building the code
Let's log into the AWS ECR so that we can upload out image.
$ aws ecr --region us-west-2 \
get-login-password | docker login \
--username AWS --password-stdin \
112233445566.dkr.ecr.us-west-2.amazonaws.com
Create the repository for our image
$ aws ecr --region us-west-2 create-repository \
--repository-name aws-lambda-image-resizer
Now let's build the code
$ docker build -t aws-lambda-image-resizer:test .
If there are any errors, fix them and get a successful build.
Now let's tag the build so that we can upload it
$ docker tag aws-lambda-image-resizer:test 112233445566.dkr.ecr.us-west-2.amazonaws.com/aws-lambda-image-resizer:latest
Let's push the image
$ docker push 112233445566.dkr.ecr.us-west-2.amazonaws.com/aws-lambda-image-resizer:latest
Now, let's create the Lambda function
$ aws lambda --region us-west-2 create-function \
--function-name aws-lambda-image-resizer \
--package-type Image \
--code ImageUri=112233445566.dkr.ecr.us-west-2.amazonaws.com/aws-lambda-image-resizer:latest \
--role arn:aws:iam::112233445566:role/awslambda-imageresizer-role
You can get information about the function
$ aws lambda --region us-west-2 get-function \
--function-name aws-lambda-image-resizer
{
"Configuration": {
"FunctionName": "aws-lambda-image-resizer",
"FunctionArn": "arn:aws:lambda:us-west-2:112233445566:function:aws-lambda-image-resizer",
"Role": "arn:aws:iam::112233445566:role/awslambda-imageresizer-role",
"CodeSize": 0,
"Description": "",
"Timeout": 3,
"MemorySize": 128,
"LastModified": "2023-07-19T14:01:11.135+0000",
"CodeSha256": "658764bc89cfa54c3f1649f263b4aca61a5aa9813ff0ca2e53ab1d3a111adb33",
"Version": "$LATEST",
"TracingConfig": {
"Mode": "PassThrough"
},
"RevisionId": "1d4f1de2-83d1-48da-b0fb-dc7c74095844",
"State": "Active",
"LastUpdateStatus": "Successful",
"PackageType": "Image",
"Architectures": [
"x86_64"
],
"EphemeralStorage": {
"Size": 512
},
"SnapStart": {
"ApplyOn": "None",
"OptimizationStatus": "Off"
}
},
"Code": {
"RepositoryType": "ECR",
"ImageUri": "112233445566.dkr.ecr.us-west-2.amazonaws.com/aws-lambda-image-resizer:latest",
"ResolvedImageUri": "112233445566.dkr.ecr.us-west-2.amazonaws.com/aws-lambda-image-resizer@sha256:658764bc89cfa54c3f1649f263b4aca61a5aa9813ff0ca2e53ab1d3a111adb33"
}
}
You can see that the default function timeout is 3 sec. This is really low. We know that our code will not be able to download, resize and upload an image in 3 sec.
So lets update the configuration.
$ aws lambda --region us-west-2 update-function-configuration \
--function-name aws-lambda-image-resizer \
--timeout 120
You can confirm the change by running the above get-function command again.
Now let's add the image upload event trigger in S3 to trigger our Lambda function.
$ aws lambda --region us-west-2 add-permission \
--function-name aws-lambda-image-resizer \
--action "lambda:InvokeFunction" \
--principal s3.amazonaws.com \
--source-arn arn:aws:s3:::awslambda-imageresizer-test-002 \
--statement-id s3-trigger
Now we'll add the bucket notification configuration so that our Lambda function is notified when anyone uploads a file with prefix "test-" and suffix ".jpg"
$ aws s3api put-bucket-notification-configuration \
--bucket awslambda-imageresizer-test-002 \
--notification-configuration '{
"LambdaFunctionConfigurations": [
{
"LambdaFunctionArn": "arn:aws:lambda:us-west-2:112233445566:function:aws-lambda-image-resizer",
"Events": ["s3:ObjectCreated:*"],
"Filter": {
"Key": {
"FilterRules": [
{
"Name": "prefix",
"Value": "test-"
},
{
"Name": "suffix",
"Value": ".jpg"
}
]
}
}
}
]
}'
Testing the setup
Now let's test our setup by uploading a file.
$ aws s3 cp test-wallpaper.jpg s3://awslambda-imageresizer-test-002/
upload: ./test-wallpaper.jpg to s3://awslambda-imageresizer-test-002/test-wallpaper.jpg
We will check the logs. In order to do that we need to find the Cloudwatch LogGroup
$ aws --region us-west-2 logs describe-log-groups
{
"logGroups": [
{
"logGroupName": "/aws/lambda/aws-lambda-image-resizer",
"creationTime": 1689843384125,
"metricFilterCount": 0,
"arn": "arn:aws:logs:us-west-2:112233445566:log-group:/aws/lambda/aws-lambda-image-resizer:*",
"storedBytes": 0
}
]
}
Now we can check the logs to see how our function ran.
$ aws --region us-west-2 logs \
tail /aws/lambda/aws-lambda-image-resizer
2023-07-20T08:56:23.440000+00:00 2023/07/20/[$LATEST]7a31213c928d4e58a8609655f34008d8 START RequestId: db0033a0-bad0-4d0a-8045-ba22b985450c Version: $LATEST
2023-07-20T08:56:23.441000+00:00 2023/07/20/[$LATEST]7a31213c928d4e58a8609655f34008d8 Got new image: test-wallpaper.jpg from the bucket: awslambda-imageresizer-test-002
2023-07-20T08:56:26.806000+00:00 2023/07/20/[$LATEST]7a31213c928d4e58a8609655f34008d8 Image resized.
2023-07-20T08:56:26.983000+00:00 2023/07/20/[$LATEST]7a31213c928d4e58a8609655f34008d8 Image resized-test-wallpaper.jpg uploaded.
2023-07-20T08:56:27.022000+00:00 2023/07/20/[$LATEST]7a31213c928d4e58a8609655f34008d8 END RequestId: db0033a0-bad0-4d0a-8045-ba22b985450c
2023-07-20T08:56:27.022000+00:00 2023/07/20/[$LATEST]7a31213c928d4e58a8609655f34008d8 REPORT RequestId: db0033a0-bad0-4d0a-8045-ba22b985450c Duration: 3582.03 ms Billed Duration: 5474 ms Memory Size: 128 MB Max Memory Used: 93 MB Init Duration: 1891.44 ms
We will also check the S3 buckets to make sure that we have our resized image.
$ aws s3 ls s3://awslambda-imageresizer-test-002/
2023-07-20 14:26:27 136279 resized-test-wallpaper.jpg
2023-07-20 14:26:10 1233774 test-wallpaper.jpg
Voila! We have our resized image.
We can see the difference in the file size and the file creation time.
So this is how we can create a simple image resize function and run it on AWS Lambda.
Clean up
We will now remove all the resources we have created for this test.
First remove the log-group from Cloudwatch
$ aws --region us-west-2 logs delete-log-group \
--log-group-name /aws/lambda/aws-lambda-image-resizer
Lets remove all the images from the S3 bucket and delete the bucket
$ aws s3 rm s3://awslambda-imageresizer-test-002/ --recursive
delete: s3://awslambda-imageresizer-test-002/resized-test-wallpaper.jpg
delete: s3://awslambda-imageresizer-test-002/test-wallpaper.jpg
And the bucket itself
$ aws s3api delete-bucket \
--bucket awslambda-imageresizer-test-002
Now we will remove the function
$ aws --region us-west-2 lambda delete-function \
--function-name aws-lambda-image-resizer
We will now delete the ECR repository
$ aws --region us-west-2 ecr delete-repository \
--repository-name aws-lambda-image-resizer --force
{
"repository": {
"repositoryArn": "arn:aws:ecr:us-west-2:112233445566:repository/aws-lambda-image-resizer",
"registryId": "112233445566",
"repositoryName": "aws-lambda-image-resizer",
"repositoryUri": "112233445566.dkr.ecr.us-west-2.amazonaws.com/aws-lambda-image-resizer",
"createdAt": "2023-07-19T19:26:31+05:30",
"imageTagMutability": "MUTABLE"
}
}
Now let's delete the S3AccessPolicy role policy
$ aws iam delete-role-policy \
--role-name awslambda-imageresizer-role \
--policy-name S3AccessPolicy
Now the attached AWSLambdaBasicExecutionPolicy
$ aws iam detach-role-policy \
--role-name awslambda-imageresizer-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
And finally, the role
$ aws iam delete-role --role-name awslambda-imageresizer-role
Conclusion
In this blog post, we explored how to use Python and AWS Lambda to resize images dynamically. We set up the AWS environment, wrote the Python code using the Pillow library, deployed the Lambda function, and tested the image resizing functionality. With AWS Lambda, you can easily automate image resizing tasks and efficiently manage your infrastructure.
AWS Lambda provides many more features and capabilities, such as configuring memory and timeout settings, handling error cases, and integrating with other AWS services. I encourage you to explore the AWS Lambda documentation for more advanced usage scenarios.
Happy image resizing!
Comments