GitHub Actions for AWS: OIDC Setup, ECS Deployment, and CodePipeline Comparison
GitHub Actions can deploy to AWS without storing any AWS credentials in GitHub. Using OIDC, each workflow run requests a short-lived cryptographic token from GitHub, presents it to AWS, and receives temporary credentials that expire when the job ends. This page walks through the complete setup: creating the OIDC trust in AWS, writing a workflow that builds a Docker image and deploys to ECS, handling secrets correctly, and choosing between GitHub Actions and AWS CodePipeline.
The full worked example targets ECS, the most common pattern for containerized services. The OIDC authentication step is identical for Lambda, S3, and EKS. Where the deployment commands differ, this page notes how to adapt.
Simple explanation
GitHub Actions is a CI/CD platform built directly into GitHub. You write a workflow file (a YAML file committed to your repository) that describes what should happen when an event occurs: a push to main, a pull request opening, a release tag being created. The workflow runs on a hosted runner, executes your steps, and reports results back to GitHub.
The question is: how does that runner get permission to push images to ECR or update an ECS service? The old answer was to create an IAM user, generate an access key, and paste those credentials into GitHub Secrets as environment variables. Those keys never expire, can be extracted from careless log output, and must be manually rotated. Long-lived AWS access keys are a well-documented security risk.
The current answer is OIDC. Think of it like a visitor badge system at a corporate office. Instead of issuing every contractor a permanent keycard (one you would have to revoke if they left or lost it), the front desk issues a single-use badge for each visit: valid for that person, only today, only for the floors they need. OIDC works the same way. For each workflow run, GitHub’s OIDC provider issues a signed JWT token that says “this is a run from branch main of repo your-org/your-repo, triggered 30 seconds ago.” AWS verifies the signature, checks the claims against a trust policy you wrote, and issues short-lived STS temporary credentials that expire automatically when the job ends.
The result: no static secret to manage, rotate, or accidentally leak.
Why teams use GitHub Actions for AWS deployments
- Pipeline lives with the code. Workflow files are committed to the same repository as the application. They go through the same pull request review and the same Git history as everything else.
- No separate CI/CD interface to learn. Teams already using GitHub for code review and issue tracking do not need a separate console to write or debug pipelines.
- Large ecosystem of official actions. AWS maintains official actions for ECR login, ECS deployment, and OIDC credential configuration. Community actions exist for most other targets.
- OIDC removes credential management entirely. No IAM user, no rotation schedule, no risk of a leaked key in build logs.
- Flexible event triggers. Workflows can trigger on push, pull request, release, schedule, or manual dispatch. Any GitHub event your process already uses can start a deployment.
Before you start
- A GitHub repository containing the application you want to deploy.
- An AWS account with permissions to create IAM identity providers and IAM roles. If you are not yet familiar with how IAM roles and trust policies work, read that page first, since the OIDC setup depends on understanding trust policy conditions.
- An Amazon ECR repository to push your Docker image to (for the ECS example).
- A running ECS cluster and service configured to pull from that ECR repository. The CI/CD Pipelines for ECS guide covers the ECS infrastructure setup in detail.
- Basic familiarity with Docker: building an image locally and pushing it to a registry.
How it works
Every GitHub Actions deployment to AWS using OIDC follows the same six-step sequence:
- A GitHub event triggers the workflow: a push to main, a release tag, or a manual dispatch.
- The runner requests an OIDC token from GitHub. The token is a signed JWT containing claims about the run: repository, branch, actor, run ID. Your workflow needs
permissions: id-token: writeto request this token. Without that permission, the step fails silently. - The
aws-actions/configure-aws-credentialsaction sends the token to AWS STS. It callsAssumeRoleWithWebIdentity, presenting the JWT and the ARN of the IAM role you want to assume. - AWS IAM validates the token. It checks the signature against GitHub’s published OIDC public keys, then checks that the
subclaim in the token satisfies the conditions in your role’s trust policy. - AWS STS returns short-lived credentials. An access key ID, secret access key, and session token are injected as environment variables in the runner. They are valid for the duration of the job (up to 1 hour by default).
- The workflow builds, pushes, and deploys using those credentials. When the job ends, the credentials expire. Nothing is left to clean up.
Easy permission to miss
The permissions: id-token: write block at the top of your workflow is what lets the runner request an OIDC token from GitHub. If you omit it or set it to none, the configure-aws-credentials step will fail with a token request error that is not always obvious about the cause. Add this block to every workflow that uses OIDC.
One-time OIDC setup in AWS
This is done once per AWS account. It creates the trust relationship between GitHub’s OIDC provider and your AWS account.
1. Create the IAM Identity Provider
This registers GitHub’s OIDC endpoint with AWS so that tokens signed by GitHub can be validated:
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1The thumbprint is the SHA-1 fingerprint of GitHub’s OIDC TLS certificate. GitHub publishes the current value in their documentation. Verify it before applying to production accounts. It occasionally changes when GitHub rotates their certificate.
2. Create the IAM role with a scoped trust policy
The trust policy is the security boundary. It defines exactly which repository and which branch can assume this role. For a detailed walkthrough of how trust policy conditions are structured, see AWS IAM Roles Explained: Trust Policy, Permissions & Examples.
Be specific. A condition like repo:your-org/: allows every repository in your entire organization to assume the production role. Use StringLike to scope to the exact repository and branch:
{
"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"
}
}
}
]
}A condition of repo:your-org/: grants every repository in your organization the ability to assume this role. That includes repositories you do not own, personal forks, and any future repository someone creates. Always scope the sub condition to the exact repository name and branch. The extra specificity takes thirty seconds and prevents a very wide blast radius if a repo in your org is compromised.
# Create the role
aws iam create-role \
--role-name GitHubActionsECSDeployRole \
--assume-role-policy-document file://trust-policy.json
# Attach a least-privilege permissions policy scoped to ECR and ECS
aws iam attach-role-policy \
--role-name GitHubActionsECSDeployRole \
--policy-arn arn:aws:iam::123456789012:policy/ECSDeployPolicyThe permissions policy attached to this role should only include the actions the pipeline actually needs: specific ECR actions for pushing images, and specific ECS actions for updating the service. For worked examples of minimal deployment policies, see Secure CI/CD Pipelines in AWS.
Complete workflow: build, push to ECR, and deploy to ECS
This workflow triggers on pushes to main. It authenticates via OIDC, builds and pushes a Docker image to ECR, then updates the ECS service to run the new image. For ECR image lifecycle policies and repository configuration, see Amazon ECR Best Practices for Production.
# .github/workflows/deploy.yml
name: Build and Deploy to ECS
on:
push:
branches:
- main
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: my-app
ECS_CLUSTER: my-cluster
ECS_SERVICE: my-app-service
CONTAINER_NAME: my-app
permissions:
id-token: write # Required: lets the runner request an OIDC token from GitHub
contents: read # Required: lets the runner check out the repository
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Exchange the GitHub OIDC token for short-lived AWS credentials
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsECSDeployRole
aws-region: ${{ env.AWS_REGION }}
# Authenticate Docker to ECR using the short-lived credentials above
- name: Log in to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1
# Build the image, tag it with the commit SHA, and push to ECR
- 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
# Fetch the current task definition JSON and update the image reference
- name: Download current ECS task definition
run: |
aws ecs describe-task-definition \
--task-definition my-app \
--query taskDefinition > task-definition.json
- name: Update ECS task definition with new image
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@4225e0b507142a2e432b018bc3ccb728559b437b # v1.5.0
with:
task-definition: task-definition.json
container-name: ${{ env.CONTAINER_NAME }}
image: ${{ steps.build-image.outputs.image }}
# Register the new task definition revision and update the service
- name: Deploy to ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@3e7310352de91b73b38eda53f0ec96b88f3fdc70 # v2.3.0
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: trueEvery action is pinned to a commit SHA rather than a version tag. Tags are mutable. Anyone with push access to the action’s repository can update a tag to point at new code. Pinning to a commit SHA is immutable and is a standard supply chain security practice.
Deploying to a different target? The OIDC authentication step is identical. After credentials are configured, replace the ECR/ECS steps:
- Lambda:
aws lambda update-function-code —function-name my-fn —image-uri … - S3 static site:
aws s3 sync ./dist s3://my-bucket —delete - EKS:
aws eks update-kubeconfig —name my-cluster, thenkubectl apply -f k8s/
When to use GitHub Actions for AWS deployments
- Your team works primarily in GitHub and wants deployments triggered and reviewed in the same place as application code.
- You are deploying a single service to a straightforward target: ECS, Lambda, or S3.
- You want pipelines that are readable, diff-able, and reviewable by engineers who already know Git.
- You are using multiple environments that map cleanly to GitHub Environments with manual approval gates.
- You want the simplest path from committed code to running container, without introducing separate AWS-native tooling.
When not to use GitHub Actions for AWS deployments
- Complex multi-account deployments. Multiple OIDC roles can be chained, but CodePipeline has native cross-account role assumption built specifically for this pattern and is easier to audit.
- AWS-native event triggers. If a deployment should start when an ECR image is pushed or an S3 file is uploaded (not when a commit is made), CodePipeline handles these natively through EventBridge.
- Compliance requires all execution inside AWS. Some regulated environments require that CI/CD pipelines run entirely within the organization’s AWS accounts for audit trail and data residency reasons.
- Pipelines spanning many repositories or services. Multi-repo orchestration with manual approval gates between stages is a native CodePipeline capability; modeling it in GitHub Actions requires significant custom work.
GitHub Actions vs AWS CodePipeline
| Factor | GitHub Actions | AWS CodePipeline |
|---|---|---|
| Pipeline definition | Workflow YAML committed to the repository | CloudFormation / Terraform / console |
| Source of truth | GitHub | GitHub, CodeCommit, S3, ECR |
| Event triggers | GitHub events (push, PR, tag, schedule, manual) | ECR push, S3 upload, EventBridge, GitHub |
| Multi-account deployments | Possible with multiple OIDC roles | Native cross-account role support |
| Manual approvals | GitHub Environments with required reviewers | Native approval action in pipeline stage |
| Build environment | GitHub-hosted or self-hosted runners | AWS CodeBuild |
| Audit trail | GitHub audit log | CloudTrail + EventBridge |
| AWS-native integrations | Requires explicit action steps | Tighter native integration |
The practical decision: choose GitHub Actions when your team lives in GitHub and you want pipelines that non-ops engineers can read and change. Choose CodePipeline when your pipeline must start from an AWS event, when you need native multi-account orchestration, or when compliance demands AWS-hosted execution. If you go with CodePipeline, AWS CodeBuild handles the build stage, the equivalent of the Docker build and push steps shown in the workflow above.
Secrets and security best practices
Build-time secrets: GitHub Secrets
GitHub Secrets stores encrypted values at the repository or organization level. They are injected as environment variables during workflow runs and masked in log output. Use GitHub Secrets for values the build process needs: a third-party API key for integration tests, a signing certificate, a token for a private package registry.
Do not use GitHub Secrets for AWS credentials. That is exactly what OIDC replaces. For a full treatment of which secret type belongs where in a pipeline, see Managing Secrets in CI/CD Pipelines on AWS.
Runtime secrets: AWS Secrets Manager
For secrets your application reads when it starts, such as database passwords or third-party API keys consumed at runtime, use AWS Secrets Manager. Reference the secret directly in the ECS task definition. ECS injects it into the container at startup; the value never passes through the pipeline.
{
"containerDefinitions": [
{
"name": "my-app",
"secrets": [
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/db-password:password::"
}
]
}
]
}A common mistake is storing a database password in GitHub Secrets and passing it as an environment variable through the workflow to the container. Every step in that chain is a place the value can be logged, printed by a debug command, or leaked in a failed build artifact. Put runtime secrets in Secrets Manager and let ECS inject them directly into the container at startup. The pipeline never sees the value.
Additional security practices
- Pin actions to commit SHAs.
uses: actions/checkout@v4runs whatever code the tag currently points to. Tags are mutable. Pinning to a full SHA means the code cannot change under you. - Scope the trust policy tightly. Use a specific
subcondition for the repository and branch. Never userepo:your-org/:for a role that has production permissions. - Give the deployment role only the permissions it needs. An ECS deploy role does not need S3 or EC2 access. Scope it to the specific ECR repository, the specific ECS cluster and service, and nothing else.
- Use GitHub Environments for production gates. Environments support required reviewers so the deploy job cannot start until an approved team member unblocks it. You can also scope the OIDC trust policy to a named GitHub Environment:
repo:your-org/your-repo:environment:production. See Managing Environments in CI/CD Pipelines for the full pattern.
Protecting the main branch
The workflow above deploys every push to main. Without branch protection, a direct push to main (no pull request, no review) triggers a production deployment. Enable branch protection to require that all changes go through a pull request with passing CI:
- In GitHub: Settings → Branches → Add branch protection rule.
- Set the pattern to
main. - Enable “Require a pull request before merging” with a minimum reviewer count.
- Enable “Require status checks to pass before merging” and add your CI workflow as a required check.
- Enable “Require branches to be up to date before merging” to prevent race conditions between concurrent merges.
With these rules active, the path to production is: passing tests, code review, merge to main, deployment. Direct pushes are blocked.
Common beginner mistakes
- Using static access keys instead of OIDC. Storing
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYin GitHub Secrets was the standard approach before OIDC support existed. Those keys do not expire, can appear in logs, and require manual rotation. Set up OIDC once and you never manage a static credential again. - Setting the trust policy condition too broadly. A condition of
repo:your-org/:allows every repository in your organization to assume the production deployment role. Always scope to the specific repository and branch. - Pinning actions to version tags.
uses: actions/checkout@v4runs whatever the tag currently points to. Tags can be moved. Pin to the full commit SHA for supply chain safety. - Not filtering the deploy job to the main branch. Without a branch filter, a push to any feature branch runs the full deploy job against production. Add
branches: [main]to the trigger, or add a job-levelifcondition. - Passing runtime secrets through the pipeline. If a container needs a database password at startup, that password should live in Secrets Manager and be injected by ECS, not stored in GitHub Secrets and piped through the workflow as an environment variable. Secrets that pass through pipelines get duplicated, logged, and exposed.
Summary
- GitHub Actions deploys to AWS securely using OIDC. GitHub issues a signed JWT token per workflow run. AWS validates it against a trust policy and returns short-lived STS credentials that expire when the job ends.
- OIDC setup is a one-time task per AWS account: create an IAM Identity Provider for
token.actions.githubusercontent.com, then create an IAM role with a trust policy condition scoped to your specific repository and branch. - The
aws-actions/configure-aws-credentialsaction handles the token exchange. Your workflow needspermissions: id-token: writeto request the OIDC token from GitHub. - Pin all GitHub Actions to commit SHAs. Version tags are mutable and can be updated or compromised.
- Build-time secrets belong in GitHub Secrets. Runtime secrets belong in AWS Secrets Manager, injected directly into the ECS task definition rather than passed through the workflow.
- The OIDC pattern works identically for ECS, Lambda, S3, and EKS. Only the deployment steps after credential configuration differ.
- Use GitHub Actions for GitHub-native teams deploying straightforward services. Use CodePipeline for complex multi-account orchestration, AWS-native event triggers, or compliance requirements that mandate AWS-hosted execution.
Frequently asked questions
Do I need AWS access keys in GitHub Actions?
No. Use OIDC instead. GitHub requests a short-lived JWT token from its built-in OIDC provider for each workflow run. AWS validates that token against a trust policy you control and returns temporary STS credentials that expire when the job ends. There is no static access key to store, rotate, or accidentally commit.
How does OIDC authentication actually work?
GitHub's OIDC provider issues a signed JWT token for each workflow run. The token contains claims including the repository name and branch. AWS IAM validates the token signature against GitHub's published OIDC public keys, then checks that the claims match the conditions in your role's trust policy. If they match, AWS STS calls AssumeRoleWithWebIdentity and returns short-lived credentials scoped to that role.
When should I use GitHub Actions instead of CodePipeline?
Use GitHub Actions when your team works in GitHub and wants pipelines defined as code alongside the application. Use CodePipeline when you need AWS-native event triggers (ECR image push, S3 upload), native multi-account deployment orchestration, or when compliance requires all CI/CD tooling to run inside AWS.
Can I deploy to AWS services other than ECS?
Yes. The OIDC authentication setup is identical for any AWS deployment target. After the credentials are configured, replace the ECR/ECS steps with the commands for your target: aws lambda update-function-code for Lambda, aws s3 sync for S3 static sites, or kubectl apply after aws eks update-kubeconfig for EKS.
How do I restrict which branches or environments can deploy to production?
Use two layers. First, scope the IAM trust policy sub condition to a specific branch: repo:your-org/your-repo:ref:refs/heads/main. This prevents any other branch or repository from assuming the role. Second, use GitHub Environments with required reviewers so the deploy job must be approved by a team member before it runs. You can also scope the trust condition to a named GitHub Environment instead of a branch.