Skip to main content

Command Palette

Search for a command to run...

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

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

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.

System Architecture

Step 1 : Build and Containerize the App

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"]

Step 2 : Set Up AWS Infrastructure

We need a place to store images (ECR), a cluster to run them (ECS), and a network.

2.1 Create ECR Repository

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.

2.2 Create ECS Cluster

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).

2.3 Define Task

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.

2.4 Networking (Default VPC)

# 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

2.5 Create Service

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.

Step 3 : Configure OpenID Connect (OIDC)

Why: avoid putting permanent AWS keys in GitHub.

3.1 Add GitHub as OIDC Identity Provider

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):

  • AmazonECS_FullAccess , AmazonEC2ContainerRegistryFullAccess

3.3 How OIDC Works at Runtime

  1. GitHub runner asks token.actions.githubusercontent.com for a JWT.

  2. JWT includes:
    iss=https://token.actions...
    sub=repo:you/ecsapp:ref:refs/heads/main
    aud=sts.amazonaws.com

  3. Action configure-aws-credentials calls AssumeRoleWithWebIdentity with JWT.

  4. AWS STS validates signature + claims + trust policy.

  5. STS returns temporary keys (valid ~1h).

  6. These keys are used by the workflow for ECR/ECS.

Step 4 : GitHub Actions Workflow

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

Step 5 : First Deployment Flow

  1. Push → triggers workflow.

  2. GitHub gets OIDC token → AWS STS returns temporary creds.

  3. Docker image builds and is pushed to ECR.

  4. ECS task definition updated with new image tag.

  5. ECS service does a rolling deployment → new container goes live.

  6. Check ECS console → Cluster → Service → Task → copy Public IP → open in browser.

Conclusion

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.