Build a serverless pipeline: upload an image to S3 → Lambda triggers → creates a thumbnail → saves to another bucket.

Architecture

  User uploads image.jpg → S3 (source bucket)
                              ↓ event trigger
                        Lambda function
                              ↓
                   S3 (destination bucket/thumbnails/)
  

Prerequisites

  • AWS account with Lambda and S3 access
  • AWS CLI configured (aws configure)
  • Python 3.12

Lambda Function

  # lambda_function.py
import json
import os
import boto3
from io import BytesIO
from PIL import Image

s3 = boto3.client("s3")
DEST_BUCKET = os.environ["DEST_BUCKET"]
THUMBNAIL_SIZE = (200, 200)

def lambda_handler(event, context):
    for record in event["Records"]:
        source_bucket = record["s3"]["bucket"]["name"]
        source_key = record["s3"]["object"]["key"]

        print(f"Processing s3://{source_bucket}/{source_key}")

        response = s3.get_object(Bucket=source_bucket, Key=source_key)
        image_data = response["Body"].read()

        image = Image.open(BytesIO(image_data))
        image.thumbnail(THUMBNAIL_SIZE)

        buffer = BytesIO()
        image.save(buffer, format="JPEG", quality=85)
        buffer.seek(0)

        dest_key = f"thumbnails/{source_key}"
        s3.put_object(
            Bucket=DEST_BUCKET,
            Key=dest_key,
            Body=buffer,
            ContentType="image/jpeg",
        )

        print(f"Saved thumbnail to s3://{DEST_BUCKET}/{dest_key}")

    return {
        "statusCode": 200,
        "body": json.dumps({"message": "Processing complete"}),
    }
  

Dependencies Layer

Lambda doesn’t include Pillow by default. Build a layer:

  mkdir -p layer/python
pip install Pillow boto3 -t layer/python/
cd layer && zip -r ../pillow-layer.zip python && cd ..
  

Deploy

  # Create buckets
aws s3 mb s3://my-image-source-bucket
aws s3 mb s3://my-image-dest-bucket

# Package function
zip function.zip lambda_function.py

# Create Lambda
aws lambda create-function \
    --function-name image-resizer \
    --runtime python3.12 \
    --handler lambda_function.lambda_handler \
    --role arn:aws:iam::ACCOUNT:role/lambda-s3-role \
    --zip-file fileb://function.zip \
    --environment "Variables={DEST_BUCKET=my-image-dest-bucket}" \
    --layers arn:aws:lambda:REGION:ACCOUNT:layer:pillow-layer:1 \
    --timeout 30 \
    --memory-size 256
  

S3 Trigger

  aws s3api put-bucket-notification-configuration \
    --bucket my-image-source-bucket \
    --notification-configuration '{
        "LambdaFunctionConfigurations": [{
            "LambdaFunctionArn": "arn:aws:lambda:REGION:ACCOUNT:function:image-resizer",
            "Events": ["s3:ObjectCreated:*"],
            "Filter": {"Key": {"FilterRules": [{"Name": "suffix", "Value": ".jpg"}]}}
        }]
    }'
  

Test

  aws s3 cp photo.jpg s3://my-image-source-bucket/photo.jpg
aws s3 ls s3://my-image-dest-bucket/thumbnails/
  

IAM Permissions Required

The Lambda execution role needs:

  {
    "Effect": "Allow",
    "Action": ["s3:GetObject"],
    "Resource": "arn:aws:s3:::my-image-source-bucket/*"
},
{
    "Effect": "Allow",
    "Action": ["s3:PutObject"],
    "Resource": "arn:aws:s3:::my-image-dest-bucket/*"
}
  

Concepts Applied

Bonus Challenges

  1. Support PNG and WebP formats
  2. Generate multiple sizes (small, medium, large)
  3. Add error handling with Dead Letter Queue (DLQ)
  4. Add CloudWatch alarms for failures
  5. Deploy with AWS SAM or Terraform
  6. Add image metadata extraction (EXIF data)

This project demonstrates event-driven serverless architecture — a core pattern in modern cloud applications.