Serverless Image Tagging Pipeline
Build an AI pipeline that auto-tags and moderates images using AWS Rekognition.
Introduction
30 Second Summary
Think about social media platforms that handle millions of photo uploads every day. Every image needs to be checked for inappropriate content and tagged with searchable labels, all without a human reviewing each one.
In this project, you will build a serverless pipeline on AWS that automatically analyzes uploaded images using AI. When you drop a photo into cloud storage, your pipeline detects objects, flags unsafe content, and stores structured results in a database.
What You'll Build
A fully automated image processing pipeline that connects Amazon S3, AWS Lambda, Amazon Rekognition, and Amazon DynamoDB into an event-driven architecture.
By the end of this project, you'll have:
- An S3 bucket configured with event notifications to automatically trigger processing when you upload an image.
- A Lambda function in Python that calls Amazon Rekognition to detect objects, scenes, and inappropriate content.
- A DynamoDB table storing image metadata including labels, moderation status, confidence scores, and timestamps.
- Secret Mission: Extend the pipeline with text detection and smart file filtering so only image files trigger your function.
Are there any prerequisites?
You need an AWS account (free tier eligible preferred) and basic familiarity with Python syntax. No prior AWS experience is required.
Create the S3 Bucket and DynamoDB Table
Your serverless pipeline needs two storage layers. One for the raw images going in, and another for the structured metadata coming out.
In this step, you'll create an S3 bucket to receive uploaded images and a DynamoDB table to store the AI-generated tags and moderation results.
In this step, get ready to:
- Create an S3 bucket for image uploads.
- Create a DynamoDB table to store image metadata.
- Verify both resources are active and ready.
Create your S3 bucket
S3 is where your images will land when they're uploaded. Once an image arrives, it will trigger the rest of the pipeline automatically.
- In the AWS Management Console, click the search bar at the top.
- Type S3 and select S3 from the results.
- Click Create bucket.
- For Bucket type, keep General purpose selected.
- In the Bucket name field, enter image-tagging-pipeline- followed by your initials (for example, image-tagging-pipeline-jm).
- Confirm the AWS Region is set to US East (N. Virginia) us-east-1.
- Leave all other settings as their defaults. Block all public access should remain checked.
- Scroll to the bottom and click Create bucket.
Why does the bucket name need to be unique?
S3 bucket names are globally unique across all AWS accounts. Adding your initials ensures no one else has already claimed the name.
You should see a green success banner and your new bucket listed on the S3 Buckets page.
Seeing a 'Bucket name already exists' error?
Someone else already owns that exact name. Try adding extra characters to make it unique (for example, image-tagging-pipeline-jm2026).
Make sure you note down the exact bucket name you used. You'll need it in later steps.
Help me fix my S3 bucket creation error.
Create your DynamoDB table
DynamoDB is a NoSQL database that will store the structured results from each image analysis. Each record will hold the detected labels, moderation status, and a timestamp.
- Click the search bar at the top of the AWS Console.
- Type DynamoDB and select DynamoDB from the results.
- In the left sidebar, click Tables.
- Click Create table.
- In the Table name field, enter ImageMetadata.
- In the Partition key field, enter imageKey.
- Keep the partition key type set to String.
- Under Table settings, select Customize settings.
- Under Read/write capacity settings, select On-demand as the capacity mode.
- Leave all other settings as their defaults.
- Scroll to the bottom and click Create table.
What is a partition key?
A partition key is the unique identifier for each item in a DynamoDB table. In your case, imageKey will be the S3 file path of each uploaded image. This means every image gets exactly one metadata record.
Why On-demand capacity mode?
On-demand mode means you only pay for the read and write requests you actually make. For a learning project processing a handful of images, this keeps things simple and avoids configuring capacity limits upfront.
The table creation takes a few seconds. Wait until the Status column shows Active before moving on.
Table stuck in 'Creating' status?
Give it up to 30 seconds. Click the refresh icon (circular arrow) in the Tables view to update the status. If it stays in Creating for more than a minute, check that you're in the us-east-1 region.
Help me troubleshoot my DynamoDB table creation.
Verify both resources are ready
- Navigate back to S3 using the search bar.
- Confirm your bucket (image-tagging-pipeline- followed by your initials) appears in the bucket list.
- Navigate back to DynamoDB using the search bar.
- Click Tables in the left sidebar.
- Confirm the ImageMetadata table shows a status of Active.
Both storage layers are in place. Next up, you'll create the IAM role that gives your Lambda function permission to read from S3, call Rekognition, and write to DynamoDB.
Create the IAM Role for Lambda
Your S3 bucket and DynamoDB table are ready to store images and metadata. Now you need to give your Lambda function permission to access these resources.
AWS services cannot access each other by default. You need to create an IAM role that grants your Lambda function the exact permissions it needs to read from S3, call Rekognition, and write to DynamoDB.
In this step, get ready to:
- Create an IAM role with Lambda as the trusted entity.
- Attach four managed policies granting access to S3, Rekognition, DynamoDB, and CloudWatch Logs.
- Verify the role and its attached policies.
Navigate to IAM and start creating the role
An IAM role defines what an AWS service is allowed to do. By selecting Lambda as the trusted entity, you tell AWS that the Lambda service is allowed to assume (use) this role whenever your function runs.
- In the AWS Console, type IAM in the top search bar and select IAM from the results.
- In the left navigation pane, click Roles.
- Click the Create role button.
- For Trusted entity type, select AWS service.
- Under Use case, select Lambda.
- Click Next.
What is an IAM role?
An IAM role is a set of permissions that an AWS service can assume. When your Lambda function runs, it automatically picks up the role you assign and inherits all the permissions attached to it.
By choosing Lambda as the trusted entity, you are telling AWS that only the Lambda service is allowed to use this role. No other service or user can assume it.
Attach the four managed policies
Your Lambda function needs four specific capabilities. AWS provides pre-built managed policies for each one.
- On the Add permissions page, search for each of the following policies individually and tick its checkbox.
- Search for AmazonRekognitionReadOnlyAccess and tick its checkbox.
- Search for AmazonS3ReadOnlyAccess and tick its checkbox.
- Search for AmazonDynamoDBFullAccess and tick its checkbox.
- Search for AWSLambdaBasicExecutionRole and tick its checkbox.
- Confirm all four policies are selected, then click Next.
Why these four policies?
- AmazonRekognitionReadOnlyAccess lets Lambda call Rekognition APIs like DetectLabels and DetectModerationLabels.
- AmazonS3ReadOnlyAccess lets Lambda read the uploaded image from the S3 bucket.
- AmazonDynamoDBFullAccess lets Lambda write results to the ImageMetadata table.
- AWSLambdaBasicExecutionRole lets Lambda send logs to CloudWatch so you can debug your function.
Name the role and create it
- In the Role name field, enter ImageTaggingLambdaRole.
- Scroll down and click Create role.
Role names are permanent
You cannot rename an IAM role after creation. Make sure you type ImageTaggingLambdaRole exactly as shown. You will reference this name when creating your Lambda function in the next step.
Verify the role was created successfully
You should see a green success banner at the top of the page after the role is created.
- In the IAM Roles list, type ImageTaggingLambdaRole in the search box.
- Click on ImageTaggingLambdaRole to open the role details.
- Select the Permissions tab and confirm all four policies are listed: AmazonRekognitionReadOnlyAccess, AmazonS3ReadOnlyAccess, AmazonDynamoDBFullAccess, and AWSLambdaBasicExecutionRole.
Don't see all four policies?
If a policy is missing, click Add permissions then Attach policies on the role's Permissions tab. Search for the missing policy and attach it.
Help me troubleshoot my IAM role policies.
Your IAM role is locked and loaded. Next up, you will build the Lambda function itself and wire it to this role so it can process images automatically.
Build the Lambda Function
Your IAM role is ready to grant permissions. Now it's time to build the core of your pipeline.
The Lambda function is the brain of this architecture. When an image lands in your S3 bucket, Lambda will automatically call Rekognition to analyze the image, then write the results to DynamoDB.
In this step, get ready to:
- Create a Lambda function in the AWS Console with the correct runtime and role.
- Write Python code that calls Rekognition and stores results in DynamoDB.
- Deploy and test the function with a sample event.
Create the Lambda function
- In the AWS Console search bar at the top, type Lambda and select the Lambda service.
- Click Create function.
- Select Author from scratch.
- Set the Function name to ImageTaggingFunction.
- Set the Runtime to Python 3.12.
- Expand the Change default execution role section.
- Select Use an existing role.
- In the Existing role dropdown, choose ImageTaggingLambdaRole.
- Click Create function.
You should see a green "Successfully created the function" banner at the top of the page.
Why Author from scratch?
Lambda offers blueprints and container image options, but Author from scratch gives you a clean editor with a minimal template. Since boto3 (the AWS SDK for Python) is already included in the Lambda Python runtime, you don't need any external packages or deployment bundles.
Don't see ImageTaggingLambdaRole in the dropdown?
Make sure you're in the same region (us-east-1) where you created the role. Also confirm the role's trusted entity is set to Lambda, not another service.
help me find my IAM role for Lambda
Write the image tagging code
Lambda created a default lambda_function.py file with placeholder code. You'll replace it entirely with the image tagging logic.
- In the Code tab, click inside the code editor.
- Select all the existing code (Cmd+A on macOS or Ctrl+A on Windows) and delete it.
- Paste the following code into the editor:
import json
import boto3
from datetime import datetime, timezone
rekognition = boto3.client('rekognition')
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('ImageMetadata')
def lambda_handler(event, context):
# Extract bucket name and object key from the S3 event
record = event['Records'][0]
bucket = record['s3']['bucket']['name']
key = record['s3']['object']['key']
print(f"Processing image: s3://{bucket}/{key}")
# Detect labels (objects, scenes, concepts)
labels_response = rekognition.detect_labels(
Image={'S3Object': {'Bucket': bucket, 'Name': key}},
MaxLabels=10,
MinConfidence=75
)
labels = [
{'name': label['Name'], 'confidence': str(round(label['Confidence'], 2))}
for label in labels_response['Labels']
]
# Detect moderation labels (inappropriate content)
moderation_response = rekognition.detect_moderation_labels(
Image={'S3Object': {'Bucket': bucket, 'Name': key}},
MinConfidence=60
)
moderation_labels = [
{'name': mod['Name'], 'confidence': str(round(mod['Confidence'], 2))}
for mod in moderation_response['ModerationLabels']
]
is_safe = len(moderation_labels) == 0
# Write results to DynamoDB
table.put_item(
Item={
'imageKey': key,
'bucket': bucket,
'labels': labels,
'moderationLabels': moderation_labels,
'isSafe': is_safe,
'processedAt': datetime.now(timezone.utc).isoformat()
}
)
print(f"Results saved. Labels: {[l['name'] for l in labels]}, Safe: {is_safe}")
return {
'statusCode': 200,
'body': json.dumps({
'image': key,
'labels': [l['name'] for l in labels],
'isSafe': is_safe
})
}
What does this code do?
This function runs every time an image is uploaded to your S3 bucket. Here's the flow:
- Lines 11-14 extract the bucket name and file key from the S3 event that triggered the function.
- Lines 18-23 call Rekognition's detect_labels API, which identifies objects, scenes, and concepts in the image (up to 10 labels with at least 75% confidence).
- Lines 31-35 call detect_moderation_labels, which flags inappropriate content like violence or explicit material.
- Lines 44-53 write everything to your ImageMetadata DynamoDB table, including the labels, moderation status, and a timestamp.
Now set the function timeout so it has enough time to process images.
- Click the Configuration tab (next to Code and Test).
- Select General configuration in the left sidebar.
- Click Edit.
- Change the Timeout value to 30 seconds.
- Click Save.
You should see the timeout displayed as 0 min 30 sec in the General configuration panel.
Why increase the timeout?
Lambda's default timeout is 3 seconds. Calling two Rekognition APIs and writing to DynamoDB can take longer than that, especially on cold starts. 30 seconds gives the function plenty of breathing room.
Timeout still showing 3 seconds?
Make sure you clicked Save after editing. If the page refreshed without saving, click Edit again and re-enter 30 seconds.
help me configure my Lambda timeout
✔️ Awesome, I've got everything!
Great. Make sure you've saved the timeout change before moving on.
ⓧ I'd like to double check the full code
Here's the complete lambda_function.py for reference:
import json
import boto3
from datetime import datetime, timezone
rekognition = boto3.client('rekognition')
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('ImageMetadata')
def lambda_handler(event, context):
# Extract bucket name and object key from the S3 event
record = event['Records'][0]
bucket = record['s3']['bucket']['name']
key = record['s3']['object']['key']
print(f"Processing image: s3://{bucket}/{key}")
# Detect labels (objects, scenes, concepts)
labels_response = rekognition.detect_labels(
Image={'S3Object': {'Bucket': bucket, 'Name': key}},
MaxLabels=10,
MinConfidence=75
)
labels = [
{'name': label['Name'], 'confidence': str(round(label['Confidence'], 2))}
for label in labels_response['Labels']
]
# Detect moderation labels (inappropriate content)
moderation_response = rekognition.detect_moderation_labels(
Image={'S3Object': {'Bucket': bucket, 'Name': key}},
MinConfidence=60
)
moderation_labels = [
{'name': mod['Name'], 'confidence': str(round(mod['Confidence'], 2))}
for mod in moderation_response['ModerationLabels']
]
is_safe = len(moderation_labels) == 0
# Write results to DynamoDB
table.put_item(
Item={
'imageKey': key,
'bucket': bucket,
'labels': labels,
'moderationLabels': moderation_labels,
'isSafe': is_safe,
'processedAt': datetime.now(timezone.utc).isoformat()
}
)
print(f"Results saved. Labels: {[l['name'] for l in labels]}, Safe: {is_safe}")
return {
'statusCode': 200,
'body': json.dumps({
'image': key,
'labels': [l['name'] for l in labels],
'isSafe': is_safe
})
}
Deploy the function and run a test
- Click the Code tab to return to the code editor.
- Click the Deploy button above the editor.
You should see a green "Successfully updated the function" banner. Your code is now live.
Next, create a test event to verify the function can be invoked.
- Click the Test tab.
- Under Test event, select Create new event.
- In the Template dropdown, search for and select s3-put.
- Enter a name for the event, such as testImageUpload.
- In the event JSON, find the bucket > name field and replace its value with your actual S3 bucket name (e.g. image-tagging-pipeline-jd).
- Find the object > key field and replace its value with test-image.jpg.
- Click Save to save the test event.
- Click Test to invoke the function.
Seeing an error? That's expected.
The test will fail because no image called test-image.jpg actually exists in your S3 bucket yet. Rekognition can't analyze a file that isn't there.
The important thing is that your function was invoked successfully. You should see an error message from Rekognition mentioning the object does not exist. This confirms your code is deployed, the IAM permissions let Lambda call Rekognition, and the function runs without syntax errors.
Seeing a timeout or permission error instead?
If you see a "Task timed out" error, double-check that your timeout is set to 30 seconds under Configuration > General configuration.
If you see an "AccessDenied" error, confirm that ImageTaggingLambdaRole has the AmazonRekognitionReadOnlyAccess and AmazonS3ReadOnlyAccess policies attached.
help me debug my Lambda function test error
Your Lambda function is deployed and ready to process images. In the next step, you'll connect it to your S3 bucket so it fires automatically whenever you upload a photo.
Connect S3 to Lambda and Test End-to-End
Your Lambda function is built, deployed, and ready to process images. The missing piece is the connection between your S3 bucket and the function.
In this step, you will wire your S3 bucket to your Lambda function so that every image upload automatically triggers processing. Then you will upload a real image and confirm the entire pipeline works from end to end.
In this step, get ready to:
- Add an S3 trigger to your Lambda function.
- Upload a test image to your S3 bucket.
- Verify results in CloudWatch Logs and DynamoDB.
Add an S3 trigger to your Lambda function
An S3 event notification tells AWS to invoke your Lambda function whenever a new object is created in the bucket. This is what makes the pipeline fully automatic.
- Return to the Lambda console and open your ImageTaggingFunction function.
- Select the Configuration tab.
- Choose Triggers in the left sidebar.
- Click Add trigger.
- In the trigger configuration dropdown, select S3.
- Under Bucket, select your image-tagging-pipeline- bucket.
- Under Event types, confirm that All object create events is selected.
- Under Recursive invocation, check the box to acknowledge that using the same S3 bucket for input and output is not recommended.
- Click Add.
What is recursive invocation?
AWS warns you about recursive invocation because if a Lambda function writes back to the same bucket that triggers it, it creates an infinite loop. Your function writes to DynamoDB instead of back to S3, so there is no risk here.
- Confirm the trigger appears in the Triggers list with the source showing your S3 bucket name.
Seeing an error when adding the trigger?
If you see an error about unable to validate destination configurations, it means your bucket may have a stale event notification pointing to a deleted function. Navigate to S3, open your bucket's Properties tab, scroll to Event notifications, and delete any old notification entries. Then try adding the trigger again.
Help me fix this S3 trigger error
Upload a test image to your S3 bucket
With the trigger active, any image you upload to the bucket will automatically invoke your Lambda function. Time to test it with a real photo.
- Navigate to the S3 console and open your image-tagging-pipeline- bucket.
- Click Upload.
- Click Add files and choose a JPEG or PNG photo from your computer.
- Click Upload.
Pick an image with recognizable objects
Choose a photo with clear subjects like people, animals, buildings, or office equipment. Amazon Rekognition works best with images that have distinct visual elements. A workshop scene, group photo, or desk setup all work well.
What happens behind the scenes?
The moment your file finishes uploading, S3 fires an event notification to your Lambda function. The function receives the bucket name and object key, sends the image to Rekognition for label detection and content moderation, then writes all results to your ImageMetadata DynamoDB table.
Verify the pipeline worked end-to-end
You will now check two places to confirm the entire pipeline executed successfully: CloudWatch Logs for the function execution, and your DynamoDB table for the stored results.
- Return to the Lambda console and open ImageTaggingFunction.
- Select the Monitor tab.
- Click View CloudWatch logs.
- Select the most recent log stream (the one at the top of the list).
- Look for log entries showing your function's print output. You should see a line with your image path followed by the detected labels and a Safe status.
What if the log stream is empty?
Lambda logs can take up to a minute to appear in CloudWatch. If you do not see entries yet, wait 60 seconds and refresh the page. If multiple log streams exist, try the most recent one at the top.
- Now navigate to DynamoDB in the AWS Console.
- Click Tables in the left sidebar.
- Select your ImageMetadata table.
- Click Explore table items.
- You should see an entry with your image's filename as the imageKey, along with labels, moderationLabels, isSafe, and processedAt attributes.
No item showing in DynamoDB?
Check that your uploaded file is a JPEG or PNG. Rekognition only supports these formats and will throw an error for other file types.
Also confirm the S3 trigger is active by returning to the Lambda console, selecting Configuration, then Triggers. If the trigger is missing, re-add it following the steps above.
Help me debug why my DynamoDB item is missing
Your serverless image processing pipeline is fully operational. Every image uploaded to your S3 bucket is now automatically analyzed for content and stored with structured metadata in DynamoDB.
Secret mission
Extend the Pipeline with Text Detection and File Filtering
Your pipeline currently processes every file uploaded to the bucket, regardless of type. What happens if someone uploads a spreadsheet or a text document? Rekognition will throw an error. In this secret mission, you'll add smart file filtering and text extraction (OCR) to make your pipeline production-ready.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses serverless AWS resources that only incur costs when triggered, so a few test images will not generate charges within free tier limits.
Cost warning
Amazon Rekognition's free tier (1,000 images per month per API group) expires after 12 months from account creation. If you continue uploading images after that period without credits remaining, you will incur charges per image processed.
Lambda and DynamoDB have always-free tiers that do not expire. S3 storage charges are negligible for a handful of test images.
Resources you used:
- S3 bucket image-tagging-pipeline-{initials} (stores uploaded images).
- Lambda function ImageTaggingFunction (processes images with Rekognition).
- DynamoDB table ImageMetadata (stores image analysis results).
- IAM role ImageTaggingLambdaRole (grants Lambda permissions to S3, Rekognition, DynamoDB, and CloudWatch).
Keep everything running
No action needed. Choose this if you plan to keep experimenting with the pipeline.
- Your serverless resources only cost money when invoked. Leaving them idle does not generate charges (aside from minimal S3 storage for uploaded images).
- You can upload more images anytime to test different label detection or moderation scenarios.
Pause - I'll come back to this later
Serverless resources like Lambda and DynamoDB have no running state to pause. They only activate when triggered.
- If you want to reduce storage costs, delete the test images from your S3 bucket while keeping the bucket and pipeline intact.
- To delete images, navigate to S3 in the AWS Console, open your bucket, select all objects, and choose Delete.
- When you return, upload a new image to trigger the pipeline again.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow these steps in order.
Delete the Lambda function:
- In the AWS Console, navigate to Lambda and open the Functions page.
- Select ImageTaggingFunction.
- Choose Actions, then Delete.
- Type confirm in the text field and choose Delete.
Delete the DynamoDB table:
- Navigate to DynamoDB in the AWS Console.
- In the left navigation pane, choose Tables.
- Select the checkbox beside ImageMetadata.
- Choose Delete and confirm the deletion when prompted.
Empty and delete the S3 bucket:
- Navigate to S3 in the AWS Console and choose General purpose buckets in the left navigation pane.
- Select the radio button next to your bucket (image-tagging-pipeline-{initials}) and choose Empty.
- Type permanently delete in the confirmation field and choose Empty.
- Return to the bucket list. Select the radio button next to your bucket and choose Delete.
- Type the bucket name to confirm and choose Delete bucket.
Delete the IAM role:
- Navigate to IAM in the AWS Console.
- In the left navigation pane, choose Roles.
- Search for ImageTaggingLambdaRole and select it.
- Choose Delete.
- Enter the role name in the text field and choose Delete to confirm.
Why delete in this order?
Deleting the Lambda function first ensures no new invocations occur while you clean up the other resources. The S3 bucket must be emptied before it can be deleted. The IAM role goes last because the other services reference it.
That's a Wrap!
That's a Wrap!
Nice work! You just built a production-grade serverless image processing pipeline that automatically tags, moderates, and catalogs uploaded photos using AWS managed services.
You've learned how to:
- Built an event-driven architecture connecting S3, Lambda, Rekognition, and DynamoDB into a fully automated pipeline.
- Used Amazon Rekognition to automatically detect objects, scenes, and inappropriate content in images with confidence scoring.
- Stored structured AI-generated metadata in a NoSQL database for downstream querying and consumption.
- Extended the pipeline with text detection (OCR) and smart file filtering using suffix-based S3 event triggers.
Ready to quiz yourself?