AWS ECS Fargate · Jenkins CI/CD · Route 53 · Terraform IaC
Region:ap-south-1(Mumbai) — closest to Pune
- Architecture Overview
- Important: Serial Port Limitation
- Prerequisites
- Repository Structure
- Step 1 — Bootstrap AWS Account
- Step 2 — Launch Jenkins EC2
- Step 3 — Configure Jenkins
- Step 4 — Provision Infrastructure with Terraform
- Step 5 — Build & Push First Docker Image
- Step 6 — Create Jenkins Pipeline
- Step 7 — DNS & HTTPS with Route 53
- Step 8 — Verify End-to-End
- Day-2 Operations
- Troubleshooting
- Cost Estimate
Developer
│ git push → GitHub/CodeCommit
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ Jenkins EC2 (t3.medium, ap-south-1) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Pipeline stages: │ │
│ │ Checkout → Lint → Test → Build → Push ECR → Deploy ECS │ │
│ └──────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
│ docker push
▼
┌────────────────────┐
│ Amazon ECR │ ← Docker image registry
└────────────────────┘
│ image pull
▼
┌─────────────────────────────────────────────────────────────┐
│ VPC 10.0.0.0/16 │
│ │
│ Public Subnets (1a, 1b) Private Subnets (1a, 1b) │
│ ┌───────────────────┐ ┌──────────────────────┐ │
│ │ Application LB │────────▶│ ECS Fargate Tasks │ │
│ │ (HTTPS :443) │ │ Flask app :5000 │ │
│ └───────────────────┘ └──────────────────────┘ │
│ ▲ │ │
│ │ A-record alias │ outbound via NAT │
└───────────┼─────────────────────────────┼───────────────────┘
│ ▼
Route 53 HostedZone NAT Gateways
tuner.yourdomain.com
Traffic flow:
User → Route 53 DNS → ALB (HTTPS/TLS termination) → ECS Fargate tasks → Flask app
Your app uses pyserial to talk to CAN/UART hardware through a physical USB port.
AWS Fargate containers do not have physical USB/serial ports.
| Option | Description | Complexity |
|---|---|---|
| Hybrid (Recommended) | Flask runs on-prem on a Raspberry Pi or small server connected to the ESC hardware. Only the web UI is reverse-proxied to the cloud. | Low |
| EC2 + USB over IP | Run the app on an EC2-like on-prem server, tunnel serial via ser2net or RFC 2217 over a VPN to the cloud instance. |
Medium |
| EC2 (not Fargate) | Use an EC2 instance with AWS Nitro and pass a USB device via udev rules + EC2 serial console. | High, limited |
| This guide (Cloud UI only) | Deploy the web dashboard to ECS. Connect hardware via the UART/CAN endpoint from a local machine that has the device. Works if users have the USB device attached locally. | Minimal change |
This guide deploys the full app to ECS. For hardware-connected use, run the app on a local machine with the USB device and use the deployed version as a remote-access mirror / config backup tool.
- AWS CLI v2 installed and configured (
aws configure) - Terraform >= 1.6.0
- Docker Desktop / Docker Engine
- Git
- IAM user with AdministratorAccess (for initial setup; lock down after)
- A registered domain in Route 53 (or transferred into Route 53)
- AWS account ID handy (12-digit number)
your-repo/
├── app.py ← Flask application
├── waveshare_can.py
├── can_tuner3.py
├── templates/
│ └── index.html
├── static/
│ ├── styles.css
│ └── app.js
├── tests/ ← Add your pytest tests here
│ └── test_app.py
│
└── deployment/
├── docker/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── docker-compose.yml ← Local dev
├── nginx/
│ └── nginx.conf
├── jenkins/
│ ├── Jenkinsfile
│ └── jenkins-setup.sh
└── terraform/
├── main.tf
├── variables.tf
├── modules/
│ ├── vpc/main.tf
│ ├── alb/main.tf
│ ├── ecs/main.tf
│ └── route53/main.tf
└── environments/
└── prod/
└── terraform.tfvars.example
Move files into correct positions:
# From your repo root
mkdir -p templates static tests
cp deployment/docker/Dockerfile . # OR build context = repo root
mv index.html templates/
mv styles.css app.js static/
cp deployment/docker/requirements.txt .# Replace ACCOUNT_ID with your 12-digit AWS account ID
aws s3api create-bucket \
--bucket can-tuner-terraform-state \
--region ap-south-1 \
--create-bucket-configuration LocationConstraint=ap-south-1
aws s3api put-bucket-versioning \
--bucket can-tuner-terraform-state \
--versioning-configuration Status=Enabled
aws s3api put-bucket-encryption \
--bucket can-tuner-terraform-state \
--server-side-encryption-configuration \
'{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'aws dynamodb create-table \
--table-name can-tuner-tf-lock \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region ap-south-1aws iam create-user --user-name jenkins-deployer
# Attach policy (save as jenkins-policy.json first — see below)
aws iam put-user-policy \
--user-name jenkins-deployer \
--policy-name jenkins-deploy-policy \
--policy-document file://jenkins-policy.json
# Create access key → SAVE THESE, you'll add them to Jenkins credentials
aws iam create-access-key --user-name jenkins-deployerjenkins-policy.json (minimum permissions):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ecs:DescribeTaskDefinition",
"ecs:RegisterTaskDefinition",
"ecs:UpdateService",
"ecs:DescribeServices",
"ecs:ListTasks",
"ecs:DescribeTasks"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": ["iam:PassRole"],
"Resource": "arn:aws:iam::*:role/can-tuner-ecs-*"
}
]
}# This must be done in the SAME region as your ALB (ap-south-1)
aws acm request-certificate \
--domain-name "yourdomain.com" \
--subject-alternative-names "*.yourdomain.com" \
--validation-method DNS \
--region ap-south-1Go to ACM Console → Certificates → your cert → Create DNS records in Route 53
Wait until status shows Issued (5–10 minutes)
-
Go to EC2 Console → Launch Instance
-
Name:
jenkins-server -
AMI: Ubuntu Server 22.04 LTS (64-bit x86)
-
Instance type:
t3.medium(2 vCPU, 4 GB RAM — minimum for Jenkins + Docker) -
Key pair: Create or select an existing key pair
-
Security Group — Inbound rules:
Port Source Purpose 22 Your IP only SSH 8080 Your IP only Jenkins UI 443 0.0.0.0/0 If you put Jenkins behind HTTPS -
Storage: 30 GB gp3
-
IAM Instance Profile: Attach a role with
AmazonEC2ContainerRegistryPowerUser(optional — you can use static credentials instead)
# SSH into the instance
ssh -i your-key.pem ubuntu@<EC2_PUBLIC_IP>
# Upload and run the setup script
scp -i your-key.pem deployment/jenkins/jenkins-setup.sh ubuntu@<EC2_PUBLIC_IP>:~/
ssh -i your-key.pem ubuntu@<EC2_PUBLIC_IP> "sudo bash jenkins-setup.sh"The script prints the Jenkins initial admin password at the end. Save it.
Open http://<JENKINS_EC2_IP>:8080 and enter the initial admin password.
After unlocking, choose Install suggested plugins, then also install:
- Blue Ocean (better pipeline UI)
- Amazon ECR (
amazon-ecr) - AWS Credentials (
aws-credentials) - AWS Steps (
pipeline-aws) - Slack Notification (
slack) - AnsiColor (
ansicolor) - Git (usually pre-installed)
Go to Manage Jenkins → Credentials → System → Global credentials → Add Credential
| ID | Type | Value |
|---|---|---|
aws-jenkins-deploy |
AWS Credentials | Access key ID + Secret from Step 5c |
ecr-registry-url |
Secret text | ACCOUNT_ID.dkr.ecr.ap-south-1.amazonaws.com |
app-health-url |
Secret text | https://tuner.yourdomain.com/health |
slack-token |
Secret text | Your Slack bot token |
Manage Jenkins → System → Slack:
- Workspace: your Slack workspace
- Credential:
slack-token - Default channel:
#deployments
# On your LOCAL machine (not Jenkins)
cd deployment/terraform
# Copy and fill in your values
cp environments/prod/terraform.tfvars.example terraform.tfvars
nano terraform.tfvars # Fill in domain_name, certificate_arn, account ID
# Initialise (downloads providers, connects to S3 backend)
terraform init
# Preview what will be created (~30 resources)
terraform plan -out=tfplan
# Apply — takes ~8 minutes
terraform apply tfplanTerraform creates:
- VPC with public + private subnets across 2 AZs
- Internet Gateway + NAT Gateways
- Application Load Balancer + Target Group + Listeners
- ECS Cluster (Fargate) + Task Definition + Service
- ECR Repository
- Route 53 A-record alias
- IAM roles for ECS
- CloudWatch Log Group
- Auto Scaling policy
Save the outputs:
terraform output
# app_url = "https://tuner.yourdomain.com"
# ecr_repository_url = "ACCOUNT_ID.dkr.ecr.ap-south-1.amazonaws.com/can-tuner"
# ecs_cluster_name = "can-tuner-prod"
# ecs_service_name = "can-tuner-prod-svc"Before Jenkins can deploy, ECR needs at least one image.
# Set variables
AWS_REGION=ap-south-1
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
ECR_URI="${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/can-tuner"
# Login to ECR
aws ecr get-login-password --region $AWS_REGION \
| docker login --username AWS --password-stdin $ECR_URI
# Build from repo root
docker build -f deployment/docker/Dockerfile -t can-tuner:latest .
# Tag and push
docker tag can-tuner:latest "${ECR_URI}:latest"
docker push "${ECR_URI}:latest"
echo "First image pushed: ${ECR_URI}:latest"- New Item → Pipeline → Name:
can-tuner-deploy - Build Triggers: Check
GitHub hook trigger for GITScm polling
(or set up a webhook in GitHub:http://<JENKINS_IP>:8080/github-webhook/) - Pipeline Definition:
Pipeline script from SCM - SCM: Git
- Repository URL: your GitHub/GitLab/CodeCommit URL
- Credentials: Add your Git credentials
- Branch:
*/main - Script Path:
deployment/jenkins/Jenkinsfile
Edit deployment/jenkins/Jenkinsfile and update:
ECR_REGISTRY = credentials('ecr-registry-url')
ECS_CLUSTER = 'can-tuner-prod' // from terraform output
ECS_SERVICE = 'can-tuner-prod-svc' // from terraform outputClick Build Now in Jenkins. Watch the Blue Ocean view.
Pipeline stages visualised:
[Checkout] → [Lint] → [Test] → [Docker Build] → [Push ECR] → [Deploy ECS] → [Verify] ✓
If you registered your domain externally (GoDaddy, Namecheap, etc.):
aws route53 create-hosted-zone \
--name yourdomain.com \
--caller-reference $(date +%s)# Get the 4 NS records AWS assigned
aws route53 list-hosted-zones-by-name --dns-name yourdomain.com \
--query 'HostedZones[0].Id' --output text | xargs -I{} \
aws route53 get-hosted-zone --id {} \
--query 'DelegationSet.NameServers'Go to your domain registrar and replace the nameservers with these 4 AWS nameservers. DNS propagation takes 10 minutes to 48 hours.
# After propagation:
nslookup tuner.yourdomain.com
# Should resolve to the ALB IP(s)
curl -I https://tuner.yourdomain.com/health
# HTTP/2 200# 1. Check ECS service
aws ecs describe-services \
--cluster can-tuner-prod \
--services can-tuner-prod-svc \
--region ap-south-1 \
--query 'services[0].{Status:status,Running:runningCount,Desired:desiredCount}'
# 2. Check health endpoint
curl https://tuner.yourdomain.com/health
# 3. Check CloudWatch logs
aws logs tail /ecs/can-tuner/prod --follow --region ap-south-1
# 4. Open the app
open https://tuner.yourdomain.comJust git push origin main — Jenkins picks it up automatically.
# List recent task definition revisions
aws ecs list-task-definitions \
--family-prefix can-tuner-prod \
--region ap-south-1 \
--sort DESC
# Roll back to a specific revision
aws ecs update-service \
--cluster can-tuner-prod \
--service can-tuner-prod-svc \
--task-definition can-tuner-prod:42 \ # ← the revision you want
--region ap-south-1aws ecs update-service \
--cluster can-tuner-prod \
--service can-tuner-prod-svc \
--desired-count 4 \
--region ap-south-1aws logs tail /ecs/can-tuner/prod --follow --region ap-south-1cd deployment/terraform
terraform destroy| Symptom | Likely Cause | Fix |
|---|---|---|
| ECS tasks keep restarting | Health check /health failing |
Add /health route to app.py (see below) |
ImagePullBackOff |
ECR auth expired or wrong image URI | Re-run Jenkins pipeline; check app_image in Terraform |
| 502 Bad Gateway from ALB | Flask not starting, wrong port | Check CloudWatch logs: aws logs tail /ecs/can-tuner/prod |
| Jenkins can't push to ECR | Missing IAM permissions | Verify aws-jenkins-deploy credentials and IAM policy |
| DNS not resolving | Nameservers not propagated | Wait 24–48 h; check with dig tuner.yourdomain.com NS |
| ACM certificate stuck "Pending" | DNS validation record not created | Go to ACM console → create validation record in Route 53 |
This is required for the ALB health check. Add this to app.py:
@app.get("/health")
def health():
return {"status": "ok"}, 200Monthly costs in ap-south-1 (Mumbai) with 2 tasks running 24/7:
| Service | Config | Est. $/month |
|---|---|---|
| ECS Fargate | 2 tasks × 0.5 vCPU × 1 GB | ~$18 |
| Application Load Balancer | 1 ALB + LCU | ~$20 |
| NAT Gateway | 2 AZs | ~$65 |
| ECR | 10 images × ~200 MB | ~$1 |
| Route 53 | 1 hosted zone + queries | ~$1 |
| CloudWatch Logs | 30-day retention | ~$2 |
| EC2 Jenkins | t3.medium, 24/7 | ~$28 |
| Total | ~$135/month |
To reduce cost:
- Use a single NAT Gateway (~$32 saved, minor HA tradeoff)
- Stop the Jenkins EC2 when not deploying
- Use FARGATE_SPOT for non-critical tasks (up to 70% cheaper)
- Reduce ECS desired count to 1 for dev/staging
Generated for CAN Tuner Dashboard · AWS ap-south-1 · Terraform 1.6+ · Jenkins LTS