1. All Blogs 2. About Me 3. Contact Me 4. Visit My Website 5. Visit My Venture

Part 7: GitHub Actions II — Advanced Automation & Custom Runners

font_size 100%
font
contrast
listen

Part 7: GitHub Actions II — Advanced Automation & Custom Runners

In Part 6, we built a fast CI workflow for a single repository. Once your team grows past a handful of services, three new problems show up:

1. You end up copy-pasting the same 150-line deployment YAML across twenty repositories.

2. Someone stores long-lived AWS_SECRET_ACCESS_KEY or GCP_SA_KEY credentials in GitHub Secrets and never rotates them.

3. You need runners that can reach a private VPC database or use a GPU without leaving leftover build state on disk between jobs.

Let's solve all three using Reusable Workflows, OpenID Connect (OIDC), GitHub Environments, and Actions Runner Controller (ARC) on Kubernetes.

Reusable Workflows, OIDC Cloud Federation and K8s ARC Runners

1. Reusable Workflows (workflow_call) vs. Composite Actions

GitHub gives you two ways to share workflow logic without copy-pasting YAML:

  • Composite Actions (action.yml with runs: using: "composite"): Packs a few common steps (like setting up Node, pnpm, and custom cache paths) into a single reusable step that runs inside an existing job.
  • Reusable Workflows (on: workflow_call): Lets you put an entire multi-job pipeline (build → scan → push image → deploy) inside one central repo (say Ace-Vector-Labs/platform-ci/.github/workflows/deploy.yml@v2.4.0). Every microservice repo can then call that whole pipeline in 10 lines of YAML.

2. Ditching Static Cloud Keys with OIDC (id-token: write)

Pasting static AWS or GCP credentials into GitHub Secrets is a bad habit. Static keys don't expire on their own, they are annoying to rotate across dozens of repos, and if one leaks, an attacker can use it from anywhere.

With OpenID Connect (OIDC), you don't store cloud keys in GitHub at all. When a job runs with permissions: id-token: write, GitHub's OIDC provider (https://token.actions.githubusercontent.com) signs a short-lived JWT containing the exact repository, branch, and environment (repo:Ace-Vector-Labs/payments-api:environment:production). AWS IAM (or GCP / Azure) checks that signature and hands the runner a temporary token that expires in 15 minutes.

3. GitHub Environments & Deployment Gates

Adding environment: production to a deployment job gives you three useful safety checks before a deploy runs:

  • Required Reviewers: Pause the deployment job until someone on @Ace-Vector-Labs/sre-oncall clicks Approve.
  • Wait Timers: Enforce a 10-minute cooldown after staging before production starts.
  • Branch Restrictions: Only allow commits from main or signed release tags (v*.*.*) to assume the production environment role.

4. Ephemeral Kubernetes Runners with Actions Runner Controller (ARC)

Sometimes GitHub's hosted runners aren't enough—you might need 64 GB of RAM, an NVIDIA GPU, or access to a staging database inside a private VPC subnet.

The mistake many teams make is spinning up a static EC2 VM, installing the runner agent on it, and leaving it running forever. Because jobs on a static VM share the same disk and OS, a dirty build or a compromised dependency from one run can linger and affect the next run.

Instead, run Actions Runner Controller (ARC) on Kubernetes using AutoscalingRunnerSet. When a job asks for runs-on: arc-vpc-runners, ARC spins up a fresh Kubernetes Pod, runs one single job, and destroys the Pod as soon as the job finishes.

5. Walkthrough: Reusable OIDC Deployments + Ephemeral ARC Runners

Step 1: Create a Reusable Deployment Workflow (platform-ci)

# Ace-Vector-Labs/platform-ci/.github/workflows/reusable-eks-deploy.yml
name: Reusable OIDC Container Build & EKS Deploy

on:
  workflow_call:
    inputs:
      service_name:
        required: true
        type: string
      aws_region:
        required: false
        type: string
        default: "us-east-1"
      environment_name:
        required: true
        type: string
      iam_role_arn:
        required: true
        type: string

jobs:
  deploy:
    runs-on: arc-vpc-runners
    environment: ${{ inputs.environment_name }}
    permissions:
      contents: read
      id-token: write # Needed to request an OIDC token

    steps:
      - uses: actions/checkout@v4

      - name: Assume AWS IAM Role via OIDC (No static secrets)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ inputs.iam_role_arn }}
          role-session-name: gha-${{ inputs.service_name }}-${{ github.run_id }}
          aws-region: ${{ inputs.aws_region }}

      - name: Build, Push & Roll Out Image
        run: |
          IMAGE_TAG="${ECR_REGISTRY}/${{ inputs.service_name }}:${GITHUB_SHA}"
          docker build -t "$IMAGE_TAG" .
          docker push "$IMAGE_TAG"
          kubectl set image deployment/"${{ inputs.service_name }}" \
            app="$IMAGE_TAG" --record=false
          kubectl rollout status deployment/"${{ inputs.service_name }}" --timeout=180s

Step 2: Lock Down the AWS IAM Trust Policy

In AWS IAM, scope the trust policy on arn:aws:iam::123456789012:role/gha-payments-prod so only the production environment of Ace-Vector-Labs/payments-api can assume the role:

{
  "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",
          "token.actions.githubusercontent.com:sub": "repo:Ace-Vector-Labs/payments-api:environment:production"
        }
      }
    }
  ]
}

Step 3: Configure Ephemeral Kubernetes Runners (values.yaml)

# helm/arc-runner-scale-set-values.yaml
githubConfigUrl: "https://github.com/Ace-Vector-Labs"
githubConfigSecret: arc-github-app-secret
runnerScaleSetName: "arc-vpc-runners"
minRunners: 0
maxRunners: 20

containerMode:
  type: "kubernetes"
  kubernetesModeWorkVolumeClaim:
    accessModes: ["ReadWriteOnce"]
    storageClassName: "gp3-encrypted"
    resources:
      requests:
        storage: 15Gi

template:
  spec:
    securityContext:
      runAsNonRoot: true
      runAsUser: 1001
    containers:
      - name: runner
        image: ghcr.io/actions/actions-runner:latest
        command: ["/home/runner/run.sh"]

Quick Checklist

  • Switch cloud deploys to OIDC: Use permissions: id-token: write and check the sub claim in your cloud IAM trust policy so you can delete static cloud keys from GitHub Secrets.
  • Never attach self-hosted runners to public repos without approval gates: Otherwise, anyone can open a fork PR that modifies the workflow file and runs arbitrary commands inside your private network.
  • Pin third-party actions to commit SHAs: For community actions outside your org, pin to a full 40-character commit SHA so a compromised upstream tag cannot inject code into your builds.

Series Navigation: ← Previous: Part 6: GitHub Actions I — CI/CD Pipeline Fundamentals | Next: Part 8: Security & Supply Chain Defense → | All Articles

← Back to all articles