Automating Deployment of a Flask Application to AWS ECS with GitHub Actions
You no longer need to spend hours at your terminal deploying your app to AWS. Now, with a single git push
, you can convert your code into a Docker container and publish it on AWS. In this article, we’ll use GitHub Actions to automatically:
- Convert the Flask-based application into a Docker image
- Push it to ECR
- Launch it on ECS
We’ll build a serverless CI/CD pipeline, step by step!
Requirements
To successfully follow the steps in this article, make sure you have the following accounts, tools, and prerequisite knowledge:
Accounts & Cloud Environment
- GitHub account — for repository and GitHub Actions usage
- AWS account — with sufficient permissions to access ECR, ECS, and IAM
🛠️ Required Tools (must be installed and configured locally)
- Git — for version control
- Docker Engine — to build and test Docker images
- AWS CLI — to interact with AWS services via terminal (AWS credentials)
Technologies Used
- GitHub Actions → Automates CI/CD workflows
- Docker → Containerizes the Flask application
- Amazon ECR → Stores Docker images
- Amazon ECS (Fargate) → Runs containers without managing servers
- IAM → Manages secure access
Project Structure
flask-ecs-cicd/
├── app/
│ └── app.py
├── Dockerfile
├── requirements.txt
├── README.md
└── .github/
└── workflows/
└── deploy.yml
Creating the Main Folder and File Structure
mkdir flask-ecs-cicd
cd flask-ecs-cicd
mkdir app
mkdir -p .github/workflows
touch app/app.py Dockerfile requirements.txt README.md .github/workflows/deploy.yml
Flask Application
app/app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "Hello from Flask on ECS v1"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Dockerfile
FROM python:3.10-slim
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ .
CMD ["python", "app.py"]
requirements.txt
Flask==2.3.2
README.md
# Flask App for AWS ECS Deployment
This is a simple Flask application deployed using GitHub Actions to AWS ECS.
Testing the Application Locally
# Build the Docker image:
docker build -t flask-ecs-app .
# Run the container:
docker run -d -p 5000:5000 flask-ecs-app
Open your browser at http://localhost:5000
Preparing the GitHub Repository
- Create a new repository named
flask-ecs-cicd
on GitHub - Push your code:
cd flask-ecs-cicd
git init
git remote add origin https://github.com/username/flask-ecs-cicd.git
git add .
git commit -m "initial commit"
git push -u origin main
☁️ Setting Up AWS Infrastructure
💡 If you prefer to automate the entire deployment process, you can skip most of the manual steps by using the provided Bash scripts (`ecs-setup.sh` and `ecs-cleanup.sh`).
These scripts will handle ECR creation, ECS cluster and service setup, task definition registration, image deployment, and even cleanup.
🛠 OPTIONS 1- Create ECS cluster and ECR repository manually
Amazon ECR (Elastic Container Registry) Creation
AWS Console > ECR > Repositories > Create repository
Repository name: flask-ecs-cicd
Image tag mutability: Mutable
Encryption: AES-256 (default and appropriate)
Image scanning settings: (deprecated — we can skip this)
Click Create repository to create the new repository.
IAM Configuration
The repository is ready. We need two different IAM configurations: one for ECR access via GitHub Actions, and one for running containers on ECS.
Create an IAM User for GitHub
User name: github-actions-user
Access type: Programmatic access
Policy:
- AmazonEC2ContainerRegistryFullAccess
- AmazonECS_FullAccess
This user will:
- Build the Docker image via GitHub Actions
- Push it to the ECR repository
⚠️ Note: ECS tasks require an execution role with the
AmazonECSTaskExecutionRolePolicy
.
If you're using Terraform or AWS CLI, you must create this role manually.
It may be created automatically by the ECS console wizard, but that is not guaranteed in automation scenarios.
Creating the User
In the console, choose IAM > Users > Add user
User name: github-actions-user
Access type: Programmatic access (API only)
Permissions: Attach policies directly:
- AmazonEC2ContainerRegistryFullAccess
- AmazonECS_FullAccess
Create the user and note the Access Key ID and Secret Access Key.
Next, double-click the github-actions-user and select Security credentials.
Click Create access key to generate the CLI credentials.
Download the CSV and save it — these are your GitHub Actions secrets.
ECS Setup
1. Create a Cluster
AWS Console > ECS > Clusters > Create Cluster
Cluster configuration:
- Cluster name: flask-ecs-cluster
- Infrastructure: AWS Fargate (serverless)
Leave everything else as default and click Create Cluster.
Cluster created.
2. Define a Task (Container + ECR Image)
AWS Console > ECS > Task Definitions > Create new Task Definition
Task Definition Settings:
Task Definition Name: flask-ecs-task
Launch type: AWS Fargate
Task Size:
CPU: 0.5 vCPU
Memory: 2 GB
🔍 Note on Roles:
ecsTaskExecutionRole
must only be used in the Task Execution Role field.
Leave the Task Role field empty unless your container needs access to AWS services like S3, DynamoDB, etc.
Task Execution Role: ecsTaskExecutionRole
Choose ecsTaskExecutionRole if listed
Otherwise click Create new role to let AWS create it
Configure the container:
- Name: flask-container
- Image URI: 339712822099.dkr.ecr.us-east-1.amazonaws.com/flask-ecs-cicd:latest
- Port: 5000
- Protocol: TCP (HTTP)
Leave all other options with their default settings and create a Task Definition by clicking the create button.
3. Create a Service (Run the Container on Fargate)
AWS Console > ECS > Clusters > flask-ecs-cluster >Services > Create
Service Details
- Task definition family: flask-ecs-task
- Revision: 1 (or latest)
- Service name: flask-ecs-task-service-ibb
Environment
- Existing cluster: flask-ecs-cluster
- Compute configuration: Capacity Provider Strategy (FARGATE)
Deployment Configuration
- Service type: Replica
- Desired tasks: 1
- Availability Zone rebalancing: Enabled
Networking
- VPC: Default
- Subnets: Select at least 2 (e.g., us-east-1a, us-east-1b)
- Security Group: Create new
- Inbound Rule: TCP 5000 from 0.0.0.0/0
- Auto-assign public IP: Enabled
Click Create to launch the service.
The service may take a few minutes to stabilize. It will remain in Pending until an image is pushed to ECR.
After waiting a little while, you may get this error because the image cannot be found in the ECR.
To resolve this, push the image to ECR via GitHub Actions or manually with the AWS CLI.
ECR Image Push (Manual Test)
- Configure the AWS CLI credentials:
aws configure
2. Log in to ECR:
aws ecr get-login-password — region us-east-1 | \
docker login — username AWS — password-stdin <your-repo-url>
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin \
339712822099.dkr.ecr.us-east-1.amazonaws.com
3. Build the Docker image:
💡 Note: Make sure you build your Docker image for the correct platform.
AWS Fargate requireslinux/amd64
images. On Apple Silicon (M1/M2), use the following command:
docker buildx build --platform linux/amd64 -t flask-ecs-cicd .
4. Tag the image:
docker tag flask-ecs-cicd:latest <your-repo-url>/flask-ecs-cicd:latest
docker tag flask-ecs-cicd:latest \
339712822099.dkr.ecr.us-east-1.amazonaws.com/flask-ecs-cicd:latest
5. Push the image:
docker push <your-repo-url>/flask-ecs-cicd:latest
docker push 339712822099.dkr.ecr.us-east-1.amazonaws.com/flask-ecs-cicd:latest
Yes, the image was successfully pushed to the ECR repository.
Select a running task to find the ECS public IP and verify your app is working.
After deployment, select a running task in the ECS console to find the public IP address and verify your Flask application is working as expected.
⚠️ If your service does not start or remains in a stopped state, it might be because ECS could not pull the latest image from the ECR registry.
In such cases, you can force a new deployment with the following command:
aws ecs update-service \
--cluster flask-ecs-cluster \
--service flask-ecs-task-service-ibb \
--force-new-deployment
In our project, we tested http://<public-IP>:5000
in the browser (e.g., http://100.28.211.212:5000).
Automating Deploys with GitHub Actions
Now, to enable automatic deploys, define these secrets and variables in the GitHub repository’s Settings → Secrets and Variables:
Secrets
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
Variables
AWS_REGION
ECR_REPO
ECS_CLUSTER
ECS_SERVICE
.github/workflows/deploy.yml
:
name: Deploy to AWS ECS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Build and Push Docker image to ECR
run: |
docker buildx build \
--platform linux/amd64 \
-t ${{ vars.ECR_REPO }}:stable \
--push .
- name: Force ECS service to redeploy
run: |
aws ecs update-service \
--cluster ${{ vars.ECS_CLUSTER }} \
--service ${{ vars.ECS_SERVICE }} \
--force-new-deployment
✅ Result
Now, on every git push
:
- The Flask application is packaged into a Docker image
- It is pushed to ECR
- It is automatically deployed on ECS
🎉 You now have a fully automated, serverless, and secure CI/CD pipeline!
✅ Conclusion
In this article, we explored how to deploy a Flask application to AWS ECS Fargate using GitHub Actions.
We covered the infrastructure setup, Docker image handling, ECR push, task definition, and CI/CD integration.
By automating these steps with GitHub Actions and optional Bash scripts, you can ensure faster, error-free deployments in real-world scenarios.
⚙️ OPTIONS 2- Create ECS Cluster and ECR repository Full Automation with Bash Scripts
For users who prefer terminal-based workflows or need repeatable infrastructure automation, we provide two fully working Bash scripts:
Automated Deployment Script
📄 ecs-setup.sh
#!/bin/bash
# === Disable AWS CLI pager ===
export AWS_PAGER=""
# === Color definitions ===
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
# === Configurable Variables ===
AWS_REGION="us-east-1"
CLUSTER_NAME="flask-cluster"
SERVICE_NAME="flask-service"
TASK_NAME="flask-task"
REPO_NAME="flask-ecr-repo"
CONTAINER_NAME="flask-app"
CONTAINER_PORT=5000
SG_NAME="flask-sg"
# === Step 1: Get AWS Account Info ===
echo -e "${CYAN}${BOLD}Step 1: Getting AWS account info...${NC}"
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
if [ $? -ne 0 ]; then
echo -e "${RED}❌ AWS CLI not configured. Run 'aws configure' first.${NC}"
exit 1
fi
echo -e "${GREEN}✔ AWS Account ID: $ACCOUNT_ID${NC}"
# === Step 2: Get default VPC and Subnet ===
echo -e "\n${CYAN}${BOLD}Step 2: Fetching default VPC and subnet...${NC}"
VPC_ID=$(aws ec2 describe-vpcs --filters Name=isDefault,Values=true --query "Vpcs[0].VpcId" --output text)
SUBNET_ID=$(aws ec2 describe-subnets --filters Name=vpc-id,Values=$VPC_ID --query "Subnets[0].SubnetId" --output text)
echo -e "${GREEN}✔ VPC ID: $VPC_ID | Subnet ID: $SUBNET_ID${NC}"
# === Step 3: Check/Create Security Group ===
echo -e "\n${CYAN}${BOLD}Step 3: Creating or using security group '${SG_NAME}'...${NC}"
SG_ID=$(aws ec2 describe-security-groups \
--filters Name=group-name,Values=$SG_NAME Name=vpc-id,Values=$VPC_ID \
--query "SecurityGroups[0].GroupId" \
--output text 2>/dev/null)
if [[ "$SG_ID" == "None" || -z "$SG_ID" ]]; then
SG_ID=$(aws ec2 create-security-group \
--group-name $SG_NAME \
--description "Allow TCP $CONTAINER_PORT" \
--vpc-id $VPC_ID \
--query 'GroupId' --output text)
aws ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol tcp --port $CONTAINER_PORT --cidr 0.0.0.0/0
echo -e "${GREEN}✔ Security group created: $SG_ID${NC}"
else
echo -e "${GREEN}✔ Using existing security group: $SG_ID${NC}"
fi
# === Step 4: Create ECR Repository if needed ===
echo -e "\n${CYAN}${BOLD}Step 4: Creating or checking ECR repository '${REPO_NAME}'...${NC}"
aws ecr describe-repositories --repository-names $REPO_NAME >/dev/null 2>&1
if [ $? -ne 0 ]; then
aws ecr create-repository --repository-name $REPO_NAME
echo -e "${GREEN}✔ ECR repository created.${NC}"
else
echo -e "${GREEN}✔ ECR repository already exists.${NC}"
fi
IMAGE_URL="$ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$REPO_NAME:latest"
# === Step 5: Create IAM Role if needed ===
echo -e "\n${CYAN}${BOLD}Step 5: Creating or checking IAM Role 'ecsTaskExecutionRole'...${NC}"
aws iam get-role --role-name ecsTaskExecutionRole >/dev/null 2>&1
if [ $? -ne 0 ]; then
aws iam create-role --role-name ecsTaskExecutionRole \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "ecs-tasks.amazonaws.com" },
"Action": "sts:AssumeRole"
}]
}'
aws iam attach-role-policy \
--role-name ecsTaskExecutionRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
echo -e "${YELLOW}⏳ Waiting 10 seconds for IAM propagation...${NC}"
sleep 10
else
echo -e "${GREEN}✔ IAM Role already exists.${NC}"
fi
# === Step 6: Create ECS Cluster if needed ===
echo -e "\n${CYAN}${BOLD}Step 6: Creating or checking ECS Cluster '${CLUSTER_NAME}'...${NC}"
aws ecs describe-clusters --clusters $CLUSTER_NAME --query "clusters[0].status" --output text | grep -q ACTIVE
if [ $? -ne 0 ]; then
aws ecs create-cluster --cluster-name $CLUSTER_NAME
echo -e "${GREEN}✔ ECS Cluster created.${NC}"
else
echo -e "${GREEN}✔ ECS Cluster already exists.${NC}"
fi
# === Step 7: Register Task Definition ===
echo -e "\n${CYAN}${BOLD}Step 7: Registering ECS Task Definition '${TASK_NAME}'...${NC}"
aws ecs register-task-definition \
--family $TASK_NAME \
--requires-compatibilities "FARGATE" \
--network-mode "awsvpc" \
--cpu "256" \
--memory "512" \
--execution-role-arn arn:aws:iam::$ACCOUNT_ID:role/ecsTaskExecutionRole \
--container-definitions "[
{
\"name\": \"$CONTAINER_NAME\",
\"image\": \"$IMAGE_URL\",
\"portMappings\": [
{
\"containerPort\": $CONTAINER_PORT,
\"protocol\": \"tcp\"
}
],
\"essential\": true
}
]"
echo -e "${GREEN}✔ Task definition registered.${NC}"
# === Step 8: Create ECS Service ===
echo -e "\n${CYAN}${BOLD}Step 8: Creating ECS Service '${SERVICE_NAME}'...${NC}"
aws ecs describe-services --cluster $CLUSTER_NAME --services $SERVICE_NAME --query "services[0].status" --output text | grep -q ACTIVE
if [ $? -ne 0 ]; then
aws ecs create-service \
--cluster $CLUSTER_NAME \
--service-name $SERVICE_NAME \
--task-definition $TASK_NAME \
--launch-type FARGATE \
--desired-count 1 \
--network-configuration "awsvpcConfiguration={
subnets=[\"$SUBNET_ID\"],
securityGroups=[\"$SG_ID\"],
assignPublicIp=\"ENABLED\"
}"
echo -e "${GREEN}✔ ECS Service created.${NC}"
else
echo -e "${GREEN}✔ ECS Service already exists.${NC}"
fi
# === Step 9: Docker login to ECR ===
echo -e "\n${CYAN}${BOLD}Step 9: Logging in to Amazon ECR...${NC}"
aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin "$ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com"
if [ $? -ne 0 ]; then
echo -e "${RED}❌ Docker login failed. Check IAM ECR permissions.${NC}"
exit 1
fi
echo -e "${GREEN}✔ Docker login successful.${NC}"
# === Step 10: Build & Push Docker image with correct platform ===
echo -e "\n${CYAN}${BOLD}Step 10: Building Docker image for linux/amd64 (Fargate)...${NC}"
docker buildx build --platform linux/amd64 -t $REPO_NAME . --load
docker tag $REPO_NAME:latest $IMAGE_URL
docker push $IMAGE_URL
echo -e "${GREEN}✔ Docker image pushed to ECR.${NC}"
# === Step 11: Force ECS service to redeploy ===
echo -e "\n${CYAN}${BOLD}Step 11: Forcing ECS Service to redeploy new image...${NC}"
aws ecs update-service \
--cluster $CLUSTER_NAME \
--service $SERVICE_NAME \
--force-new-deployment >/dev/null 2>&1
if [ $? -eq 0 ]; then
echo -e "${GREEN}✔ ECS Service successfully redeployed with latest image.${NC}"
else
echo -e "${RED}❌ Failed to force ECS Service redeployment.${NC}"
fi
# === DONE ===
echo -e "\n${CYAN}${BOLD}🎉 Setup complete! Your Flask app is deployed on AWS ECS Fargate.${NC}\n"
This script will:
- Automatically detect default VPC and subnet
- Create or reuse a Security Group
- Create ECR repository (if missing)
- Create ECS Cluster and Task Definition
- Build Docker image for `linux/amd64` (Fargate compatible)
- Push the image to ECR
- Deploy and force update the ECS Service
Run it with:
bash ecs-setup.sh
💡 Useful for CI/CD pipelines or developers who want one-command deployments.
Automated Teardown Script
This script removes all created resources:
- Stops any running ECS tasks
- Deletes the ECS service and waits for it to shut down
- Deletes ECS Cluster
- Deregisters all task definitions in the family
- Deletes the ECR repository
- Removes the Security Group
🧹 ecs-cleanup.sh
:
#!/bin/bash
# === Disable AWS CLI pager ===
export AWS_PAGER=""
# === Color codes ===
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
# === Configuration ===
AWS_REGION="us-east-1"
CLUSTER_NAME="flask-cluster"
SERVICE_NAME="flask-service"
TASK_NAME="flask-task"
REPO_NAME="flask-ecr-repo"
SG_NAME="flask-sg"
# === Get AWS account ID ===
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text 2>/dev/null)
if [ $? -ne 0 ]; then
echo -e "${RED}❌ Failed to get AWS account ID. Make sure AWS CLI is configured.${NC}"
exit 1
fi
echo -e "${CYAN}${BOLD}🧹 Starting cleanup of AWS ECS and ECR resources...${NC}"
# === Step 1: Stop running tasks (if any) ===
echo -e "\n${CYAN}${BOLD}Step 1: Checking for running ECS tasks...${NC}"
TASKS=$(aws ecs list-tasks --cluster $CLUSTER_NAME --query "taskArns[]" --output text)
if [ -n "$TASKS" ]; then
echo -e "${YELLOW}Stopping running tasks...${NC}"
for task in $TASKS; do
aws ecs stop-task --cluster $CLUSTER_NAME --task $task >/dev/null 2>&1
echo -e "${GREEN}✔ Stopped task: $task${NC}"
done
else
echo -e "${GREEN}✔ No running tasks found.${NC}"
fi
# === Step 2: Delete ECS Service ===
echo -e "\n${CYAN}${BOLD}Step 2: Deleting ECS service '${SERVICE_NAME}'...${NC}"
aws ecs describe-services --cluster $CLUSTER_NAME --services $SERVICE_NAME --query "services[0].status" --output text | grep -q "ACTIVE"
if [ $? -eq 0 ]; then
aws ecs delete-service --cluster $CLUSTER_NAME --service $SERVICE_NAME --force >/dev/null 2>&1
echo -e "${GREEN}✔ Service deleted.${NC}"
echo -e "${YELLOW}⏳ Waiting for service to become inactive...${NC}"
aws ecs wait services-inactive --cluster $CLUSTER_NAME --services $SERVICE_NAME
else
echo -e "${GREEN}✔ ECS service already deleted or not found.${NC}"
fi
# === Step 3: Delete ECS Cluster ===
echo -e "\n${CYAN}${BOLD}Step 3: Deleting ECS cluster '${CLUSTER_NAME}'...${NC}"
aws ecs delete-cluster --cluster $CLUSTER_NAME >/dev/null 2>&1
if [ $? -eq 0 ]; then
echo -e "${GREEN}✔ Cluster deleted.${NC}"
else
echo -e "${YELLOW}⚠️ Cluster not found or already deleted.${NC}"
fi
# === Step 4: Deregister all Task Definitions ===
echo -e "\n${CYAN}${BOLD}Step 4: Deregistering ECS Task Definitions (family: $TASK_NAME)...${NC}"
TASK_DEFS=$(aws ecs list-task-definitions --family-prefix $TASK_NAME --query "taskDefinitionArns[]" --output text)
if [ -n "$TASK_DEFS" ]; then
for def in $TASK_DEFS; do
aws ecs deregister-task-definition --task-definition $def >/dev/null 2>&1
echo -e "${GREEN}✔ Deregistered: $def${NC}"
done
else
echo -e "${GREEN}✔ No task definitions to deregister.${NC}"
fi
# === Step 5: Delete ECR Repository ===
echo -e "\n${CYAN}${BOLD}Step 5: Deleting ECR repository '${REPO_NAME}'...${NC}"
aws ecr delete-repository --repository-name $REPO_NAME --force >/dev/null 2>&1
if [ $? -eq 0 ]; then
echo -e "${GREEN}✔ ECR repository deleted.${NC}"
else
echo -e "${YELLOW}⚠️ Repository not found or already deleted.${NC}"
fi
# === Step 6: Delete Security Group ===
echo -e "\n${CYAN}${BOLD}Step 6: Deleting Security Group '${SG_NAME}'...${NC}"
VPC_ID=$(aws ec2 describe-vpcs --filters "Name=isDefault,Values=true" --query "Vpcs[0].VpcId" --output text)
SG_ID=$(aws ec2 describe-security-groups --filters Name=group-name,Values=$SG_NAME Name=vpc-id,Values=$VPC_ID --query "SecurityGroups[0].GroupId" --output text 2>/dev/null)
if [ "$SG_ID" != "None" ] && [ -n "$SG_ID" ]; then
aws ec2 delete-security-group --group-id $SG_ID >/dev/null 2>&1
if [ $? -eq 0 ]; then
echo -e "${GREEN}✔ Security group deleted.${NC}"
else
echo -e "${YELLOW}⚠️ Could not delete security group. It might still be attached or in use.${NC}"
fi
else
echo -e "${GREEN}✔ Security group not found or already deleted.${NC}"
fi
# === Done ===
echo -e "\n${CYAN}${BOLD}✅ Cleanup completed successfully.${NC}\n"
Run it with:
bash ecs-cleanup.sh
⚠️ Make sure you don’t need any remaining resources before running the cleanup.
📎 GitHub Repository
You can find all code, scripts, and complete instructions in the following GitHub repository:
👉 https://github.com/hakanbayraktar/flask-ecs-cicd