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 that provided guests with the ability to share the original photos in one place. I also wanted to explore the ability to achieve this while 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 used the Serverless Framework to manage deploying the transient application while leaving the foundational building blocks for Terraform.
Each API endpoint’s functionality is split into individually packaged artefacts using esbuild for bundling and transpilation.
This provides smaller artefacts that only contain the dependencies (such as Sharp, AWS-SDK) which are actually required within each of the desired endpoints.
Persistence
For storing the photo metadata and gallery listing (used for 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, allowing optimal data retrieval based on predefined 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 practice, but based on this usage pattern and load, it suffices for this use case.
In its current form, the use of a Single-Table Design may seem unnecessary since the application lacks multiple access patterns and concerns. However, there is future scope to better showcase the idea with 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 pre-request supplied by the client.
Upon successful photo upload, the resulting s3:ObjectCreated
event triggers a Lambda function responsible for moving the photo to the persistent S3 bucket and generating 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 in the gallery response.
The initial upload S3 bucket uses a lifecycle rule to clean up any failed photo uploads after seven 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 replicate both pre-signing S3 POST requests and handling S3 event triggers thanks to s3rver. Doing this allowed me 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 an opportunity to explore 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 enforces 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 displayed, 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, after seeing how this gallery has been used in practice, I feel the addition of handling video media would be a great future improvement. Using AWS Elemental MediaConvert as a possible means to process uploaded video media could provide similar elastic compute functionality that Node.js Sharp, hosted on Lambda, does for photos.