https://ibb.co/8gs3bwCv https://ibb.co/ZpxBhfp6 https://ibb.co/B2ZXTsWT
- AmazonECS_FullAccess
- AmazonEC2ContainerRegistryFullAccess
- AmazonElasticFileSystemFullAccess
- AmazonS3FullAccess
- AmazonDynamoDBFullAccess
- ElasticLoadBalancingFullAccess
- AWSCertificateManagerFullAccess
- AmazonVPCFullAccess
- AmazonEC2FullAccess
Go to VPC → Create VPC → VPC and more. Set the name to abc-vpc, CIDR 10.0.0.0/16, 2 availability zones, 2 public subnets, 2 private subnets, and enable NAT Gateway (1 per AZ is fine for now). This gives your Fargate tasks internet access to pull images and call APIs without being publicly exposed.
Go to EFS → Create file system. Give it the name abc-chroma-efs. Select your abc-vpc. After creation, click into it and go to Network tab — AWS will auto-create mount targets in each subnet. You need to make sure the security group on those mount targets allows NFS traffic (port 2049) from your ECS tasks' security group. You'll come back to update this once you create the ECS security group.
Note the EFS File System ID (looks like fs-0abc1234) — you'll need it in the task definition.
Go to ECR → Create repository for each abc. Name them like abc, quiz-abc`, etc. After creation, click the repo and use the View push commands button — it gives you the exact Docker commands to build and push your FastAPI image. You'll need the AWS CLI configured locally for this.
-
ecsTaskExecutionRole
Note : selects "AWS Service" → "Elastic Container Service Task", this is set automatically.
-
AmazonECSTaskExecutionRolePolicy
-
CloudWatchLogsFullAccess
-
-
ecsTaskRole
-
AmazonS3FullAccess
-
AmazonDynamoDBFullAccesss
-
EFSAccessPolicy
## With wild card for efs { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "elasticfilesystem:ClientMount", "elasticfilesystem:ClientWrite", "elasticfilesystem:DescribeMountTargets" ], "Resource": "*" } ] } ## With proper efs id { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "elasticfilesystem:ClientMount", "elasticfilesystem:ClientWrite", "elasticfilesystem:DescribeMountTargets" ], "Resource": "arn:aws:elasticfilesystem:YOUR_REGION:YOUR_ACCOUNT_ID:file-system/YOUR_EFS_ID" } ] }
-
Go to EC2 → Security Groups → Create security group, VPC = abc-vpc.
ALB security group (sg-alb-abc): Inbound — HTTP 80 from 0.0.0.0/0, HTTPS 443 from 0.0.0.0/0. Outbound — all traffic.
ECS tasks security group (sg-ecs-abc): Inbound — port 8000 (your FastAPI port) from the ALB security group ID only. Outbound — all traffic (so the container can reach EFS, RDS, S3, etc.).
EFS security group (sg-efs-abc): Inbound — port 2049 (NFS) from the ECS tasks security group ID only. Outbound — none needed.
Now go back to your EFS mount targets and update their security group to sg-efs-abc.
Go to ECS → Clusters → Create cluster. Name it abc-cluster. Select AWS Fargate (serverless) as the infrastructure. CloudWatch Container Insights can be enabled here — recommended for production.
Go to ECS → Task definitions → Create new task definition.
- Family name:
abc-abc-task - Launch type: Fargate
- Task execution role:
ecsTaskExecutionRole - Task role:
ecsTaskRole - CPU:
1 vCPU, Memory:2 GB(start here, tune later)
Container definition:
- Name:
abc-abc - Image URI: your ECR image URI
- Port mappings:
8000 - Environment variables: add your DB connection strings, API keys, etc.
Add the EFS volume — this is the key part. Scroll to Storage section:
- Volume name:
chroma-data - Volume type: EFS
- File system ID: your EFS ID from step 2
- Root directory:
/(or a specific path like/chroma)
Then in your container definition, add a Mount point:
- Container path:
/app/chroma_data(whatever path your Python code uses for ChromaDB) - Source volume:
chroma-data
This means every Fargate task — no matter which host it runs on — will read and write ChromaDB from the same EFS path. Your AIML team just needs to point ChromaDB's persist_directory to /app/chroma_data in the FastAPI code.
Go to EC2 → Load Balancers → Create → Application Load Balancer.
- Name:
abc-alb - Scheme: Internet-facing
- VPC:
abc-vpc, select your public subnets - Security group:
sg-alb-abc
Create a Target Group during ALB setup:
- Target type: IP (required for Fargate — not instance type)
- Protocol: HTTP, Port: 8000
- Health check path:
/health(make sure your FastAPI app has this endpoint)
Go to ECS → your cluster → Services → Create.
- Launch type: Fargate
- Task definition:
quran-abc-task - Service name:
quran-abc-service - Desired tasks:
2(for high availability) - VPC:
abc-vpc, select your private subnets - Security group:
sg-ecs-abc - Load balancing: select your ALB and target group from step 8
Auto Scaling — enable it here:
- Minimum tasks:
2 - Maximum tasks:
10 - Scaling policy: Target tracking — CPU at 70% (when CPU goes above 70%, ECS adds tasks automatically)
Go to Route 53 (you already have this permission). Create an A record pointing your subdomain (e.g. abc.yourapp.com) to your ALB's DNS name using an Alias record.
========
========
Before writing the YAML, you need to set up OIDC authentication. This is the production-safe way — GitHub connects to AWS directly using a trusted identity, so you never store AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY in GitHub secrets. Tell your admin to do this:
Admin needs to create an OIDC Identity Provider in IAM:
- Provider URL:
https://token.actions.githubusercontent.com - Audience:
sts.amazonaws.com
Then create an IAM role named github-actions-ecs-deploy with this trust policy:
json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::YOUR_ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:YOUR_GITHUB_ORG/YOUR_REPO_NAME:*"
},
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
}
}
}
]
}Attach these permissions to that role:
AmazonEC2ContainerRegistryFullAccess— push images to ECRAmazonECS_FullAccess— update task definitions and servicesiam:PassRole— scoped only to yourecsTaskExecutionRoleandecsTaskRole
Go to your repo → Settings → Secrets and variables → Actions → New repository secret:
| Secret name | Value |
|---|---|
AWS_ACCOUNT_ID |
your 12-digit AWS account ID |
AWS_REGION |
e.g. ap-southeast-1 |
ECR_REPOSITORY |
e.g. abc-abc |
ECS_CLUSTER |
e.g. abc-cluster |
ECS_SERVICE |
e.g. abc-abc-service |
CONTAINER_NAME |
e.g. abc-abc (must match name in task definition) |
Create this file at .github/workflows/deploy.yml in your repo:
yaml
name: Build and Deploy to ECS
on:
push:
branches:
- main # triggers on every push to main
workflow_dispatch: # also allows manual trigger from GitHub UI
env:
AWS_REGION: ${{ secrets.AWS_REGION }}
ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }}
ECS_CLUSTER: ${{ secrets.ECS_CLUSTER }}
ECS_SERVICE: ${{ secrets.ECS_SERVICE }}
CONTAINER_NAME: ${{ secrets.CONTAINER_NAME }}
permissions:
id-token: write # required for OIDC authentication
contents: read
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
# Step 1: checkout the code
- name: Checkout code
uses: actions/checkout@v4
# Step 2: login to AWS using OIDC (no access keys needed)
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-ecs-deploy
aws-region: ${{ env.AWS_REGION }}
# Step 3: login to ECR
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
# Step 4: build docker image, tag with git commit SHA, push to ECR
# using commit SHA means every build has a unique tag — never overwrite
- name: Build, tag, and push image to ECR
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
# Step 5: download the current task definition from ECS
# this is important — we modify the existing one, not overwrite it
- name: Download current task definition
run: |
aws ecs describe-task-definition \
--task-definition ${{ env.ECS_SERVICE }} \
--query taskDefinition \
> task-definition.json
# Step 6: inject the new image URI into the task definition
- name: Update task definition with new image
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: ${{ env.CONTAINER_NAME }}
image: ${{ steps.build-image.outputs.image }}
# Step 7: register the new task definition and update the ECS service
# wait-for-service-stability = true means the job only passes
# when ECS confirms the new tasks are healthy — real production safety
- name: Deploy to ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true- GitHub Actions spins up a fresh Ubuntu runner
- It authenticates with AWS via OIDC — no passwords, no keys
- Builds your Docker image and tags it with the Git commit SHA (like
abc1234) — this means every build is traceable back to a specific commit - Pushes to ECR
- Downloads your current task definition from ECS (so all your existing settings — EFS mounts, env vars, CPU/memory — are preserved)
- Updates only the image URI in that task definition
- Registers it as a new revision (ECS keeps all revisions — you can rollback anytime)
- Updates the ECS service to use the new revision
- Waits until ECS confirms the new tasks are healthy — if health check fails, the job fails and the old tasks keep running