IT/CI CD

ARC 0.14.0로 self-hosted runner 셋업하기, multilabel까지

gfrog 2026. 8. 25. 03:17

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에 남겨두고 있으면, 완전히 넘긴 사례가 있으면 배우고 싶다.


참고 리소스

태그: DevOps, GitHub Actions, ARC, Kubernetes, self-hosted-runner, CI/CD