At the last AWS ReInvent, it was announced that AWS Lambda would support Ruby as a runtime language. I was eager to try this out, Ruby’s powerful syntax and features are a joy to work with and coupling this with AWS Lambda I figured it could be leveraged for some easy image resizing Lambda.
I started off with the serverless framework as this is an easy way provision Lambda functions. The goal is that when an image is uploaded to an S3 bucket, a Lambda is started, it resizes the image, and then uploads it to another bucket.
The first step is to install serverless
npm install -g serverless
serverless create -t aws-ruby -p image_resizer
This creates the basic boilerplate of a serverless framework ruby Lambda function. It will create two files, a handler.rb
, the file with the actual function the Lambda will call, and a serverless.yml
a file that the serverless framework uses to configure and provision the lambda and affiliated services such as AWS S3 and AWS API Gateway.
We will first change the serverless.yml
configuration. We add a function handle_resize
that calls the ImageHandler.process
function in handler.rb
whenever an object is created in the your-images
S3 bucket. Furthermore, under the provider key, we grant the function access to S3 and use the ruby2.5
runtime.
We also add an S3 resource resized-your-images
, this will be the bucket where we moved the resized images to.
service: image-resizer
provider:
name: aws
runtime: ruby2.5
iamRoleStatements:
- Effect: Allow
Action:
- s3:*
Resource: "*"
functions:
handle_resize:
handler: handler.ImageHandler.process
events:
- s3:
bucket: your-images
event: s3:ObjectCreated:*
resources:
Resources:
ResizedImages:
Type: AWS::S3::Bucket
Properties:
BucketName: resized-your-images
Now we can implement the ImageHandler.process
function.
Writing the Ruby lambda
First we’ll add a Gemfile because of two dependencies we need to use. The AWS S3 SDK to retrieve and upload files to S3, and the mini_magick
gem, a wrapper for imagemagick.
#Gemfile
source 'https://rubygems.org'
gem 'aws-sdk-s3', '~> 1.30.0'
gem "mini_magick", '~> 4.9.0'
Next we update the handler.rb
file.
#handler.rb
require 'uploaded_file'
class ImageHandler
def self.process(event:, context:)
event = event["Records"].first
bucket_name = event["s3"]["bucket"]["name"]
object_name = event["s3"]["object"]["key"]
file = UploadedFile.from_s3(bucket_name, object_name)
file.resize "100x100"
file.upload_file("resized-your-images", "resized_" + event["s3"]["object"]["key"] )
end
end
The ImageHandler.process
is the function called by the lambda and has two arguments, the event
, in our case an event sent by S3 about newly created files, and the context
, basically metadata about the function and its environment.
The process
function retrieves the bucket and key of the newly created object from the event object. Then it calls into the not yet implemented UploadedFile
class. This class retrieves the s3 object, resizes it and then uploads it to another bucket. This class is independent from the lambda, and in general all business logic should be. This makes it easier to use the code in other contexts and also allows easier testing. E.g. I can test the UploadedFile.from_s3(bucket_name, object_name)
without having to mock the entire S3 Lambda event.
Now we’ll implement the UploadedFile class.
# uploaded_file.rb
require "aws-sdk-s3"
require "mini_magick"
class UploadedFile
def self.from_s3(bucket_name, object_name)
s3 = Aws::S3::Resource.new()
object = s3.bucket(bucket_name).object(object_name)
tmp_file_name = "/tmp/#{object_name}"
object.get(response_target: tmp_file_name)
UploadedFile.new(tmp_file_name)
end
def initialize(tmp_file)
@tmp_file = tmp_file
end
def resize(params)
image = MiniMagick::Image.open(@tmp_file)
image.resize params
@resized_tmp_file = "/tmp/resized.jpg"
image.write @resized_tmp_file
end
def upload_file(target_bucket, target_object)
s3 = Aws::S3::Resource.new()
object = s3.bucket(target_bucket).object(target_object).upload_file(@resized_tmp_file)
end
end
The uploaded_file.rb
is typical ruby. It retrieves the S3 file, stores it in a tmp-file. We call mini_magick
to resize the image, and then upload the file again to the target bucket.
Deployment
When deploying we need to make sure that the Lambda function has access to the dependencies. Serverless framework will build a zip-file with the code and all dependencies locally and then deploy this to AWS. Therefore it is important that we run the same version of ruby locally as on AWS, so that would be ruby 2.5. If you use rbenv
, you can use rbenv install 2.5.0
and rbenv local 2.5.0
to set the ruby version for this project.
Next we need to vendorize the dependencies so they are included in the package. Run bundle install --path vendor/bundle
, now the dependencies are in the vendor
directory.
Before we can deploy we still need to set the credentials for AWS. See Serverless Framework documentation for more information.
Now that is done you can invoke sls deploy
and the lambda function will be deployed and the S3 buckets will be created. Upload an image to the your-images
bucket and moments later a resized version will appear in the resized-your-images
bucket. If anything goes wrong you can follow that in the log files in AWS Cloudwatch. There will be a log stream for the lambda function. You can also call sls logs -f handle_resize
to tail the logs.
Find the code on github
Photo by Paulius Dragunas on Unsplash