05 β GitHub Actions CI/CD Pipeline
What it is: an automated workflow that runs on every push to main and does three things in sequence:
- Builds your Docker image
- Pushes it to ECR
- Deploys it to the K8s cluster β your changes go live with no manual steps
Location: .github/workflows/ci.yml
Rules
- Triggers on push to
main - Authenticates to AWS via OIDC (short-lived token; no AWS keys stored in GitHub)
- Tags images with the git commit SHA (traceability) and
latest - Deploys with
kubectl set imageand waits forkubectl rollout statusbefore passing - Uses a
concurrencygroup so two merges in quick succession don't race each other's deploys - (Recommended) a second workflow that builds β but does not push/deploy β on pull requests, so broken Dockerfiles are caught before merge
Values the DevOps team provides β do not invent them
| Value | Where it goes |
|---|---|
AWS_REGION (e.g. eu-west-1) |
env: block in ci.yml |
ECR_REPOSITORY |
env: block |
K8S_CLUSTER_NAME |
env: block |
K8S_NAMESPACE |
env: block |
AWS_ROLE_ARN |
GitHub repository secret |
RUNNER_LABELS |
runs-on: β our pipelines run on the DevOps team's self-hosted runners, not GitHub-hosted ones |
Example workflow
name: Build, Push to ECR, and Deploy to Kubernetes
on:
push:
branches: [main]
env:
AWS_REGION: <REGION> # β provided by DevOps team
ECR_REPOSITORY: <ECR_REPO_NAME> # β provided by DevOps team
K8S_CLUSTER_NAME: <CLUSTER_NAME> # β provided by DevOps team
K8S_NAMESPACE: <NAMESPACE> # β provided by DevOps team
DEPLOYMENT_NAME: my-app # must match metadata.name in deployment.yaml
CONTAINER_NAME: my-app # must match containers[].name in deployment.yaml
permissions:
id-token: write # required for OIDC authentication to AWS
contents: read
concurrency:
group: deploy-main
cancel-in-progress: false # let an in-flight deploy finish; queue the next
jobs:
build-push-deploy:
runs-on: <RUNNER_LABELS> # β provided by DevOps team (self-hosted runners)
steps:
- name: Checkout code
uses: actions/checkout@v4
# Step 1 β Authenticate to AWS via OIDC (no stored AWS keys)
- name: Authenticate to AWS
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
# Step 2 β Log in to ECR
- name: Log in to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
# Step 3 β Build and push the Docker image
- name: Build and push Docker image
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -f dockerfiles/Dockerfile \
--platform linux/amd64 \
-t $REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG \
-t $REGISTRY/$ECR_REPOSITORY:latest .
docker push $REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $REGISTRY/$ECR_REPOSITORY:latest
echo "IMAGE=$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_ENV
# Step 4 β Install kubectl (the runners don't ship it) and configure it
- name: Install kubectl
uses: azure/setup-kubectl@v4
with:
version: 'v1.31.0' # keep within one minor version of the cluster
- name: Configure kubectl
run: |
aws eks update-kubeconfig \
--region $AWS_REGION \
--name $K8S_CLUSTER_NAME
# Step 5 β Roll out the new image (zero-downtime rolling update)
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/$DEPLOYMENT_NAME \
$CONTAINER_NAME=$IMAGE \
--namespace $K8S_NAMESPACE
kubectl rollout status deployment/$DEPLOYMENT_NAME \
--namespace $K8S_NAMESPACE \
--timeout=180s
What happens during deployment
Kubernetes performs a rolling update: it starts a new pod with the new image, waits for it to pass the readiness probe, then shuts down an old pod. Your app is never completely offline.
Important operational notes
Your
AWS_ROLE_ARNis locked to your repository and tomain. The role's trust policy only accepts the OIDC identity of your repo on themainbranch, and it can only push to your ECR repo and roll your deployment in your namespace. If the "Authenticate to AWS" step fails with Not authorized to perform sts:AssumeRoleWithWebIdentity, you're running from a fork/branch or a different repo β that's by design.Manifest changes don't deploy themselves.
kubectl set imageonly updates the image tag. If you changekubernetes/deployment.yaml(resources, env vars, probes), those changes reach the cluster only when the DevOps team applies them (or via the DevOps team's GitOps process). Flag manifest changes to DevOps when you submit them.Rollback: if a deploy goes bad, DevOps can run
kubectl rollout undo deployment/<name>. Because every image is tagged with a commit SHA, any previous version can be redeployed precisely.First deploy: the initial
kubectl applyof your manifests (with the real ECR image URI, Secrets, and ConfigMap in place) is done by the DevOps team; the pipeline handles every deploy after that.
Prompt tip
"Write a GitHub Actions workflow at
.github/workflows/ci.ymlthat: triggers on push tomain; authenticates to AWS via OIDC usingsecrets.AWS_ROLE_ARN; builds the Docker image fromdockerfiles/Dockerfileforlinux/amd64; pushes it to ECR tagged with the git SHA andlatest; then updates the Kubernetes deployment withkubectl set imageand waits forkubectl rollout status. Add a concurrency group to serialise deploys. Leave region/repo/cluster/namespace as clearly marked placeholders."