Complete step-by-step instructions to deploy CivicAI on AWS. Follow each section in order — later services depend on earlier ones.
Region: Use
ap-south-1(Mumbai) throughout. All services must be in the same region.
- S3 Bucket
- DynamoDB Table
- SES Email
- IAM Roles
- EC2 — YOLO Inference Server
- Amazon Bedrock — Enable Claude
- Lambda 1 — generate_upload_url
- Lambda 2 — process_image
- API Gateway
- Connect Frontend
- Testing
This bucket stores uploaded complaint images.
-
Go to S3 → Create bucket
-
Configure:
Setting Value Bucket name civicai-imagesRegion ap-south-1Object Ownership ACLs disabled Block all public access ✅ Enabled (keep all 4 checkboxes checked) Versioning Disabled -
Click Create bucket
After creating the bucket:
- Go to civicai-images → Permissions tab → CORS configuration → Edit
- Paste:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["PUT", "GET"],
"AllowedOrigins": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]- Click Save changes
Important
The CORS config is required — without it, the frontend cannot upload images directly to S3 via presigned URLs.
We will come back to add the S3 → Lambda trigger in Step 8.
Stores all complaint records.
-
Go to DynamoDB → Create table
-
Configure:
Setting Value Table name ComplaintsPartition key incident_id(String)Sort key Leave empty Table settings Default settings Capacity mode On-demand -
Click Create table
The table will store these attributes (created automatically on first write):
| Attribute | Type | Description |
|---|---|---|
incident_id |
String | UUID primary key |
category |
String | pothole, garbage, water, streetlight |
confidence |
String | YOLO confidence (stored as string) |
severity |
String | High, Medium, Low, Pending Review |
department |
String | Mapped department name |
description |
String | Bedrock-generated complaint text |
status |
String | Pending, In Progress, Resolved |
timestamp |
String | ISO 8601 timestamp |
s3_key |
String | S3 object key |
latitude |
String | GPS latitude (optional) |
longitude |
String | GPS longitude (optional) |
Sends email notifications when complaints are processed.
- Go to Amazon SES → Verified identities → Create identity
- Select Email address
- Enter your email (e.g.,
your-email@gmail.com) - Click Create identity
- Check your inbox — click the verification link in the email from AWS
Warning
SES Sandbox Mode: New accounts are in sandbox mode. You can only send to verified emails. For production, request Production Access under SES → Account dashboard → Request production access.
You will use this verified email as SES_SOURCE_EMAIL environment variable in Lambda 2.
You need two IAM roles — one for each Lambda function.
For the generate_upload_url Lambda.
- Go to IAM → Roles → Create role
- Trusted entity: AWS service → Lambda
- Click Next
- Attach these policies:
AWSLambdaBasicExecutionRole(search and check it)
- Click Next → Name:
civicai-lambda-upload-role→ Create role - After creation, click the role → Add permissions → Create inline policy
- Switch to JSON tab and paste:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::civicai-images/*"
}
]
}- Name:
civicai-s3-upload-policy→ Create policy
For the process_image Lambda.
- Go to IAM → Roles → Create role
- Trusted entity: AWS service → Lambda
- Attach:
AWSLambdaBasicExecutionRole - Click Next → Name:
civicai-lambda-process-role→ Create role - Click the role → Add permissions → Create inline policy → JSON:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3Read",
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::civicai-images/*"
},
{
"Sid": "DynamoDB",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:Scan"
],
"Resource": "arn:aws:dynamodb:ap-south-1:*:table/Complaints"
},
{
"Sid": "SES",
"Effect": "Allow",
"Action": [
"ses:SendEmail",
"ses:SendRawEmail"
],
"Resource": "*"
},
{
"Sid": "Bedrock",
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel"
],
"Resource": "arn:aws:bedrock:ap-south-1::foundation-model/anthropic.claude-v2"
}
]
}- Name:
civicai-process-policy→ Create policy
Hosts the FastAPI YOLO microservice that classifies images.
-
Go to EC2 → Launch instance
-
Configure:
Setting Value Name civicai-yolo-serverAMI Amazon Linux 2023 or Ubuntu 22.04 Instance type t3.medium(minimum for YOLO)Key pair Create new or use existing -
Network settings → Create a security group:
Type Port Source Description SSH 22 My IP SSH access Custom TCP 8000 0.0.0.0/0 YOLO FastAPI endpoint -
Storage: 20 GB gp3
-
Click Launch instance
# Connect via SSH
ssh -i your-key.pem ec2-user@<PUBLIC_IP>
# Install Python and dependencies
sudo yum update -y # Amazon Linux
sudo yum install python3 python3-pip -y
# Install packages
pip3 install fastapi uvicorn ultralytics boto3 pillow
# Create the FastAPI app (create predict_server.py)
# Your YOLO model should expose POST /predict
# Input: { "bucket": "civicai-images", "key": "complaints/uuid.jpg" }
# Output: { "category": "pothole", "confidence": 0.92 }
# Run the server
uvicorn predict_server:app --host 0.0.0.0 --port 8000Your YOLO endpoint will be:
http://<EC2_PUBLIC_IP>:8000/predict
You'll use this as the EC2_ENDPOINT env var in Lambda 2.
Tip
For production, use an Elastic IP so the address doesn't change on restart. Go to EC2 → Elastic IPs → Allocate → Associate with your instance.
Used to generate formal complaint text.
- Go to Amazon Bedrock → Model access (left sidebar)
- Click Manage model access
- Find Anthropic → Check Claude (specifically
anthropic.claude-v2) - Click Request model access
- Wait for status to change to Access granted (usually instant)
Important
If Bedrock is not available in ap-south-1, use us-east-1 and update the REGION env var in Lambda 2 accordingly. Check Bedrock region availability.
Generates presigned S3 upload URLs for the frontend.
-
Go to Lambda → Create function
-
Configure:
Setting Value Function name civicai-generate-upload-urlRuntime Python 3.12 Architecture x86_64 Execution role Use existing role → civicai-lambda-upload-role -
Click Create function
- In the Code tab, delete the default
lambda_function.py - Copy-paste the contents of your local file:
backend/generate_upload_url/lambda_function.py - Click Deploy
-
Go to Configuration → Environment variables → Edit
-
Add:
Key Value BUCKET_NAMEcivicai-imagesREGIONap-south-1URL_EXPIRY300 -
Click Save
- Go to Configuration → General configuration → Edit
- Set:
- Timeout: 15 seconds
- Memory: 128 MB
- Click Save
Processes uploaded images: YOLO → severity → department → Bedrock → DynamoDB → SES.
-
Go to Lambda → Create function
-
Configure:
Setting Value Function name civicai-process-imageRuntime Python 3.12 Architecture x86_64 Execution role Use existing role → civicai-lambda-process-role -
Click Create function
This Lambda has multiple files. You must upload as a ZIP:
- On your local machine, navigate to
backend/process_image/ - Select ALL files:
lambda_function.pyconfig.pyaws_utils.pyinference_client.pyseverity_rules.pydepartment_mapper.pyprompt_builder.py
- Right-click → Send to → Compressed (zipped) folder → name it
process_image.zip - In Lambda console → Code tab → Upload from → .zip file → upload
process_image.zip - Click Deploy
The process_image Lambda uses the requests library (not included in Lambda runtime).
Option A — Use a public Lambda Layer:
- Go to Layers → Add a layer
- Choose Specify an ARN
- Paste (for
ap-south-1, Python 3.12):(Check Klayers for latest ARN)arn:aws:lambda:ap-south-1:770693421928:layer:Klayers-p312-requests:5 - Click Add
Option B — Create your own layer:
mkdir python
pip install requests -t python/
zip -r requests-layer.zip python/Upload as a Lambda Layer, then attach it to your function.
-
Go to Configuration → Environment variables → Edit
-
Add:
Key Value TABLE_NAMEComplaintsEC2_ENDPOINThttp://<EC2_PUBLIC_IP>:8000/predictMODEL_IDanthropic.claude-v2SES_SOURCE_EMAILyour-verified-email@gmail.comREGIONap-south-1BUCKET_NAMEcivicai-imagesYOLO_TIMEOUT10 -
Click Save
- Configuration → General configuration → Edit
- Set:
- Timeout: 60 seconds (Bedrock can be slow)
- Memory: 256 MB
- Click Save
Now go back to S3 and connect it:
-
Go to S3 → civicai-images → Properties tab
-
Scroll to Event notifications → Create event notification
-
Configure:
Setting Value Event name complaint-image-uploadedPrefix complaints/Event types ✅ s3:ObjectCreated:PutDestination Lambda function Lambda function civicai-process-image -
Click Save changes
Important
This trigger means: whenever an image is uploaded to complaints/ in S3, the process_image Lambda fires automatically. No API call needed.
Creates the REST API that the frontend calls.
-
Go to API Gateway → Create API
-
Choose REST API (not HTTP API) → Build
-
Configure:
Setting Value API name CivicAI-APIEndpoint type Regional -
Click Create API
You need these routes:
POST /upload/presign → civicai-generate-upload-url Lambda
-
Click Create Resource
- Resource name:
upload - Resource path:
/upload - ✅ Enable CORS
- Click Create Resource
- Resource name:
-
Select
/upload→ Create Resource again- Resource name:
presign - Resource path:
/presign - ✅ Enable CORS
- Click Create Resource
- Resource name:
-
Select
/upload/presign→ Create Method- Method type: POST
- Integration type: Lambda Function
- ✅ Lambda Proxy Integration
- Lambda function:
civicai-generate-upload-url - Click Create Method
- Select
/upload/presignresource - Click Enable CORS
- Check:
POST,OPTIONS - Access-Control-Allow-Origin:
* - Access-Control-Allow-Headers:
Content-Type,Authorization - Click Save
- Click Deploy API
- Stage: New Stage → Stage name:
prod - Click Deploy
After deployment, you'll see:
Invoke URL: https://xxxxxxxxxx.execute-api.ap-south-1.amazonaws.com/prod
Copy this URL — you need it for the frontend.
Open civicai-frontend/.env and set:
VITE_API_BASE_URL=https://xxxxxxxxxx.execute-api.ap-south-1.amazonaws.com/prodReplace xxxxxxxxxx with your actual API Gateway ID.
npm run devThe frontend will now make real API calls to your AWS backend.
curl -X POST https://YOUR_API_URL/prod/upload/presign \
-H "Content-Type: application/json" \
-d '{"fileName": "test.jpg", "fileType": "image/jpeg"}'Expected response:
{
"incident_id": "uuid-string",
"upload_url": "https://civicai-images.s3.amazonaws.com/...",
"s3_key": "complaints/uuid-string.jpg"
}curl -X PUT "<upload_url_from_step_1>" \
-H "Content-Type: image/jpeg" \
--data-binary @test-image.jpg- Go to CloudWatch → Log groups →
/aws/lambda/civicai-process-image - Check the latest log stream — you should see processing logs
- Go to DynamoDB → Complaints table → Explore items — the record should appear
- Open
http://localhost:5173 - Login with any phone number + OTP
123456 - Go to Report an Issue → Upload a photo
- Click Analyze with AI → should trigger real YOLO analysis
- Review and Submit Complaint
- Verify in DynamoDB and your email inbox
| Lambda | Variable | Example Value |
|---|---|---|
| Lambda 1 | BUCKET_NAME |
civicai-images |
| Lambda 1 | REGION |
ap-south-1 |
| Lambda 1 | URL_EXPIRY |
300 |
| Lambda 2 | TABLE_NAME |
Complaints |
| Lambda 2 | BUCKET_NAME |
civicai-images |
| Lambda 2 | EC2_ENDPOINT |
http://1.2.3.4:8000/predict |
| Lambda 2 | MODEL_ID |
anthropic.claude-v2 |
| Lambda 2 | SES_SOURCE_EMAIL |
your-email@gmail.com |
| Lambda 2 | REGION |
ap-south-1 |
| Lambda 2 | YOLO_TIMEOUT |
10 |
| Frontend | VITE_API_BASE_URL |
https://xxx.execute-api.ap-south-1.amazonaws.com/prod |
| Service | Estimated Cost |
|---|---|
| Lambda | Free tier (1M requests/month) |
| S3 | ~$0.02/GB stored |
| DynamoDB | Free tier (25 GB + 25 WCU/RCU) |
| API Gateway | Free tier (1M calls/month) |
| SES | $0.10 per 1000 emails |
EC2 t3.medium |
|
| Bedrock Claude v2 | ~$0.008 per 1K input tokens |
Tip
Total prototype cost: Under $35/month, mostly from EC2. Stop the EC2 instance when not testing to save costs.