Skip to content
$ ash_
All work

CloudSnap

→ Backend defined in 33 Terraform resource blocks across 5 modules, applied through OIDC

A serverless image library on AWS: 7 Python Lambdas behind a Cognito-authorised API Gateway, Rekognition tagging off the upload path, and the backend in 5 Terraform modules deployed through OIDC.

Problem

An image library that tags photos by what's in them and finds matches from an example image, with no servers to run. It started as a 2023 Monash team project. In 2026 I rebuilt it solo: new frontend, infrastructure as code and CI/CD. The key decision was keeping Rekognition off the upload path.

My Contribution

For the 2026 rebuild:

  • 7 Python 3.12 Lambdas: upload, gallery, tag search, reverse image search, tag editing, delete, and an S3-triggered object detector
  • Backend infrastructure in 5 Terraform modules (storage, auth, compute, API, observability), where for_each generates each Lambda's role, log group, alarm and route
  • 3 GitHub Actions workflows on OIDC with no stored AWS keys. terraform plan is posted to the pull request and apply waits for manual approval. Backend deploys are gated on pytest at 70% coverage or more; frontend deploys run type-check, lint, build, S3 sync and CloudFront invalidation
  • Cognito JWT auth at API Gateway, ownership checks returning 403, 1-hour presigned URLs and a 200 req/s throttle
  • 59 pytest tests using moto, with LocalStack for local development

Architecture

Upload validates JPEG or PNG up to 10 MB, writes to S3 under the user's ID, seeds a DynamoDB record with no tags and returns. The S3 event then triggers the detector, which writes Rekognition labels, and the gallery shows "Analysing…" until they land. Tagging can't slow down or fail an upload, and failed detector invocations go to an SQS dead-letter queue.

Reverse search has Rekognition label the query image (up to 20 labels at 70% confidence or more). The Lambda then reads the user's records from the UserID-UploadedAt global secondary index and keeps the ones that share a label. Every read is scoped to one user's partition, and label overlap is enough for this, so there's no vector store.

Auth runs through Cognito with 4-hour token expiry, enforced at API Gateway with a JWT authorizer, and every handler re-checks ownership and returns 403 on a mismatch. The gateway proves who you are; the handler proves the object is yours. Presigned URLs expire after an hour, so a leaked link stops working.

Each Lambda has its own error-rate alarm (over 5%) publishing to SNS email, alongside a CloudWatch dashboard and X-Ray tracing at 10% sampling. The frontend is a Next.js 16 static export on S3 behind CloudFront. The hosting bucket and distribution sit outside Terraform and are deployed by the frontend workflow.

Outcomes

Live on CloudFront. The backend is reproducible from terraform apply, and every infrastructure change shows its plan on the pull request before it can be applied. The AWS-facing code is tested with moto and runs locally on LocalStack, without touching a real account.

Learnings

Making tagging asynchronous changed the UI as much as the backend: the gallery has to show an image as "Analysing…" instead of pretending it's done. And dead-letter queues, alarms and tracing are much cheaper to add while building than to retrofit after something silently fails in an async path.