An Open Source and Completely Serverless Image Resizing Service in AWS

Horace Nelson
6 min readMay 17, 2021

--

Go straight to the Github Repo

The need to properly size and optimize images within web and app content is such a common problem that there are numerous—if not seemingly innumerable—solutions out there. Generally, you’ll find this to be just one feature of Digital Asset Managers (or DAMs), like Cloudinary, Widen, and Bynder. These services also provide many more primary features—proofing, approval workflows, asset organization, content localization, slick UIs, etc.—and you’ll pay handsomely. What if I don’t need all of that, and just want to be able to dynamically (at request time) resize images?

Isn’t there already open source for this?

There are actually quite a few public Github repos out there to address this problem. A couple of years back (in the early days of Lambda@Edge) I was looking for a solution and found this tutorial on the official AWS blog. Of course, that tutorial was just intended as a demonstration of what was capable and not instruction on building a production-ready solution per sé. In fact, both the architecture and the code were a little lacking. Still, I tried to use that blog post as a jump-off to create several subsequent solutions. The problem is that all of the blogs, tutorials, walkthroughs, and open source solutions that I’ve been able to find since then are just as incomplete, as if they’ve all learned from the same example.

TL;DR

Yeah, this is a bit long-winded, so I’ll just copy-paste the Conclusion here.

If all you need is to host, resize, optimize, and cache images, don’t break the bank on a Digital Asset Management solution. Building a robust solution is simple with a little knowledge of a handful of AWS services, and I went ahead and did it all for you. Check out my fully documented and production-ready solution on Github, which builds and deploys in just 2 commands!

Introducing: Image Flex

Image Flex is a robust and fully open source image resizing service built on AWS Serverless technologies and used to resize, optimize, and cache images on “the edge,” on the fly. Served by CloudFront via an Origin Access Identity. Executed on Lambda@Edge. Backed by S3. Protected by AWS WAF. Provisioned via CloudFormation. Built and deployed by the Serverless Application Model (SAM) CLI.

The Approach

I started by using what I evangelize as “template-driven architecture.” That is, I don’t manually create or configure any resources. All of the infrastructure will be code, configured via a SAM/CloudFormation template, and only via that template. This is a good (if not best) practice to prevent drift between your actual resources and your template, or vice versa. Other good solutions for template-driven architecture include Terraform, Ansible, and Serverless Framework.

I systematically composed a robust serverless… er… service on AWS, organically, one resource at a time. The end product can host large, unoptimized images, resize them at request time on demand, optimize them to AVIF format, store the new versions, cache the new versions on the edge, and return them to the client. To do this, the application required provisioning, security, storage, caching, and business logic (i.e., compute). AWS provided the tools and services that fulfilled all of those requirements:

Architecture

Provisioning

The CloudFormation template implements AWS’s Serverless Application Model (SAM) syntax. SAM provides an alternative shorthand for defining certain serverless-specific resources in a CloudFormation template. Note that SAM resource definitions can differ dramatically from the same resource definitions using straight CloudFormation syntax.

Security

The entire service is behind an AWS WAF Web access control list. I only employ the AWS Common Rule Set to defend against numerous vulnerabilities. You could further lock it down by whitelisting requester IP addresses or geo-locking requests.

I also implement an Origin Access Identity, which prevents public access to the hosting S3 bucket by only allowing the CloudFront distribution to read from it.

Storage

To host both the original and optimized images, S3 was the obvious and only choice here. A private S3 bucket resource is defined, blocking all public access except by the CloudFront distribution, which employs the Origin Access ID. The bucket definition also includes CORS settings that allow clients on any domain to GET images from the service. You might want to lock this down further by providing a list of AllowedOrigins (domains from which your images may be requested).

A second S3 bucket is defined to store logs for the CloudFront distribution (see Caching next).

Caching

Now that we have a bucket to host our images, we’ll want to put a CDN in front of it to enable caching. A CDN (content delivery network) like CloudFront will serve cached copies of the images from edge locations closest to the actual user. The definition for the CloudFront distribution specifies the hosting S3 bucket (above) as its origin, as well as the Origin Access ID to use to be allowed access to that bucket. The definition also specifies the WAF ACL to use (created earlier). Lastly and as mentioned in the previous section, the definition also enables saving logs to an S3 bucket. These logs will have an entry for every request to the CloudFront distribution.

Compute

To process our requests (and to prevent having to manually run servers) Lambdas were a perfect fit. These Lambdas scripts are actually Lambda@Edge scripts, as they will be attached to different parts of the CloudFront request/response event cycle, therefore executing on the CloudFront edge servers.

CloudFront Events That Can Trigger a Lambda Function

*See CloudFront Events That Can Trigger a Lambda Function

Viewer Request

The first script is triggered by CloudFront’s viewer request event, which fires before the request reaches the cache. This script will see a URL that looks like /myimage.png?w=500 and convert it to an S3 key that looks like /500/myimage.avif.

Observer Response

The second script is triggered by CloudFront’s observer response event, which fires after CloudFront has gotten the response from the origin, but before it has returned it. This script follows this logic:

  1. If the response from the origin has an HTTP status of 200 (the resized image has already been previously generated), return that response.
  2. If the response from the origin has an HTTP status of 403 or 404, fetch the base image, resize to the requested dimensions, save the resized copy of the image to S3, and return that new image in the response.

Usage

Using the service is as simple as constructing a URL to request an image.

For example, let’s say we have a large, unoptimized photo that we want to include on a website. We don’t want to pull that image in directly, as that would negatively affect performance. We want it sized appropriately, and optimized if possible.

Our large, unoptimized base image might be 2400x1350 (a 16:9 aspect ratio):

https://my.service.url/myphoto.png

Once we upload that to the service’s S3 bucket, we can easily request different sizes using the w (width, required) and h (height, optional) query string parameters.

For example, requesting the following for the first time…

https://my.service.url/myphoto.png?w=800

… will generate, save, cache, and return the following image resized to 800wx450h (the 16:9 aspect ratio is preserved) and optimized to AVIF format (if supported by the requesting browser):

https://my.service.url/800/myphoto.avif

Using the optional height parameter…

https://my.service.url/myphoto.png?w=400&h=400

… the resulting image will be 400w x 400h, the aspect ratio of the original image still preserved, but the image will be clipped (as with CSS’s object-fit: cover):

https://my.service.url/400x400/myphoto.avif

Additionally, you can use the optional format (f) parameter to pass a file extension of the expected image format:

https://my.service.url/myphoto.png?w=400&h=400&f=png

… the resulting image will be 400w x 400h PNG:

https://my.service.url/400x400/myphoto.png

Try it out

If all you need is to host, resize, optimize, and cache images, don’t break the bank on a Digital Asset Management solution. Building a robust solution is simple with a little knowledge of a handful of AWS services, and I went ahead and did it all for you. Check out my fully documented and production-ready solution on Github, which builds and deploys in just 2 commands!

If you like the solution, please go ahead and star that Github repo, and be sure to post any issues you come across or new features you’d like to see (you could even submit your own pull request, should you feel so motivated).

Cheers!

--

--