Deploying to AWS from GitHub Actions: The Production Setup
The most common GitHub Actions deploy pattern in production: build a container, push to ECR, update an ECS service. This is the working config — copy-pasteable, with the IAM nuances baked in. No mystery; every line earns its keep.
The Working Workflow
name: deploy-prod
on:
push:
branches: [main]
permissions:
id-token: write # required for OIDC — no AWS access keys
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
aws-region: us-east-1
- name: Login to ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, push
env:
REGISTRY: 123456789012.dkr.ecr.us-east-1.amazonaws.com
REPO: ninja-app
run: |
docker build -t $REGISTRY/$REPO:${{ github.sha }} -t $REGISTRY/$REPO:latest .
docker push $REGISTRY/$REPO:${{ github.sha }}
docker push $REGISTRY/$REPO:latest
- name: Update ECS service
run: |
aws ecs update-service \
--cluster ninja-prod \
--service ninja-app \
--force-new-deployment
- name: Wait for stable
run: |
aws ecs wait services-stable \
--cluster ninja-prod \
--services ninja-app
The IAM Trust Policy (the part most tutorials skip)
OIDC removes the need for long-lived AWS access keys in GitHub. The trust policy on the IAM role looks like:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
}
}
}]
}
Why This Works
- OIDC, not access keys. No secrets to rotate, no keys to leak.
- Sub-claim scoped to
refs/heads/main. A PR branch can't assume this role. - Force new deployment instead of registering a new task definition. Faster, simpler, fewer moving parts.
ecs wait services-stableat the end. Failure here means the new task didn't come up; the workflow fails loudly.
Common Pitfalls
- Forgetting
id-token: write. Without it, OIDC silently fails. - Loose
StringLikesub claim. Allowingrepo:org/*:*means any branch in any repo in your org can assume the role. Tight scoping matters. - Not pinning action versions. Pin to
@v4at minimum; better, pin to a SHA. - Building Docker images on every commit, not caching. Use Docker buildx with a cache backend; cuts build time 5-10x.
FAQ
How do I do this for blue/green deploys?
Swap aws ecs update-service for AWS CodeDeploy with aws deploy create-deployment. The CodeDeploy hooks let you run smoke tests before flipping traffic.
What about secrets at runtime?
Use AWS Secrets Manager or SSM Parameter Store and reference them in the ECS task definition's secrets block. Never put secrets in environment variables in the workflow.
Have a correction or a different field experience? We update these pieces. Honest critique welcome.