GitHub Actions self-hosted runner를 Kubernetes 위에 굴리는 방법으로 요즘 표준은 사실상 Actions Runner Controller(ARC)다. 근데 올해 3월에 0.14.0이 GA로 풀리면서 꽤 큰 변화가 있었다. multilabel 지원이 들어왔고, 내부 클라이언트도 actions/scaleset 라이브러리로 교체됐다. 우리 팀도 이번 주에 0.11 → 0.14로 올리면서 몇 가지 정리한 게 있어서 가이드 형태로 남긴다.
이 글은 "ARC 처음 세팅한다" 시나리오를 기준으로 쓴다. 이미 legacy(runner deployment) 모드로 굴리고 있는 팀이라면 뒷부분 마이그레이션 노트만 봐도 된다.
왜 scale set 모드인가
ARC는 두 가지 모드가 있다. 예전부터 있던 RunnerDeployment 기반의 legacy 모드와, GitHub이 공식적으로 밀고 있는 AutoscalingRunnerSet(scale set) 모드. legacy는 webhook + HRA(HorizontalRunnerAutoscaler)를 조합해서 job 큐를 폴링하는 구조인데, 이게 대규모에서는 이벤트 유실이나 스케일 지연이 자주 걸렸다.
scale set 모드는 GitHub이 서버 쪽에서 직접 관리하는 listener를 두고 그쪽으로 job을 밀어넣기 때문에 이 문제가 훨씬 덜하다. 0.14부터는 아예 scale set 클라이언트 라이브러리가 별도 Go 모듈로 공개돼서, 원하면 K8s 없이 다른 인프라 위에도 오토스케일러를 자체 구현할 수 있게 됐다. 우리 팀 기준으로는 그냥 K8s에서 scale set 모드 쓰는 게 답이라 그대로 간다.
설치
Helm chart 두 개를 나눠서 설치한다. 컨트롤러 하나, scale set 하나. 이게 익숙하지 않은 사람이 많은데, 컨트롤러는 클러스터에 한 벌만 있으면 되고 scale set은 팀/워크로드 단위로 여러 개 만들어 붙이는 구조다.
# 1. 컨트롤러 설치
helm install arc \
--namespace arc-systems --create-namespace \
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller \
--version 0.14.0
컨트롤러가 잘 떴는지는 이걼로 확인:
kubectl -n arc-systems get pods
kubectl -n arc-systems logs -l app.kubernetes.io/name=gha-rs-controller
scale set 하나 만들기
이제 실제 runner pool 역할을 하는 scale set을 붙인다. PAT 대신 GitHub App 인증을 쓰는 걸 강력히 권장한다. PAT는 만료 관리가 지옥이다.
# values-devops-runners.yaml
githubConfigUrl: https://github.com/kurly-org
githubConfigSecret:
github_app_id: "123456"
github_app_installation_id: "78910"
github_app_private_key: |
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
# 여기가 0.14의 포인트
runnerScaleSetName: "devops-linux-x64"
runnerGroup: "default"
# multilabel — 이게 새로 지원됨
labels:
- "self-hosted"
- "linux"
- "x64"
- "devops"
minRunners: 1
maxRunners: 20
template:
spec:
containers:
- name: runner
image: ghcr.io/actions/actions-runner:2.323.0
resources:
requests:
cpu: "1"
memory: "2Gi"
limits:
cpu: "4"
memory: "8Gi"
helm install devops-runners \
--namespace arc-runners --create-namespace \
-f values-devops-runners.yaml \
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set \
--version 0.14.0
multilabel, 이게 왜 편한가
이전 버전에서는 scale set 하나당 label 하나만 붙일 수 있었다. 그래서 워크플로에서 runs-on: [self-hosted, linux, x64, gpu] 이런 식으로 여러 label로 매칭하려고 하면, scale set 이름 자체가 사실상 하나의 label이 되고 나머지는 그냥 태그처럼 붙는 구조라 좀 헶갈렸다.
0.14부터는 위의 values처럼 labels: 배열에 여러 개 나열하면 되고, 워크플로에서도 그대로 여러 label을 조합해서 쓴다:
# .github/workflows/build.yml
jobs:
build:
runs-on: [self-hosted, linux, x64, devops]
steps:
- uses: actions/checkout@v4
- run: make build
이렇게 하면 위에서 만든 devops-linux-x64 scale set으로 정확히 라우팅된다. 서로 다른 팀에서 label 조합이 겹치지 않게만 잘 관리하면 된다. 우리 팀은 첫 번째 label을 팀명(devops, platform, data)으로 두고 CPU/GPU/아키텍처를 뒤에 붙이는 규칙으로 정리했다.
리스너 파드에 관한 팁
0.14의 두 번째 변화는 listener pod scheduling 개선인데, 실제로 우리가 부딪혔던 문제 하나가 이걸로 해결됐다. 이전에는 listener pod이 컨트롤러 네임스페이스에 뜨면서 taint/toleration이 안 먹는 경우가 있어서, spot 노드 위에 잘못 스케줄되면 listener가 뜨는 순간 죽는 이슈가 있었다. 0.14에서는 listener 리소스에도 label/annotation, 그리고 nodeSelector/tolerations를 직접 지정할 수 있다.
listenerTemplate:
spec:
nodeSelector:
workload: system
tolerations:
- key: dedicated
operator: Equal
value: system
effect: NoSchedule
runner용 노드가 spot이라도 listener는 on-demand 노드에 붙게 하는 식으로 안정성을 확보할 수 있다.
마이그레이션 노트 (legacy → scale set)
RunnerDeployment로 굴리던 클러스터에서 옮길 때 조심할 것 몇 가지:
첫째, 두 모드는 같은 컨트롤러 안에 공존할 수 없다. 그래서 새 컨트롤러는 다른 네임스페이스에 별도로 띄우고, 워크플로를 하나씩 runs-on 라벨만 바꿔서 옮긴 다음, 기존 RunnerDeployment는 완전히 비워지고 나서 걷어내는 식으로 진행했다.
둘째, HRA에서 쓰던 webhook 기반 스케일링 설정은 그대로 옮길 수 없다. scale set은 GitHub 서버가 알아서 스케일 신호를 보내주기 때문에 우리 쪽에서 튜닝할 게 별로 없다. minRunners, maxRunners 정도만 신경 쓰면 된다.
셋째, 캐시 볼륨. legacy에서 hostPath로 캐시 공유하던 팀은 scale set에서도 그대로 쓸 수 있지만, 노드 하나에 여러 runner가 붙을 때 lock 이슈가 생길 수 있어서 우리는 그냥 팀별로 PVC 분리하는 쪽으로 정리했다.
검증할 것
세팅 다 하고 나면 세 가지만 확인하면 된다:
1. kubectl -n arc-systems get autoscalingrunnerset — 새 scale set이 등록됐는가
2. GitHub org > Settings > Actions > Runners — self-hosted runner가 idle 상태로 뜨는가
3. 실제 워크플로 하나 돌려서 pod이 스케줄되고 job이 성공적으로 끝나는가
마무리
0.14의 multilabel 하나만 가지고도 upgrade 할 만한 가치가 있다. 특히 팀이 여러 개고 scale set을 팀별로 나눠 쓰는 조직이라면 label 규칙을 다시 짜기 좋은 타이밍이다. 우리 팀은 이번 upgrade하면서 label naming convention 문서를 처음으로 만들었는데, 진작에 만들 걸 그랬다는 후기다.
혹시 legacy 모드에서 못 옮기고 있는 팀 있으면 어떤 이유 때문인지 궁금하다. 우리도 아직 GPU 워크로드는 legacy에 남겨두고 있으면, 완전히 넘긴 사례가 있으면 배우고 싶다.
참고 리소스
- GitHub Docs: Deploying runner scale sets with Actions Runner Controller
- GitHub Changelog: Actions Runner Controller release 0.14.0
태그: DevOps, GitHub Actions, ARC, Kubernetes, self-hosted-runner, CI/CD
'IT > CI CD' 카테고리의 다른 글
| Argo Rollouts vs Flagger, 뭘 쓸까 (0) | 2026.08.28 |
|---|---|
| Renovate vs Dependabot, 컨테이너 이미지 업데이트 관점에서 (0) | 2026.08.22 |
| ArgoCD ApplicationSet Progressive Sync 실무 가이드 (0) | 2026.08.22 |
| 새벽에 GHA self-hosted runner가 디스크로 뻗은 이야기 (0) | 2026.08.15 |
| BuildKit cache mount, CI에서도 진짜 캐시 되게 하기 (0) | 2026.08.13 |