⚡ S3 → Lambda Image Resize Pipeline
An event-driven image-processing practical using Amazon S3 and AWS Lambda — verified end-to-end with CloudWatch, then deliberately broken at the IAM layer to practice diagnosing a real production-style failure.
Objective
Build a pipeline where an image uploaded to S3 automatically triggers a Lambda function, which reads the image, resizes it, and writes the generated thumbnail back to a separate S3 prefix — then validate both the working path and a deliberate failure path:
Normal operation: S3 upload → Lambda → resize → thumbnail
Failure handling: S3 upload → Lambda →
AccessDenied→ CloudWatch diagnosis
Architecture
S3 Bucket
s3-image-resize-lambda-lab
/ \
/ \
uploads/ thumbnails/
│ ▲
│ ObjectCreated │ PutObject
▼ │
┌────────────────────────────┐
│ Lambda │
│ s3-image-processor │
│ │
│ 1. Read image from S3 │
│ 2. Resize with Pillow │
│ 3. Write thumbnail to S3 │
└────────────────────────────┘
│
▼
CloudWatch Logs
The S3 trigger was scoped to the uploads/ prefix only, and Lambda writes its output to thumbnails/ — keeping the generated thumbnail from ever re-triggering the function.
Resources created:
-
S3 bucket:
s3-image-resize-lambda-lab, with prefixesuploads/(input) andthumbnails/(output) -
Lambda:
s3-image-processor— Python 3.12, x86_64 -
Lambda Layer:
pillow-image-processing— Pillow wasn't in the base Python runtime, so it was added as a custom layer -
Trigger: S3
ObjectCreatedevents onuploads/ -
Execution role:
s3:GetObjectonuploads/*,s3:PutObjectonthumbnails/*— deliberately scoped rather than broad bucket access
Step 1 — Build the Pipeline
The S3 trigger was configured for all object-create events, scoped to the uploads/ prefix with no suffix filter (event type s3:ObjectCreated:*), which also auto-granted S3 the permission to invoke the Lambda. The execution role was given exactly two permissions — s3:GetObject on arn:aws:s3:::s3-image-resize-lambda-lab/uploads/* and s3:PutObject on arn:aws:s3:::s3-image-resize-lambda-lab/thumbnails/* — intentionally avoiding a broader S3 grant.
The handler logic follows a straightforward sequence:
Read S3 event → extract bucket + object key → get the original image from S3 → open with Pillow → resize while preserving aspect ratio → swap
uploads/forthumbnails/in the key → put the resized image back into S3
The thumbnail target is derived directly from the source key — uploads/photo.png becomes thumbnails/photo.png.
Step 2 — Verify the Normal Workflow
An image was uploaded to uploads/. Lambda was invoked automatically, and the resized thumbnail appeared under thumbnails/ moments later. CloudWatch confirmed the full round trip:
New file uploaded: s3://s3-image-resize-lambda-lab/uploads/56291ef0-ccd4-4201-8334-1915f49fd835.png
Thumbnail created: s3://s3-image-resize-lambda-lab/thumbnails/56291ef0-ccd4-4201-8334-1915f49fd835.png
Every stage of the path checked out: S3 upload, the S3 → Lambda trigger, Lambda execution, the image read, the resize, and the thumbnail write.
Step 3 — Break It on Purpose, Then Diagnose
To simulate a real permissions failure, the s3:GetObject statement for uploads/* was removed from the Lambda execution role (leaving s3:PutObject on thumbnails/* untouched), and a new image was uploaded to uploads/.
The S3 event still fired and Lambda still ran — it just failed partway through. CloudWatch isolated exactly where:
[ERROR] AccessDenied
An error occurred (AccessDenied) when calling the GetObject operation:
User: arn:aws:sts::...:assumed-role/s3-image-processor-role-.../s3-image-processor
is not authorized to perform: s3:GetObject
This pinned the failure precisely on the execution role's permissions, not the S3 trigger, the Lambda Layer, or the runtime — the trigger fired correctly and the function only failed once its code tried to read the source image. The original s3:GetObject grant was then restored and the successful flow re-verified before cleaning up the lab environment.
The event path worked; the execution permission did not.
Troubleshooting Pattern
The sequence that actually isolated the fault, worth reusing on any event-driven pipeline:
Did the upload happen? → did S3 invoke Lambda? → did Lambda start? → where did Lambda fail? → check the CloudWatch error → check the execution-role permission → restore only the required permission
This is more useful than simply checking whether the final thumbnail exists, because it separates an event/configuration problem from an execution/permission problem instead of treating "nothing happened" as one undifferentiated failure.
Key Takeaways
-
Separate input and output prefixes (
uploads/vsthumbnails/) stop the pipeline's own output from re-triggering itself — for larger systems, separate input/output buckets give an even stronger isolation boundary. -
Scope Lambda permissions to the exact paths needed (
GetObjectonuploads/*,PutObjectonthumbnails/*) rather than granting broad, bucket-wide S3 access. - CloudWatch is the first place to look when an event-driven pipeline fails — a successful S3 upload says nothing about whether downstream processing succeeded, since the failure can occur well after the event reaches Lambda.
- Event-driven systems fail at multiple independent boundaries, and a production troubleshooting process should identify which boundary failed before changing anything:
S3 upload → S3 event → Lambda invocation → IAM authorization → S3 read → image processing → S3 write
Final Result
Normal path: image upload → S3
/uploads/→ObjectCreatedevent → Lambda → Pillow resize → S3/thumbnails/→ thumbnail ✅Failure path: image upload → S3
/uploads/→ObjectCreatedevent → Lambda →GetObject→AccessDenied❌ → CloudWatch identifies the missing permission
Outcome: successfully built and verified an S3-triggered Lambda image-processing pipeline end to end, then deliberately broke and correctly diagnosed an IAM permission failure using CloudWatch logs.
Top comments (0)