I recently built an image recognition app that identifies celebrities and detects objects in photos using Amazon Rekognition. It started as a simple Lambda function triggered by S3 in a simple presentation I made before cloud enthusiasts. Now, I've evolved the setup into a full web application with a custom domain, CI/CD pipeline, and API documentation.
In this article, I'll walk you through how I did it — first the simple version using the AWS Console, then the enhanced version using Terraform.
Part 1: The Simple Version (AWS Console)
This is the foundation. You'll create a Lambda function that automatically analyzes any image you upload to S3. No frontend, no API — just upload an image to S3 and check CloudWatch for results.
What You'll Build
- An S3 bucket to store images
- A Lambda function that calls Amazon Rekognition
- An S3 trigger that invokes Lambda whenever an image is uploaded
Step 1: Create an IAM Role for Lambda
Lambda needs permissions to read from S3, call Rekognition, and write logs.
- Go to IAM Console → Roles → Create role
- Select AWS service → Lambda → Next
- Attach these managed policies:
AmazonS3ReadOnlyAccessAmazonRekognitionReadOnlyAccessAWSLambdaBasicExecutionRole
- Name the role:
rekognition-lambda-role - Click Create role
That's it for permissions. Lambda can now read images from S3, send them to Rekognition, and log results to CloudWatch.
Step 2: Create the S3 Bucket
This is where your images will live.
- Go to S3 Console → Create bucket
- Bucket name:
my-rekognition-images(must be globally unique) - Region: Pick your preferred region (I used
us-east-1) - Leave everything else as default (Block all public access = ON)
- Click Create bucket
Step 3: Create the Lambda Function
This is the brain of the operation.
- Go to Lambda Console → Create function
- Choose Author from scratch
- Settings:
- Function name:
image-recognition-processor - Runtime: Python 3.11
- Architecture: x86_64
- Execution role: Use an existing role → select
rekognition-lambda-role
- Function name:
- Click Create function
Now paste this code in the function editor:
import json
import boto3
from urllib.parse import unquote_plus
rekognition_client = boto3.client('rekognition')
def lambda_handler(event, context):
"""Process S3 event and analyze image with Rekognition."""
# Get bucket and key from the S3 event
record = event['Records'][0]
bucket = record['s3']['bucket']['name']
key = unquote_plus(record['s3']['object']['key'])
print(f"Processing image: s3://{bucket}/{key}")
# Call Rekognition to detect labels
response = rekognition_client.detect_labels(
Image={
'S3Object': {
'Bucket': bucket,
'Name': key
}
},
MaxLabels=10,
MinConfidence=70.0
)
# Log the results
labels = response['Labels']
print(f"\nDetected {len(labels)} labels:")
for label in labels:
print(f" - {label['Name']}: {label['Confidence']:.1f}%")
return {
'statusCode': 200,
'body': json.dumps({
'image': key,
'labels': [{'name': l['Name'], 'confidence': l['Confidence']} for l in labels]
})
}
Click Deploy to save.
Step 4: Increase the Timeout
Rekognition can take a few seconds, and Lambda's default timeout is 3 seconds.
- Go to Configuration tab → General configuration → Edit
- Set timeout to 30 seconds
- Set memory to 256 MB
- Save
Step 5: Add S3 Trigger
This is what makes it automatic — every image upload triggers analysis.
- In your Lambda function, click Add trigger
- Select S3
- Configure:
- Bucket:
my-rekognition-images - Event types: All object create events
- Suffix:
.jpg(optional — filters to only JPEG files)
- Bucket:
- Check the acknowledgment box
- Click Add
Step 6: Test It
- Go to your S3 bucket
- Upload any JPEG or PNG image
- Go to CloudWatch → Log groups →
/aws/lambda/image-recognition-processor - Open the latest log stream
- You'll see something like:
Processing image: s3://my-rekognition-images/test-photo.jpg
Detected 8 labels:
- Person: 99.2%
- Human: 99.2%
- Outdoors: 95.1%
- Building: 88.4%
- City: 85.7%
- Architecture: 82.3%
- Urban: 78.9%
- Metropolis: 71.2%
That's it! You now have an AI-powered image recognition pipeline. Upload an image, and within seconds Rekognition tells you what's in it.
Want Celebrity Recognition Instead?
Replace detect_labels with recognize_celebrities:
response = rekognition_client.recognize_celebrities(
Image={
'S3Object': {
'Bucket': bucket,
'Name': key
}
}
)
celebrities = response['CelebrityFaces']
for celeb in celebrities:
print(f" - {celeb['Name']}: {celeb['MatchConfidence']:.1f}%")
if celeb.get('Urls'):
print(f" Links: {celeb['Urls']}")
Part 2: The Enhanced Version (Terraform + Web UI)
The console version is great for learning, but what if you want a proper web app? One where users can upload images from a browser, see results instantly, and access it from a custom domain?
That's what I built next. Here's the architecture:
- Frontend: Static website on S3, served via CloudFront with a custom domain
- Backend: API Gateway + Lambda functions (presigned URLs, processing, results)
- Storage: S3 for images, DynamoDB for results
- AI: Amazon Rekognition (celebrity + label detection)
- CI/CD: GitHub Actions deploys everything automatically
Why Terraform?
Clicking through the console works for learning, but it's:
- Slow to reproduce
- Easy to misconfigure
- Impossible to version control
Terraform lets you define everything as code. One command creates the entire infrastructure. Another destroys it. Push to GitHub and it deploys automatically.
Prerequisites
Before you start, you'll need:
- AWS Account with a registered domain in Route 53
- ACM Certificate (in us-east-1 for CloudFront) for your domain
- Terraform installed locally (download here)
- AWS CLI configured with your credentials
- GitHub account for CI/CD (optional but recommended)
Project Structure
image-recognition/
├── .github/workflows/deploy.yml # CI/CD pipeline
├── codes/
│ ├── celebrity.py # Standalone celebrity script
│ ├── image.py # Standalone label detection script
│ └── processor-web.py # Combined processor (deployed)
├── terraform/
│ ├── lambda_packages/
│ │ ├── presign/ # Generates upload URLs
│ │ ├── processor/ # Analyzes images (auto-generated)
│ │ └── results/ # Returns results from DynamoDB
│ ├── main.tf # All infrastructure
│ ├── variables.tf # Configuration variables
│ └── outputs.tf # Useful output values
└── website/
├── index.html # Main page
├── app.js # Frontend logic
├── config.js # API endpoint (auto-injected)
└── styles.css # Styling
Step 1: Clone the Repository
git clone https://github.com/Fidelisesq/image-recognition-app.git
cd image-recognition-app
Step 2: Configure Variables
Edit terraform/variables.tf and update:
variable "domain_name" {
default = "your-domain.com" # Your domain
}
You'll also need to set your certificate ARN and hosted zone ID (either in a terraform.tfvars file or pass them as -var flags).
Step 3: Deploy with Terraform
cd terraform
terraform init
terraform apply \
-var="certificate_arn=arn:aws:acm:us-east-1:ACCOUNT:certificate/CERT-ID" \
-var="hosted_zone_id=YOUR_ZONE_ID"
Terraform will show you everything it's about to create. Type yes to confirm.
In about 5-10 minutes, you'll have:
- 2 S3 buckets (images + website)
- 3 Lambda functions
- API Gateway with endpoints
- CloudFront distribution
- Route 53 DNS record
- DynamoDB table
- All the IAM roles and policies
Step 4: Deploy the Website
After Terraform finishes, grab the outputs:
# Get the API endpoint and bucket name
terraform output api_endpoint
terraform output website_bucket
terraform output cloudfront_distribution_id
Update website/config.js with your API endpoint, then sync:
# Upload website files
aws s3 sync ../website s3://YOUR-WEBSITE-BUCKET/ --delete
# Clear CDN cache
aws cloudfront create-invalidation --distribution-id YOUR_DIST_ID --paths "/*"
Wait a minute or two, then visit your domain. Done!
Step 5: Automate with GitHub Actions (Optional)
If you want push-to-deploy, set up the CI/CD pipeline:
- Create a GitHub OIDC IAM role (guide here)
- Add these GitHub secrets:
-
CERTIFICATE_ARN— your ACM certificate ARN -
HOSTED_ZONE_ID— your Route 53 zone ID
-
- Push to
main— GitHub Actions handles everything
The pipeline:
- Deploys infrastructure via Terraform
- Auto-injects the API Gateway URL into the frontend
- Syncs the website to S3
- Invalidates the CloudFront cache
To destroy: push a commit with "destroy" in the message, or use workflow dispatch.
How the Web App Works
Here's the flow when a user uploads an image:
-
Frontend calls
POST /uploadwith filename and mode (celebrity or labels) - Presign Lambda generates a presigned S3 URL and returns it
- Frontend uploads the image directly to S3 using the presigned URL
- S3 trigger fires and invokes the Processor Lambda
- Processor Lambda calls Rekognition, fetches Wikipedia bios (for celebrities), and saves results to DynamoDB
-
Frontend polls
GET /results/{imageId}until results are ready - Results are displayed with confidence scores, bios, and links
Security Considerations
A few things I added to keep costs and abuse in check:
- API Gateway throttling — 20 requests/second max, burst 50
- Presigned URL expiry — Upload links valid for 5 minutes only
- Content type validation — Server-side check for JPEG/PNG only
- S3 private — No public access, CloudFront with OAC only
- HTTPS enforced — TLS 1.2+ via CloudFront
- DynamoDB TTL — Results auto-delete after 7 days
- No secrets in code — Certificate ARN and zone ID stored as GitHub Secrets
- Lambda Concurrency — You can set reserved concurrency to cap how many Lambda instances run simultaneously, preventing cost spikes. My account's default limit (10 concurrent executions) was already low enough to act as a natural safeguard.
Cost
For ~100 uploads per day, expect about $4-7/month. Rekognition is the main cost at $1 per 1,000 images. Everything else stays in free tier or near-zero at this scale.
Wrapping Up
The beauty of serverless is you pay nothing when nobody's using it, and it scales automatically when they are. You may see some of the use case of this app in my presentation that inspired the project here.
The complete source code is available at: github.com/Fidelisesq/image-recognition-app
Feel free to fork it, swap in your own domain, and deploy your own version. If you have questions, open an issue on the repo.
Happy building!





Top comments (0)