Engineering reference: the product image cleaner architecture
This is the product image cleaner with the friendly labels removed: the real function names, which one is a container image and why, the bucket prefixes and the event that fires, the job table’s keys, and the IAM scope that keeps each function to its own job. If you want to build it rather than understand it, start here.
Key takeaways
- Six Lambda functions: one container image on arm64 with 6 GB for background removal, five small Python 3.14 zip functions for everything else.
- One versioned S3 bucket,
picr-images, with EventBridge notifications on; theinbox/upload event is what starts the pipeline. - One DynamoDB table, on-demand, keyed by
job_idwith a status-history sort key and a GSI onstatusfor the review sweep. - No Bedrock, no API Gateway, no NAT Gateway. The model runs inside the container (or behind a hosted-API key in Secrets Manager).
- One region,
eu-west-2, with per-function IAM roles scoped to exactly the prefixes, queues, and secrets each one touches.
The architecture, for engineers
This is the same system as Part 1 with the friendly labels removed and the real resources named. Everything is in one region, eu-west-2 (London), in one account. There is no API Gateway, no NAT Gateway, and nothing always-on; photos arrive through a watched-folder sync, an SES inbound rule, or a direct upload, all landing under one S3 prefix, and the bucket’s upload event — routed through EventBridge to SQS — is the only thing that starts work.
Lambda functions
Six functions. Five are small Python 3.14 zip packages on arm64; one — the cutout — is a container image, for the reasons in Part 3. Each does one job and hands to the next over SQS, sharing a single dead-letter queue (picr-dlq) after five attempts. All carry CloudWatch Logs at 7-day retention.
picr-ingest— zip, arm64, 1024 MB. Triggered bypicr-work(the upload event). Validates the file is a real image, rotates by EXIF, converts HEIC, caps the longest edge, strips metadata, writeswork/<job_id>/source.jpg, opens the job row, and enqueues the cutout. Rejects non-images here.picr-cutout— container image, arm64, 6144 MB, ~120s timeout. Runs the segmentation model (rembg / U^2-Net by default), composites the transparent cutout, and scores coverage and fragmentation. On pass, enqueues variants; on fail, marks the jobneeds reviewand notifies. The only function that may reach an external model and the only one that reads the model secret.picr-variants— zip, arm64, 2048 MB. Trims to the alpha bounding box, then builds each channel variant (pad, resize, composite onto white, watermark where allowed) with Pillow, writes them under theout/prefixes, and enqueues publish.picr-publish— zip, arm64, 512 MB. Writesmanifest.json, moves the job todone, and sends the single ready notification via SES or SNS.picr-drive-sync— zip, arm64, 512 MB. Scheduled. Mirrors the Drive drop-folder intoinbox/and theout/prefixes back into Drive; reads the Drive key from Secrets Manager.picr-sweep— zip, arm64, 256 MB. Scheduled, daily. Queries thestatusGSI for jobs stuck inneeds reviewpast a day and re-notifies the owner.
Storage, events, and schedules
- S3. One bucket,
picr-images, versioning on, EventBridge notifications enabled. Prefixes:inbox/(uploads),work/<job_id>/(normalised source and cutout), andout/web/,out/market/,out/social/(finished variants and the manifest). Lifecycle rules expireinbox/raw files and thework/intermediates after 30 days; theout/set is kept. - Events.
ObjectCreatedon theinbox/prefix is delivered to EventBridge; a single rule matches the prefix and targets thepicr-workSQS queue, which triggerspicr-ingest. The heavier stages are chained by SQS too, with the cutout queue given a visibility timeout longer than the function’s 120-second ceiling so a slow cutout isn’t redelivered mid-run. - DynamoDB (on-demand). One table,
picr-jobs— PKjob_id, SKtsfor an append-only status history (ingested, cut, varied, published, or needs-review). Attributes:source_key,status,coverage,confidence,variants(list of keys), andmanifest_key. A GSI onstatusletspicr-sweepfind held jobs without a scan. - EventBridge Scheduler. Two rules —
picr-drive-syncon a few-minute rate, andpicr-sweepon a daily cron (early morning). - Secrets Manager. Two secrets — the Google Drive key (read by
picr-drive-sync) and the hosted-model API key (read only bypicr-cutout). Fetched at call time, never in env vars. - ECR. Holds the
picr-cutoutcontainer image with the model weights baked in, deployed by digest. There is no Bedrock in this system; the only model is the one inside the container or behind the hosted-API key.
IAM scope and region
Each function gets its own execution role scoped to exactly what it touches, no wildcards. picr-ingest can read inbox/, write work/, put to picr-jobs, and send to the cutout queue; it cannot read out/ or any secret. picr-cutout can read and write work/, read only the hosted-model secret, publish a review notification, update picr-jobs, and send to the variants queue; it’s the only role allowed to reach the internet for the optional hosted model. picr-variants can read work/, write out/, and enqueue publish — nothing more. picr-publish can read work/ and out/, write the manifest, update picr-jobs, and send via SES/SNS, but cannot delete from any prefix. The scheduled functions hold only the narrow Drive and table permissions they need and have no inbound trigger surface at all. Everything runs in eu-west-2; there is no cross-Region path. An AWS Budgets alarm watches the monthly spend and notifies if it drifts above a few dollars — the early signal that an image is looping the cutout or a lane is misbehaving.
Design rules that shaped the build
- One container, by necessity. Only the cutout needs a model and the memory to run it; everything else stays a small zip.
- The upload is the only trigger.
ObjectCreatedoninbox/→ EventBridge → SQS → ingest; nothing polls. - One bucket, clear prefixes. Inbox, work, and per-channel out, with lifecycle on the throwaway prefixes.
- Least privilege, per role. Only the cutout reads the model secret and reaches the internet; only ingest reads
inbox/. - State in DynamoDB, pixels in S3. A single job table with a status GSI drives the review sweep; images never live in the table.
- One region, no model service.
eu-west-2throughout; the model is in the container, not in Bedrock.