A

Blog Post

Ephemeral GitHub Actions Runners di Kubernetes dengan Actions Runner Controller

Panduan operasional untuk menjalankan GitHub Actions runner yang ephemeral di Kubernetes dengan isolasi lebih baik, autoscaling, dan kontrol secret yang lebih rapi.

6 min read
Ephemeral GitHub Actions Runners di Kubernetes dengan Actions Runner Controller

TL;DR

  • Ephemeral runner jauh lebih aman daripada runner VM panjang umur karena state build dibuang setelah job selesai.
  • Actions Runner Controller memudahkan scale set runner di Kubernetes, tetapi isolasi network, registry access, dan secret distribution tetap harus dirancang.
  • Jangan jadikan runner pod sebagai extension dari cluster admin. Runner adalah workload untrusted yang menjalankan kode pipeline.

Pendahuluan

Self-hosted CI runner selalu menggoda karena memberi fleksibilitas penuh. Kita bisa memasang tool sendiri, menaruh runner dekat artifact registry internal, dan menghindari batasan worker hosted. Masalahnya, runner tradisional yang hidup lama hampir selalu mengumpulkan drift: credential tertinggal, Docker layer cache tidak pernah dibersihkan, dan satu job dapat meninggalkan state yang memengaruhi job berikutnya. Untuk supply chain modern, itu terlalu berisiko.

Pendekatan ephemeral runner di Kubernetes lebih sehat. Setiap job mendapat worker yang baru, lalu worker itu hilang setelah selesai. Namun memindahkan runner ke cluster bukan berarti risiko hilang. Kode pipeline tetap untrusted. Ia bisa menjalankan container build, menarik dependency acak, atau mencoba mengakses network internal. Karena itu desainnya harus menganggap runner sebagai beban kerja berisiko tinggi yang perlu dibatasi, bukan dibantu.

Tutorial ini membangun GitHub Actions runner ephemeral menggunakan Actions Runner Controller (ARC) berbasis runner scale set. Fokusnya ada pada konektivitas GitHub, autoscaling, image custom, dan guardrail minimum untuk production.

Problem di Production

Runner yang hidup lama sering menjadi tempat paling buruk untuk menyimpan fragmen rahasia organisasi. SSH key, AWS credential, npm token, dan container registry auth mudah tertinggal di disk worker. Ketika satu job kompromi, dampaknya bisa melompat ke job lain karena state host tidak pernah dibuang bersih.

Problem lain adalah kapasitas. Jika runner terlalu sedikit, pipeline menunggu lama. Jika runner terlalu banyak dan tidak ephemeral, biaya cluster serta attack surface ikut membengkak. ARC membantu scale dinamis, tetapi hanya jika image runner, network egress, dan secrets dirancang dengan benar.

Prerequisites

  • Cluster Kubernetes 1.29+.
  • kubectl, helm, dan akses admin untuk namespace CI.
  • GitHub organization atau repository dengan izin membuat GitHub App atau PAT yang sesuai.
  • Container registry internal jika ingin custom runner image.
  • Namespace terpisah untuk runner, misalnya ci-runners.
  • cert-manager tersedia jika chart ARC memerlukannya pada versi yang digunakan.

Architecture / Context

Model yang dipakai adalah runner scale set. ARC controller berkomunikasi ke GitHub untuk mendapatkan pekerjaan. Saat job muncul, controller membuat runner pod baru dari image yang sudah ditentukan. Pod itu menjalankan satu pekerjaan, lalu dihancurkan. Artifact dan log tetap berada pada ecosystem CI, bukan di worker yang hidup lama.

Boundary pentingnya ada tiga. Pertama, ARC controller yang punya koneksi terkontrol ke GitHub API. Kedua, runner pod sebagai workload untrusted. Ketiga, storage dan registry internal yang hanya boleh diakses secukupnya. Memisahkan tiga area ini penting agar kompromi runner tidak otomatis berubah menjadi kompromi cluster.

Concept diagram

Step-by-Step Implementation

Step 1: Buat namespace dan secret GitHub App

kubectl create namespace ci-runners

Jika memakai GitHub App, simpan secret:

kubectl -n ci-runners create secret generic github-auth \
  --from-literal=github_app_id='123456' \
  --from-literal=github_app_installation_id='7890123' \
  --from-file=github_app_private_key=./github-app.pem

GitHub App lebih sehat daripada PAT karena scope dan rotasinya lebih jelas.

Step 2: Pasang ARC

helm repo add actions-runner-controller https://actions-runner-controller.github.io/actions-runner-controller
helm repo update
helm upgrade --install arc actions-runner-controller/actions-runner-controller \
  -n ci-runners \
  --set authSecret.create=false \
  --set authSecret.name=github-auth

Beberapa distribusi ARC terbaru memisahkan chart controller dan runner scale set. Prinsipnya sama: controller dipasang dulu, runner baru menyusul.

Step 3: Buat custom runner image

Untuk job Docker build, Terraform, atau kubectl, image runner sebaiknya sudah berisi tool standar.

FROM ghcr.io/actions/actions-runner:latest

USER root
RUN apt-get update && apt-get install -y \
    docker.io \
    git \
    unzip \
    jq \
    curl \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

RUN curl -fsSL https://get.helm.sh/helm-v3.15.2-linux-amd64.tar.gz | tar -xzO linux-amd64/helm > /usr/local/bin/helm \
    && chmod +x /usr/local/bin/helm

USER runner

Build dan push ke registry internal:

docker build -t registry.example.com/ci/gha-runner:2026.04.25 .
docker push registry.example.com/ci/gha-runner:2026.04.25

Jangan biarkan runner apt install tool saat runtime kecuali benar-benar perlu. Itu memperbesar drift dan memperlambat startup job.

Step 4: Buat runner scale set

githubConfigUrl: "https://github.com/example-org"
githubConfigSecret: github-auth

maxRunners: 20
minRunners: 0

template:
  spec:
    serviceAccountName: gha-runner
    containers:
      - name: runner
        image: registry.example.com/ci/gha-runner:2026.04.25
        command: ["/home/runner/run.sh"]
        resources:
          requests:
            cpu: "2"
            memory: 4Gi
          limits:
            cpu: "4"
            memory: 8Gi
        securityContext:
          runAsNonRoot: true
          allowPrivilegeEscalation: false
          capabilities:
            drop: ["ALL"]

Install:

helm upgrade --install gha-runner-set actions-runner-controller/gha-runner-scale-set \
  -n ci-runners \
  -f runner-scale-set.yaml

Step 5: Tambahkan NetworkPolicy

Runner butuh keluar ke GitHub, registry, dan kadang artifact storage. Jangan beri egress bebas jika tidak perlu.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: runner-egress-policy
  namespace: ci-runners
spec:
  podSelector:
    matchLabels:
      actions.github.com/scale-set-name: gha-runner-set
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0

Dalam cluster nyata, lebih baik gantikan 0.0.0.0/0 dengan egress gateway atau daftar domain lewat proxy terkontrol.

Step 6: Pisahkan credential registry dan cloud access

Jika pipeline butuh pull image private:

kubectl -n ci-runners create secret docker-registry regcred \
  --docker-server=registry.example.com \
  --docker-username=robot-ci \
  --docker-password='replace-me'

Kemudian referensikan sebagai imagePullSecrets atau mount sesuai kebutuhan. Jangan tempel credential cloud luas ke semua job jika hanya sebagian workflow yang perlu.

Step 7: Buat workflow uji

name: ci-ephemeral

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: [self-hosted, linux, x64]
    steps:
      - uses: actions/checkout@v4
      - name: Show runner info
        run: |
          hostname
          whoami
          docker version
          helm version

Jika organisasi memakai label scale set khusus, sesuaikan runs-on.

Step 8: Tambahkan cleanup guardrail

Secara default runner ephemeral akan dibuang, tetapi pastikan workspace sensitif tidak dipersist ke PVC bersama. Hindari hostPath. Jika butuh cache dependency, gunakan cache yang scoped dan disposable, bukan disk node mentah.

Architecture flow

Verification

Cek controller dan runner set:

kubectl -n ci-runners get pods
kubectl -n ci-runners get deployments
kubectl -n ci-runners get autoscalingrunnersets

Trigger workflow test, lalu lihat pod runner muncul:

kubectl -n ci-runners get pods -w

Log runner:

kubectl -n ci-runners logs POD_NAME

Sukses berarti:

  • job GitHub mendapat runner self-hosted dengan cepat
  • pod runner dibuat saat job mulai
  • pod runner hilang setelah job selesai
  • tidak ada state job lama yang tertinggal untuk job berikutnya

Troubleshooting

Jika runner tidak muncul, cek secret GitHub App dan log controller:

kubectl -n ci-runners logs deploy/arc-actions-runner-controller
kubectl -n ci-runners get secret github-auth -o yaml

Jika job queue lama, cek maxRunners, resource request, dan capacity node:

kubectl describe nodes
kubectl -n ci-runners describe autoscalingrunnerset gha-runner-set

Jika pod runner gagal pull image, cek registry secret dan policy egress:

kubectl -n ci-runners describe pod RUNNER_POD

Masalah umum lain adalah workflow meminta label runner yang tidak cocok dengan scale set.

Production Notes

Anggap runner sebagai workload semi-trusted. Ia menjalankan kode dari repo dan action pihak ketiga. Batasi haknya ke registry, cloud, dan Kubernetes API. Gunakan image runner yang dipin versinya dan dipindai rutin. Tambahkan observability minimum: queue time job, startup time runner, pod failure rate, dan volume workflow gagal karena environment issue.

Jika cluster digunakan multi-tenant, pertimbangkan node pool khusus CI agar noise build tidak mengganggu aplikasi produksi. Untuk build container yang sensitif, gunakan rootless build tool atau builder terisolasi daripada Docker daemon penuh jika memungkinkan.

Trade-offs

Ephemeral runner meningkatkan keamanan dan kebersihan state, tetapi startup job sedikit lebih lambat dibanding worker yang sudah panas. ARC memudahkan autoscaling, tetapi menambah dependency platform yang harus dipelihara. Runner image custom mengurangi drift, tetapi perlu release management seperti artifact platform lainnya.

Failure Modes

  • Runner pod terlalu permisif dan menjadi jalur lateral movement ke internal network.
  • Image runner membengkak dan startup job melambat drastis.
  • GitHub App atau registry credential salah rotasi sehingga seluruh pipeline berhenti.
  • Cache dibangun dengan hostPath atau volume bersama sehingga state job tidak lagi benar-benar ephemeral.

Best Practices

  • Gunakan GitHub App daripada PAT jika memungkinkan.
  • Pisahkan controller, runner, dan build dependency access dengan batas yang jelas.
  • Terapkan egress control dan image scanning pada runner image.
  • Uji runner pada workflow sederhana sebelum memindahkan pipeline besar.
  • Dokumentasikan label runs-on agar tim aplikasi tidak bingung.

Kesalahan Umum

  • memindahkan runner ke Kubernetes tetapi tetap memberi hak cluster-admin
  • memakai image runner latest tanpa release process
  • mengandalkan disk node untuk cache sensitif
  • tidak memisahkan node CI dari node aplikasi produksi

Kesimpulan

Ephemeral GitHub Actions runner di Kubernetes memberi improvement nyata pada kebersihan state dan keamanan supply chain, tetapi nilainya baru terasa jika runner diperlakukan sebagai workload berisiko tinggi. Dengan ARC, image yang dipin, dan batas network yang masuk akal, tim bisa mendapatkan fleksibilitas self-hosted CI tanpa mewarisi semua masalah runner panjang umur.

Kalau artikel ini membantu, kamu bisa support eksperimen berikutnya.

Apresiasi di Trakteer

Keep Reading

Related posts