flowardΒ·vibes
Home / 05 β€” GitHub Actions CI/CD Pipeline 4 min read

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:

  1. Builds your Docker image
  2. Pushes it to ECR
  3. 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 image and waits for kubectl rollout status before passing
  • Uses a concurrency group 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_ARN is locked to your repository and to main. The role's trust policy only accepts the OIDC identity of your repo on the main branch, 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 image only updates the image tag. If you change kubernetes/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 apply of 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.yml that: triggers on push to main; authenticates to AWS via OIDC using secrets.AWS_ROLE_ARN; builds the Docker image from dockerfiles/Dockerfile for linux/amd64; pushes it to ECR tagged with the git SHA and latest; then updates the Kubernetes deployment with kubectl set image and waits for kubectl rollout status. Add a concurrency group to serialise deploys. Leave region/repo/cluster/namespace as clearly marked placeholders."