IT/CI CD

GitHub Actions Runner Controller(ARC) v0.14 scale set 도입 가이드

gfrog 2026. 6. 13. 06:44
SMALL

GitHub-hosted runner 비용이 매달 슬금슬금 오르더니, 우리 팀에서도 결국 self-hosted로 옮길지를 진지하게 검토하게 됐다. 처음에는 옛날 ARC(legacy mode, RunnerDeployment CRD 쓰던 그거)를 떠올리고 망설였는데, 알아보니 ARC는 2년 전부터 scale set 기반으로 완전히 재설계됐고, 올해 3월에 나온 0.14.0에서는 내부 GitHub API 클라이언트마저 공개 라이브러리(actions/scaleset)로 교체됐다. 즉, 지금 ARC는 사실상 "scale set 전용"이라고 봐도 된다.

이 글은 처음부터 ARC scale set으로 시작하려는 사람을 위한 도입 가이드다. 우리 팀이 PoC 단계에서 겪은 함정도 함께 정리했다.

무엇이 바뀌었나

기존 ARC는 webhook으로 GitHub의 workflow 이벤트를 받아서 자체 컨트롤러가 runner pod 수를 조절하는 구조였다. webhook 받는 ingress가 필요했고, scaling 로직이 복잡했고, 무엇보다 GitHub Actions 내부 API 변경에 자주 끌려다녔다.

scale set 기반(gha-runner-scale-set)은 다르다. GitHub Actions 서비스가 공식적으로 "runner scale set"이라는 1급 리소스를 노출하고, ARC의 listener pod가 거기에 long-poll로 붙어서 job 수를 받아온다. webhook도, ingress도 필요 없다. listener가 큐 길이를 보고 ephemeral runner pod를 떠서 job 하나 처리하고 죽는 방식이다.

요약하자면 push 모델에서 pull 모델로 바뀌었다. 운영 입장에서 훨씬 단순하다.

사전 준비

GitHub App을 먼저 만든다. PAT(personal access token)로도 되긴 하는데, 권장은 GitHub App이다. 권한은 organization 레벨이면 Self-hosted runners: Read & write, repository 레벨이면 추가로 Actions: Read. App을 설치한 뒤 App ID, Installation ID, private key를 챙긴다.

쿠버네티스 측은 1.28 이상이면 충분하다. ARC는 cluster-scoped CRD를 깐다는 점만 미리 확인해두자. 멀티 테넌시 환경에서 RBAC 정책 검토가 필요할 수 있다.

설치

컨트롤러부터 먼저 깐다.

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

여기까진 가볍다. listener와 ephemeral runner pod의 동작을 관장하는 컨트롤러만 깔린 상태다.

다음은 실제 runner scale set. GitHub App 비밀을 먼저 만든다.

kubectl create namespace arc-runners

kubectl create secret generic arc-gh-app \
  --namespace arc-runners \
  --from-literal=github_app_id=123456 \
  --from-literal=github_app_installation_id=78901234 \
  --from-file=github_app_private_key=./app-private-key.pem

그리고 scale set을 띄운다.

# values-runner.yaml
runnerScaleSetName: k8s-runners-default
githubConfigUrl: https://github.com/our-org
githubConfigSecret: arc-gh-app

minRunners: 1
maxRunners: 30

containerMode:
  type: kubernetes   # 또는 "dind"

template:
  spec:
    containers:
      - name: runner
        image: ghcr.io/actions/actions-runner:latest
        resources:
          requests:
            cpu: "500m"
            memory: "1Gi"
          limits:
            cpu: "2"
            memory: "4Gi"
helm install k8s-runners-default \
  --namespace arc-runners \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set \
  --version 0.14.0 \
  -f values-runner.yaml

설치 후 GitHub organization의 Settings → Actions → Runners 페이지에 가보면 k8s-runners-default가 idle 상태로 떠 있어야 한다. 워크플로우에서 runs-on: k8s-runners-default로 지정하면 그 큐로 job이 흘러간다.

containerMode를 무엇으로 정할지

가장 많이 헷갈리는 부분이다. 두 가지 선택지가 있다.

dind는 익숙한 방식이다. runner pod 안에서 docker CLI가 그대로 동작한다. workflow 안에서 docker build, docker run을 자유롭게 쓸 수 있다. 단점은 privileged 컨테이너가 필요하다는 점. 보안팀이 싫어한다.

kubernetes 모드는 runner가 job step마다 새로운 pod을 띄워서 step을 실행한다. privileged가 필요 없고, 격리가 더 깔끔하다. 다만 일부 워크플로우 패턴(특히 docker/build-push-action 같은 액션)이 그대로 안 먹힌다. Buildx의 kubernetes driver나 BuildKit standalone을 별도로 붙여야 한다.

우리 팀은 처음에 kubernetes 모드로 갔다가, 기존 워크플로우 중 컨테이너 빌드하는 게 너무 많아서 결국 두 개의 scale set을 운영하기로 했다. 일반 워크플로우용 k8s-runners-default(kubernetes 모드)와 빌드 전용 k8s-runners-build(dind, privileged 허용 네임스페이스). 양쪽 다 같은 컨트롤러 밑에 산다.

운영하면서 만난 함정

minRunners를 0으로 두면 첫 job이 30초 정도 늦다. ephemeral runner pod 뜨고, GitHub Actions에 등록되고, job pull하는 데까지 시간이 걸린다. 캐시가 콜드한 노드면 1분 이상 걸릴 때도 있다. 자주 도는 워크플로우라면 minRunners를 1~2로 두자. 비용은 거의 안 든다.

listener pod가 죽으면 큐가 멈춘다. listener는 single replica다. 노드 drain 같은 이벤트에 약하다. 우리는 listener pod에 PodDisruptionBudget을 명시적으로 걸고, 노드 affinity로 안정적인 노드에만 뜨도록 고정했다.

job 로그가 GitHub-hosted runner보다 살짝 느리게 보인다. ephemeral runner가 job 끝나면 곧장 죽기 때문에 마지막 몇 줄이 잘릴 때가 있다. 0.14에서 많이 개선됐지만 완전히 없어진 건 아니다. step 마지막에 sync 한 줄 넣는 걸로 워크어라운드 중이다.

Karpenter나 cluster-autoscaler와의 궁합. runner pod는 짧게 살다 죽으니까 노드 consolidation이 너무 공격적이면 노드가 계속 떴다 꺼졌다 한다. Karpenter라면 consolidationPolicy: WhenEmptyconsolidateAfter를 넉넉히 주자. ephemeral runner 워크로드는 spot 인스턴스와도 잘 맞는데, 빌드 캐시를 PVC로 들고 다닐 거면 spot 회수 정책을 한번 더 검토해야 한다.

마이그레이션할 때 고려할 점

기존 GitHub-hosted runner로 다 돌리던 조직이 한번에 self-hosted로 옮기는 건 권장 안 한다. workflow별로 runs-on 라벨만 바꾸면 되니까 점진적으로 이관 가능하다. 비용 큰 워크플로우(긴 빌드, 매트릭스 잡)부터 옮기면 ROI가 빨리 나온다.

GitHub-hosted larger runner(특히 ARM, GPU 변종)와 비교했을 때 self-hosted가 항상 싸지는 않다. 노드 유틸라이제이션이 60% 이상 나와야 비용 우위가 보이기 시작한다. 우리는 처음 두 달간 매주 Prometheus로 runner pod utilization 보면서 maxRunners를 조정했다.

추가 리소스

다음에는 ARC scale set에 metrics-server 붙여서 build queue depth 기반으로 노드 프로비저닝을 미리 트리거하는 패턴을 다뤄볼 생각이다.

BIG