Building a Serverless Wedding Photo Gallery using AWS Lambda, S3 and DynamoDB
Whilst documenting how I structured the infrastructure used for hosting our wedding website, I mentioned the possibility of showcasing its use for another application concern. In the tradition of over engineering a problem related to our wedding - we really did not want resized/compressed photos shared through WhatsApp/iMessage of the big day. So instead, I decided to create a Serverless photo gallery which provided guests with the ability to share the original photos in one place. I also wanted to explore the ability to achieve this with having 100% feature parity locally in a development setting. In this post I would like to discuss how I went about building these photo-upload/resizing and lazy-loaded gallery capabilities using AWS Lambda, S3 and DynamoDB. The final implementation can be found in this GitHub repository.
The API
For the backend API I opted to use the Node 16 (nodejs16.x
) supported Lambda runtime, conforming to the RESTful Hypertext Application Language (HAL) standard.
Like in previous projects, I opted to use the Serverless Framework to manage deploying the transient application, whilst leaving the foundational building blocks for Terraform.
Each API endpoint’s functionality is split into individually packaged artifacts using esbuild for bundling and transpilation.
This provides smaller artifacts that only contain the dependencies (such as Sharp, AWS-SDK) which are actually required within each of the desired endpoint’s.
Persistence
For storing the photos metadata and gallery listing (used for the infinite scroll) I opted to use a Single-Table Design DynamoDB table.
This uses the PK
/SK
and GSI*PK
/GSI*SK
partition/sort key pattern found in Single-Table Design, to be able to optimally fetch data based on pre-defined access patterns.
The GSI1PK
is used as a hot key 😬 for listing the gallery photos in sorted processed timestamp order - using the last evaluated key as a marker to progressively provide the next batch of gallery photos to the client.
Using a hot key is not best practise, but based on this usage pattern and load, suffices for this use-case.
In its current form the use of a Single-Table Design may seem unnecessary with the application lacking lots of different access patterns and concerns. However, there is future scope to better showcase the idea with the additional functionality such as photo comments.
Photo Upload and Resizing
Uploaded photos are stored within an initial upload S3 bucket using pre-signed form POST requests, which are generated by the API based on a supplied pre-request by the client.
Upon successful photo upload, the resulting s3:ObjectCreated
event triggers a Lambda whose responsibility is to move the photo to the persistent S3 bucket, and generate resized web and thumbnail variants in WebP (using Node.js Sharp).
At the end of this process a record with the photo’s metadata is stored in the DynamoDB table, for inclusion into the gallery response.
The initial upload S3 bucket uses a lifecycle rule to clean up any failed photo uploads after 7 days.
Local Development
As stated above, one of my goals was to have local feature parity for development purposes. Thanks to a combination of Serverless Offline, Serverless DynamoDB Local and Serverless S3 Local I was able to achieve just that 🎉. I was pleasantly surprised at how easy it was to be able to replicate both pre-signing S3 POST requests and handling S3 event triggers thanks to s3rver. In doing this I was able to have greater confidence in my implementation before deploying it to AWS.
The Client
The client is developed as a Single-page application (SPA) using React and Create React App. I used this project as a good opportunity to explore using Tailwind CSS for presentational concerns. The client is built statically within the build pipeline, distributed via S3 and fronted by CloudFront. To ensure only authorised users have access to the gallery, a CloudFront function is employed to ensure that shared HTTP Basic Authentication credentials are supplied. As the same CloudFront distribution is used to front the Client, API and S3 buckets, we can be certain that all requests are protected by these security measures.
Photo Lazy Loading and Lightbox
The gallery is presented as an infinite scrollable list, which progressively (lazily) requests the next available batch of photos. When a photo is clicked on, a full-screen lightbox variant is presented to the user, providing the ability to visually swipe back and forth (on mobile devices) between photos. Effectively handling touch events, with graceful desktop mouse click fallback, was an interesting challenge using React Hooks.
Conclusion
I really enjoyed the chance to add additional functionality that could be hosted within the infrastructure project I had set up for our wedding website. Being able to replicate all the behaviour within a local setting was very satisfying, enabling a quick REPL for API/Client development. I was also very happy with the decision to structure both the client and API in a monorepo, with independent build pipelines for any changes that are pushed.
Finally, since getting the chance to now see how this gallery has been used in practise, I feel the addition of handling video media would be a great future improvement. Using AWS Elemental MediaConvert as a possible means to handle processing uploaded video media could provide similar elastic compute that Node.js Sharp hosted on Lambda does for photos.