I Stopped Using AWS Access Keys in GitHub Actions — Here’s the Secure Way Instead.
Deploying Docker to AWS ECS with GitHub OIDC
One of the most common ways developers deploy applications to AWS using GitHub Actions looks like this:
Create an IAM User
Generate an Access Key and Secret Key
Store them inside GitHub Secrets
Hope they never leak
This approach works, but it has a major problem.
Long-lived credentials are one of the biggest causes of cloud security incidents.
Access keys can be leaked, forgotten, or exposed in logs, and rotating them manually is often neglected.
Fortunately, AWS and GitHub provide a much better solution: OpenID Connect (OIDC).
In this article, I’ll walk through how I built a zero-secret CI/CD pipeline that securely deploys a Dockerized Node.js application to AWS ECS Fargate using GitHub Actions and OIDC authentication.
What We’re Building
The pipeline automatically deploys our application whenever code is pushed to GitHub.
Workflow Overview
Git Push
↓
GitHub Actions
↓
OIDC Authentication
↓
Docker Build
↓
Amazon ECR
↓
ECS Fargate Deployment
↓
CloudWatch Logs
Pipeline Features
Secure OIDC authentication (no stored secrets)
Docker image builds on every push
Images pushed to Amazon ECR
Automated deployment to AWS ECS Fargate
CloudWatch logging
Self-healing containers
Deployment time from push to production is about 3 minutes.
Understanding OIDC in 60 Seconds
Traditional Approach
GitHub Secrets → AWS Access Keys → AWS API
(long-lived credentials stored in GitHub)
Problems:
Keys can leak
Must rotate manually
Security audits become painful
OIDC Approach
GitHub Workflow → OIDC Token → AWS STS → Temporary Credentials
AWS verifies that the request came from a trusted GitHub repository and issues temporary credentials valid for about 1 hour.
No credentials are stored anywhere.
This is why OIDC is now considered a best practice for CI/CD pipelines.
Phase 1 — The Application
The application is a simple Node.js HTTP server returning a JSON response.
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
message: 'Hello from ECS!',
version: process.env.APP_VERSION || '1.0.0'
}));
});
server.listen(3000, () => console.log('Server running on port 3000'));
Containerizing the Application
We package the application using Docker.
Using Alpine Linux keeps the image lightweight.
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]
Build and test locally:
docker build -t my-app .
docker run -p 3000:3000 my-app
Test the API:
curl http://localhost:3000
If everything works locally, we are ready to deploy.
Phase 2 — Amazon ECR
Amazon Elastic Container Registry stores our Docker images.
Create the repository:
aws ecr create-repository \
--repository-name my-app \
--region us-east-1 \
--image-scanning-configuration scanOnPush=true \
--image-tag-mutability IMMUTABLE
Why these settings matter
scanOnPush
Automatically scans images for security vulnerabilities.
IMMUTABLE tags
Prevents overwriting image tags, making deployments reproducible.
Push the image:
docker tag my-app:latest ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/my-app:v1.0.0
docker push ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/my-app:v1.0.0
We also add a lifecycle policy to keep the registry clean.
Phase 3 — ECS Cluster and Service
Amazon ECS runs our containerized application.
Four key ECS concepts:
| Concept | Meaning |
|---|---|
| Cluster | Pool of compute resources |
| Task Definition | Container blueprint |
| Task | Running container |
| Service | Maintains desired number of tasks |
Create the cluster:
aws ecs create-cluster --cluster-name my-app-cluster --capacity-providers FARGATE
Create CloudWatch logs:
aws logs create-log-group --log-group-name /ecs/my-app
Deploy the ECS service:
aws ecs create-service \
--cluster my-app-cluster \
--service-name my-app-service \
--task-definition my-app \
--desired-count 1 \
--launch-type FARGATE
Once deployed, ECS runs the container automatically.
Phase 4 — GitHub Actions CI/CD Pipeline
Now we automate everything.
The workflow runs on every push to the main branch.
name: Build & Deploy to ECS
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-deploy
aws-region: us-east-1
- name: Login to ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Build and Push Image
run: |
docker build -t my-app .
docker push IMAGE_URI
Every deployment uses the Git commit SHA as the image tag, making releases fully traceable.
Monitoring with CloudWatch
Application logs are sent to Amazon CloudWatch.
You can stream logs in real time:
aws logs tail /ecs/my-app --follow
This makes debugging production issues much easier.
Self-Healing Containers
ECS services automatically maintain the desired number of tasks.
To demonstrate this, you can manually stop a running task.
aws ecs stop-task --cluster my-app-cluster --task TASK_ID
Within seconds, ECS launches a new container automatically.
No human intervention required.
Cost Breakdown
| Resource | Monthly Cost |
|---|---|
| Fargate (0.25 vCPU / 0.5GB RAM) | ~$9.00 |
| ECR Storage | ~$0.05 |
| CloudWatch Logs | ~$0.50 |
Total cost is about $9.55 per month.
Scaling the service to 0 tasks when idle reduces costs even further.
Key Takeaways
OIDC eliminates the need for long-lived AWS credentials
Git commit SHA tags make deployments traceable
ECS Fargate removes server management
CloudWatch provides centralized logging
ECS services provide built-in self-healing
This architecture provides a secure, scalable, and automated deployment pipeline suitable for modern cloud applications.
Final Thoughts
Moving away from IAM access keys toward OIDC-based authentication is one of the simplest ways to improve the security of your CI/CD pipelines.
By combining:
GitHub Actions
OIDC authentication
Docker containers
AWS ECS Fargate
you can build a fully automated, zero-secret deployment pipeline that is both secure and production-ready.