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.
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.
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.
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-onagar tim aplikasi tidak bingung.
Kesalahan Umum
- memindahkan runner ke Kubernetes tetapi tetap memberi hak cluster-admin
- memakai image runner
latesttanpa 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 TrakteerKeep Reading
Related posts
Hardening vLLM Inference Service di Kubernetes dengan Istio dan OPA
Panduan production-ready untuk menjalankan vLLM di Kubernetes dengan kontrol jaringan, policy admission, mTLS, dan guardrail operasional yang lebih aman.
eBPF Runtime Security di Kubernetes dengan Guardrail yang Praktis
Panduan production-grade untuk menerapkan eBPF runtime security di Kubernetes tanpa menciptakan alert noise dan operability issue yang tidak perlu.
Mengamankan Ephemeral CI Runners dari Supply Chain Drift
Strategi production-grade untuk memperkecil blast radius ephemeral CI runners melalui workload identity, egress control, dan artifact trust yang dapat diaudit.