Deep Dive & Step-By-Step: Deploying a Flask App to Amazon ECS with GitHub Actions & OpenID Connect

Cloud Engineer | AWS Community Builder
Search for a command to run...

Cloud Engineer | AWS Community Builder
No comments yet. Be the first to comment.
You have a working Amazon EKS cluster and deployed pods. Now, your pods need secure, fine-grained access to AWS services like S3, DynamoDB, or Secrets Manager, but: You don't want to hardcode AWS cre

As your team and projects grow, managing a single EKS cluster can get complicated. You'll likely have different teams like a "Backend Team" and a "Frontend Team" all needing to deploy their applicatio

Serverless computing with AWS Lambda is one of the most powerful ways to run code in the cloud without managing servers. But when a Lambda function is placed inside a VPC (Virtual Private Cloud) to co

Learn Bash Scripting: A Beginner's Guide Table of Contents Introduction Bash Scripting Basics Variables and User Input Conditional Statements Loops and Iterations Functions Working with Files Scripting Best Practices Conclusion 1. Introdu...

Modern developers want fast CI/CD but without storing long-lived AWS keys.By combining Amazon ECS (Fargate), Amazon ECR, GitHub Actions, and OpenID Connect (OIDC) you can deploy securely and automatically.
This blog explains how to deploy a containerized app to ECS with GitHub Actions CI/CD and why OIDC is the modern, secure way to let your pipeline assume AWS IAM roles.
Why: ECS runs containers; your app must be packaged into one. So, lets create minimal Flask app.
#app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "🚀 Hello from my-app on Amazon ECS using Github actions auto deployment!"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=80)
#requirements.txt
flask
#Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 80
CMD ["python", "app.py"]
We need a place to store images (ECR), a cluster to run them (ECS), and a network.
aws ecr create-repository --repository-name my-app
Gives a URI like:
XXXXXXXXXXXX.dkr.ecr.us-east-1.amazonaws.com/my-app
ECR is a private Docker registry. ECS will pull images from here.
aws ecs create-cluster --cluster-name my-app-cluster --region <aws-region>
A cluster is just a logical group of capacity (in Fargate you don’t manage servers).
Create ecs-task-def.json:
{
"family": "my-app-task",
"executionRoleArn": "arn:aws:iam::<ACCOUNT_ID>:role/ecsTaskExecutionRole",
"networkMode": "awsvpc",
"containerDefinitions": [
{
"name": "my-app",
"image": "<ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/my-app:latest",
"cpu": 256,
"memory": 512,
"essential": true,
"portMappings": [{ "containerPort": 80 }]
}
],
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512"
}
Register:
aws ecs register-task-definition --cli-input-json file://ecs-task-def.json
The execution role lets the task pull from ECR and send logs. The image is a placeholder; GitHub Actions will later push and update it.
# Get default VPC
aws ec2 describe-vpcs --filters "Name=isDefault,Values=true" --query "Vpcs[0].VpcId" --output text
# Get subnets in that VPC
aws ec2 describe-subnets --filters "Name=vpc-id,Values=<VPC_ID>" --query "Subnets[*].SubnetId" --output text
# Get default SG
aws ec2 describe-security-groups --filters "Name=vpc-id,Values=<VPC_ID>" --query "SecurityGroups[?GroupName=='default'].GroupId" --output text
# Open HTTP if needed
aws ec2 authorize-security-group-ingress --group-id <SG_ID> --protocol tcp --port 80 --cidr 0.0.0.0/0
aws ecs create-service \
--cluster my-app-cluster \
--service-name my-app-service \
--task-definition my-app-task \
--desired-count 1 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[subnet-aaa,subnet-bbb],securityGroups=[sg-xxx],assignPublicIp=ENABLED}"
Service keeps 1 running copy, updates when a new task definition revision arrives.
Why: avoid putting permanent AWS keys in GitHub.
In IAM → Identity providers → Add provider
Type: OpenID Connect
Audience: sts.amazonaws.com
Attach a trust policy like:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::<ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:your-username/ecsapp:ref:refs/heads/main"
},
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
}
}
}
]
}
Attach only what’s needed (I am giving full access but you can give only the required access):
GitHub runner asks token.actions.githubusercontent.com for a JWT.
JWT includes:
iss=https://token.actions...
sub=repo:you/ecsapp:ref:refs/heads/main
aud=sts.amazonaws.com
Action configure-aws-credentials calls AssumeRoleWithWebIdentity with JWT.
AWS STS validates signature + claims + trust policy.
STS returns temporary keys (valid ~1h).
These keys are used by the workflow for ECR/ECS.
Create .github/workflows/deployment.yaml:
name: Deploy my-app to Amazon ECS
on:
push:
branches: [ master ]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::943653636298:role/GitHub_Actions_Role
aws-region: us-east-1
- name: Login to Amazon ECR
id: ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push Docker image
run: |
IMAGE_URI=\({{ steps.ecr.outputs.registry }}/my-app:\){{ github.sha }}
docker build -t $IMAGE_URI .
docker push $IMAGE_URI
- name: Render Amazon ECS task definition
id: taskdef
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ecs-task-def.json
container-name: my-app
image: \({{ steps.ecr.outputs.registry }}/my-app:\){{ github.sha }}
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.taskdef.outputs.task-definition }}
service: my-app-service
cluster: my-app-cluster
wait-for-service-stability: true
Push to GitHub:
git add .
git commit -m "add CI/CD"
git push origin master
Push → triggers workflow.
GitHub gets OIDC token → AWS STS returns temporary creds.
Docker image builds and is pushed to ECR.
ECS task definition updated with new image tag.
ECS service does a rolling deployment → new container goes live.
Check ECS console → Cluster → Service → Task → copy Public IP → open in browser.

With this setup, every push to your GitHub repository triggers a secure, automated deployment to Amazon ECS. Using OpenID Connect, your workflow gets short-lived AWS credentials on demand, eliminating static keys and reducing risk. ECS Fargate runs your containers without managing servers, while ECR stores immutable images for each build.This approach is simple, scalable, and secure a modern DevOps pattern that combines fast CI/CD with least-privilege, keyless authentication.