목차
- EC2 self-hosted runner를 떠나기로 한 이유
- ARC가 두 종류였다 — 여기서 1주일 날렸다
- Helm 설치는 5분, 인증은 한나절
- 운영 들어가서 마주친 구간들
- 3개월 청구서 — EC2 대비 얼마나 줄었나
- 언제 쓰고 언제 안 쓸 것인가
EC2 self-hosted runner를 떠나기로 한 이유
GitHub Actions ARC 쿠버네티스 러너 설치 검토는 self-hosted runner를 EC2 m5.large 3대로 운영하다가 빌드 큐가 적체되는 일이 반복되면서 시작됐다. 동시 빌드가 4개를 넘으면 5번째 빌드는 평균 30분 가까이 대기했다. 그렇다고 EC2를 5대 6대로 늘려두자니 새벽이나 주말은 거의 idle이라 비용이 아까웠다.
GitHub-hosted runner로 그냥 넘어가지 못한 이유도 있었다. 사내 ECR과 RDS에 접근하는 통합 테스트가 있어서 VPC 내부에서 도는 러너가 필요했다. 사설 VPN 게이트웨이를 띄우는 우회로도 검토했지만 운영 부담 대비 이점이 약하다는 판단이었다.
여기까지가 출발점이다. 빌드 트래픽은 들쑥날쑥한데 EC2는 24시간 켜져 있고, 급한 빌드는 큐에서 기다린다. 그래서 쿠버네티스 위에서 자동 확장되는 ephemeral 러너로 옮기기로 했다. 3개월짜리 마이그레이션 프로젝트의 시작이었다.
ARC가 두 종류였다 — 여기서 1주일 날렸다
이 글을 보고 시작하는 사람은 절대 헤매지 않아야 할 부분이다.
ARC는 실제로 두 종류가 공존한다. 2026년 5월 현재 기준이다.
- Legacy ARC (
summerwind/actions-runner-controller): 커뮤니티에서 시작된 원조. CRD가RunnerDeployment,HorizontalRunnerAutoscaler같은 형태로 분리돼 있다. - GitHub 공식 ARC (
actions/actions-runner-controller, scale set 모드): GitHub이 인수해 만든 새 아키텍처. CRD가AutoscalingRunnerSet하나로 단순화됐다.
처음 구글 검색 상위에 잡히는 한국어 글들은 대부분 legacy 기준이다. 블로그 글대로 helm repo를 추가하고 따라가다 보면 새 버전 문서와 명령어가 미묘하게 어긋난다. 같은 ARC라는 이름인데 내부 동작이 달라서, 디버깅하다 한참 후에야 "이거 다른 프로젝트구나"를 깨닫는다. (개인적으로 이 부분이 마이그레이션 전체에서 가장 큰 함정이었다.)
신규 도입이라면 무조건 GitHub 공식 scale set 버전을 골라야 한다. Legacy 쪽은 deprecation 안내가 공식 README에 박혀 있다 (출처: actions/actions-runner-controller README, 2026년 5월 확인). 기존에 legacy로 깔린 환경이 있다면 마이그레이션 가이드가 같은 저장소 docs/ 하위에 따로 있다. 새 차트는 OCI 레지스트리(oci://ghcr.io/actions/...)에서 받는다는 점도 다르다. helm repo add 명령으로 잡히지 않는다.
Helm 설치는 5분, 인증은 한나절
한편, 설치 자체는 의외로 짧다. controller와 runner scale set이 별도 차트로 분리돼 있다는 점만 인지하면 된다.
# 1) Controller 설치 — 클러스터 전체에 하나
helm install arc \
--namespace arc-systems \
--create-namespace \
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller \
--version 0.9.3
# 2) Runner scale set 설치 — 실제 워커 풀
helm install arc-runner-set \
--namespace arc-runners \
--create-namespace \
--set githubConfigUrl="https://github.com/myorg/myrepo" \
--set githubConfigSecret=arc-runner-secret \
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set \
--version 0.9.3
또한, 설치는 5분이면 끝나고, 진짜 시간은 인증에서 잡아먹힌다.
PAT vs GitHub App, 결국 App으로 갔다
그러나, 처음에는 빠르게 가려고 PAT(Personal Access Token)로 시작했다. 5분 만에 러너 pod가 떴다. 다만 PAT는 발급한 사람 계정에 묶이고, 권한 스코프가 너무 넓고, 그 사람이 퇴사하면 토큰이 회수된다. 운영 환경에서는 GitHub App을 만드는 게 사실상 표준이다.
GitHub App 발급은 Settings → Developer settings → GitHub Apps에서 한다. 권한은 Actions: Read & Write, Administration: Read & Write, Metadata: Read까지. App ID, Installation ID, private key 세 가지를 secret으로 만들어 controller에 전달한다.
kubectl create secret generic arc-runner-secret \
--namespace=arc-runners \
--from-literal=github_app_id=123456 \
--from-literal=github_app_installation_id=78901234 \
--from-file=github_app_private_key=./private-key.pem
그런데, 여기서 한 번 더 헤맸다. Installation ID 자리에 App ID를 잘못 넣은 경우다. 두 값이 비슷한 자릿수의 숫자라 혼동하기 쉽다. controller 로그에 401 Unauthorized가 뜨면 십중팔구 이 둘 중 하나가 바뀌었다. 그다음으로 흔한 실수는 private key를 다운로드받을 때 줄바꿈이 깨지는 경우다. --from-file로 넣으면 안전하지만 --from-literal로 복붙하면 거의 실패한다.
운영 들어가서 마주친 구간들
설치까지는 깔끔했는데 실서비스 빌드가 들어오니 예상 못 한 구간이 줄줄이 나왔다.
Pod가 뜨다 말다 한다
처음 며칠은 워크플로우를 트리거해도 러너 pod가 뜨다 말고 사라졌다. 원인은 노드에 spot interruption이 자주 발생한 것이었다. Karpenter로 spot 노드를 뽑고 있었는데, ephemeral 러너는 작업 도중 노드가 빠지면 그 잡 자체가 그대로 실패한다. 자동 재시도 로직도 없다. 단발성이다.
예를 들어, 해결책은 두 갈래로 나눴다. 짧은 잡(린트, 단위 테스트, 5분 이내)은 spot으로 보내고, 긴 잡(통합 테스트, 도커 빌드)은 on-demand 노드로 보냈다. nodeSelector와 tolerations를 잡 종류별로 분리하는 식이다.
# values.yaml — on-demand 전용 scale set
template:
spec:
nodeSelector:
workload-type: ci-ondemand
tolerations:
- key: ci-ondemand
operator: Exists
effect: NoSchedule
긴 잡 전용 scale set을 따로 깔았다. 즉 같은 organization에 두 개의 scale set을 운영한다. 워크플로우에서 runs-on: arc-ondemand / runs-on: arc-spot으로 골라 쓰면 된다. 이 분리만 해줘도 빌드 실패율이 체감상 1/5 수준으로 떨어졌다.
캐시가 매번 사라진다
이게 가장 짜증난 구간이었다. ephemeral 러너는 잡이 끝나면 pod 자체가 삭제된다. ~/.gradle/caches도, node_modules도 같이 사라진다. EC2 self-hosted 시절보다 빌드 시간이 길어지는 결과로 이어졌다.
예를 들어, 해결책 후보는 셋이었다.
- PVC로 ReadWriteMany 볼륨 공유 (EFS) — 동시성 충돌과 EFS 자체 비용 우려
- S3 기반 외부 캐시 (
actions/cache@v4그대로 사용) — GitHub 측에서 제공 - self-hosted S3 캐시 서버 (
runs-on/cache등) — 별도 운영 부담
2번을 골랐다. actions/cache@v4는 self-hosted runner에서도 정상 동작하고, GitHub 측 캐시 스토리지를 추가 비용 없이 쓴다. 다만 organization당 10GB라는 제약이 있어서, 큰 도커 이미지 캐시는 ECR로 분리했다 (출처: GitHub Actions cache 공식 문서, 2026년 5월 확인). Gradle, npm, pip 같은 의존성 캐시는 actions/cache@v4로, 도커 레이어는 docker/build-push-action의 cache-from/cache-to로 ECR을 가리키는 구성이다.
메트릭이 안 보인다
ARC controller는 Prometheus 메트릭을 노출한다. helm 설치 시 metrics.controllerManagerAddr 옵션으로 활성화하면 큐 대기 시간, 활성 러너 수, scale-up 지연 같은 값을 긁어올 수 있다. Grafana 대시보드는 GitHub 공식 저장소의 샘플 JSON을 그대로 import해 쓰면 된다. 별도로 만들 필요는 없었다. 이건 짧다. 켜기만 하면 된다.
3개월 청구서 — EC2 대비 얼마나 줄었나
3개월 운영 후 청구서 기준이다. 월 빌드 시간이 약 3,500 CI minutes 정도 되는 환경이다(백엔드 1팀, 프론트 1팀, 통합 테스트 포함).
| 구성 | 월 컴퓨트 비용 | 관리 포인트 | 평균 큐 대기 |
|---|---|---|---|
| EC2 self-hosted (m5.large × 3, 24/7) | 약 $260 | EC2 패치, runner 등록 갱신 | 4~8분 (피크) |
| GitHub-hosted only (Linux 2 vCPU) | 약 $280 | 거의 없음 | 0~1분 |
| ARC on EKS (spot 70% + on-demand 30%) | 약 $95~120 | 클러스터 + ARC 차트 | 1~3분 |
따라서, 수치는 us-east-1, 작성 시점(2026년 5월) AWS 정가표 기준 추정치다. 실제 청구액은 reserved instance 적용 여부, EBS, NAT gateway, 데이터 전송량에 따라 달라진다.
그런데, EKS 컨트롤 플레인 비용($73/월)은 이미 다른 워크로드와 공유 중이라 회계상 분리하지 않았다. ARC 전용으로 클러스터를 새로 띄운다면 그 $73을 그대로 더해야 한다. 이게 의외로 손익분기점을 흔드는 항목이다. 빌드 시간이 적은 팀이라면 컨트롤 플레인 비용만으로 GitHub-hosted를 못 이기는 경우도 있다.
이처럼, 체감 측면에서 의외였던 건 큐 대기 시간이다. 1~3분이면 EC2 시절보다 훨씬 좋고, GitHub-hosted만큼 즉시는 아니지만 실무에서 거의 차이를 못 느끼는 수준이다. spot 비중을 더 올리면 비용은 더 떨어지겠지만 큐 대기는 늘어나는 트레이드오프가 있다.
언제 쓰고 언제 안 쓸 것인가
즉, 3개월 굴려본 결과 판단 기준은 이렇다.
한편, ARC가 맞는 경우
- 이미 운영 중인 EKS/GKE 클러스터가 있고, 추가 워크로드를 얹을 여유가 있다
- 월 GitHub Actions 빌드 시간이 3,000분을 넘긴다 (그 이하면 GitHub-hosted가 거의 항상 더 싸고 편하다)
- VPC 내부 리소스 접근이 필요한 잡이 정기적으로 돈다
- spot 인스턴스를 다뤄본 사람이 팀에 있다
ARC가 맞지 않는 경우
- 클러스터가 없고 ARC만을 위해 새로 띄워야 한다 — 컨트롤 플레인 비용에 운영 부담까지 합치면 손익분기가 안 맞는다
- 빌드가 가끔만 돈다 — GitHub-hosted 무료분(public repo는 무제한, private은 플랜별)으로 충분하면 그걸 쓰는 게 정답이다
- 팀에 쿠버네티스 트러블슈팅 가능한 사람이 한 명도 없다 — 빌드가 멈추면 빌드 큐가 아니라 클러스터를 디버깅해야 한다
신규 도입이라면 처음부터 gha-runner-scale-set(scale set 모드)으로 가라. Legacy ARC 문서 보고 시작하면 한 번 더 갈아엎게 된다. 캐시는 처음부터 actions/cache@v4 + ECR로 잡고, 짧은 잡과 긴 잡을 별도 scale set으로 분리해라. 이 세 가지만 미리 잡아도 내가 날린 시간을 통째로 아낄 수 있다.
그러나, 현재 운영 환경은 이 구성으로 굳어져 4개월째 그대로 돌고 있다. 다음 분기에는 캐시 적중률을 더 끌어올리려고 self-hosted S3 캐시 도입을 보고 있다.
관련 글
- ArgoCD로 Kubernetes GitOps 배포 자동화 — Helm 연동부터 롤백까지 실전 구성 – kubectl apply 스크립트를 ArgoCD GitOps로 전환하면서 겪은 일들을 정리했다. Helm Chart 연동, 자동 동기화 설…
- Trivy로 컨테이너 이미지 취약점 스캔을 CI/CD에 붙인 3개월 회고 – 프론트엔드에서 백엔드로 넘어오고 나서 처음 맡은 컨테이너 보안 스캔 자동화. Trivy를 GitHub Actions에 붙이면서 겪은 시행착…
- GitHub Actions CI/CD 구축기 — 테스트부터 배포 자동화까지 – 수동 배포 3개월 차에 터진 사고를 계기로 GitHub Actions CI/CD를 구축한 과정이다. YAML 문법 에러, 시크릿 설정 실수…