엔지니어링 블로그

GitHub Self-hosted Runner 운영기: VM에서 ARC까지

NAS와 Kubernetes 기반 CI 러너 운영 구조 AI 생성 이미지이며 실제 운영 환경을 그대로 표현한 것은 아닙니다.

GitHub Actions 비용을 아끼겠다는 꽤 단순한 이유로 self-hosted runner를 시작했다. 그런데 막상 운영해 보니 비용만의 문제는 아니었다. 디스크는 계속 차고, 이미지 보안 업데이트는 빌드를 막고, 매 작업마다 의존성을 다시 받느라 시간이 새어 나갔다.

이 글은 Mac의 컨테이너에서 시작한 runner를 NAS의 Linux VM, 그리고 k3s 위의 GitHub Actions Runner Controller(ARC)로 옮기며 겪은 시행착오를 정리한 기록이다.

Mac에서 Linux VM으로

처음에는 Mac에서 AMD64 Linux 컨테이너를 띄워 runner를 실행했다. 가볍게 시작하기에는 좋았지만, CI 안에서 Docker를 써야 하는 순간 한계가 뚜렷해졌다. 컨테이너 안의 Docker를 어떻게 다룰지, 권한과 마운트를 어디까지 열어줄지, 빌드 환경을 얼마나 실제 Linux와 같게 유지할지가 계속 신경 쓰였다.

그래서 NAS에 Linux VM을 올리고 runner 세 대를 붙였다. 일단은 안정적으로 돌아갔지만, 곧 다른 문제가 나왔다. 이미지 정리를 workflow에 넣지 않은 탓에 Docker 이미지와 빌드 레이어가 쌓였고, 결국 디스크를 거의 다 써 버렸다.

여기서 배운 것은 단순하다. self-hosted runner에서는 디스크 정리도 CI의 일부다. 이후에는 이미지 정리와 디스크 사용량 점검을 넣고, 보안 점검 스크립트와 추가 용량도 함께 준비했다.

이미지 보안은 배포 직전에 처음 만나면 늦다

Docker 이미지와 Lambda 컨테이너 이미지를 AWS에 올릴 때, 갱신되지 않은 패키지의 보안 이슈 때문에 간헐적으로 빌드가 막히는 일도 있었다. 배포 시점에 처음 발견하면 원인을 찾고 베이스 이미지를 갱신하는 동안 CI가 멈춘다.

그래서 이미지 생성 과정에서 사용할 Trivy 바이너리를 명시하고, 로컬에서 먼저 검사하는 단계를 추가했다. Lambda 이미지 역시 한 번 만들고 끝내지 않고 주기적으로 검토해 갱신하도록 Codex automation에 넣었다. 보안 검사는 마지막 관문이라기보다, 이미지 수명 주기를 관리하는 일에 더 가깝다는 생각이 들었다.

실행형 runner에서 ARC로

VM 위에서 오래 실행되는 runner는 결국 용량과 상태를 계속 관리해야 한다. 그래서 GitHub ARC로 옮겼다. ARC는 GitHub Actions 작업마다 Kubernetes에서 임시 runner Pod를 만들고, 작업이 끝나면 폐기한다. 작업 흔적이 다음 빌드로 계속 누적되지 않아 용량 관리가 훨씬 단순해진다.

NAS의 Linux VM에는 경량 Kubernetes 배포판인 k3s를 설치했다. Docker로만 관리하는 방식도 가능하지만, Docker-in-Docker(DinD) 환경을 다른 워크로드와 분리하고 싶었다. 별도 VM에 k3s를 올리면 runner의 권한과 빌드 환경을 운영 서비스와 분리하기도 편하다.

다만 ARC가 모든 문제를 자동으로 해결해 주지는 않는다. runner 자체는 매번 깨끗해지지만, 의존성을 매번 새로 내려받으면 시간과 네트워크 비용이 커진다.

캐시와 사전 빌드로 오버헤드 줄이기

ARC로 옮긴 뒤에는 캐시를 분리했다. PVC(Persistent Volume Claim)에 Playwright 브라우저, Python 패키지, npm·pnpm 캐시를 두고 runner Pod에서 마운트해 재사용했다. runner에 필요한 toolchain도 이미지에 넣었다.

또 runner 이미지와 베이스 이미지를 CI마다 새로 만들지 않도록 정기 빌드로 전환했다. 패키지 업데이트와 dnf update를 각 작업에서 기다리지 않게 된 것이다. NAS VM에는 private registry도 두어, 자주 쓰는 이미지를 로컬에서 빠르게 가져오도록 했다.

정리하면 역할은 다음처럼 나뉜다.

  • ARC: 작업이 끝난 runner를 폐기해 실행 환경을 깨끗하게 유지한다.
  • PVC 캐시: 매 빌드마다 내려받던 큰 도구와 패키지를 재사용한다.
  • 사전 빌드 이미지와 로컬 registry: 업데이트·이미지 풀링 비용을 작업 경로에서 분리한다.

캐시를 넣을 때는 무조건 공유하기보다, 언어별 패키지 매니저와 lockfile에 맞춰 캐시 키를 설계해야 한다. 그래야 오래된 의존성이 새 빌드를 오염시키지 않는다.

ARM과 AMD64 사이의 현실적인 선택

AWS에서는 비용 때문에 Graviton(ARM) 인스턴스를 사용한다. 하지만 저전력 CPU에서 AMD64 이미지를 크로스 빌드하면 시간이 꽤 든다. 이미지 빌드까지 겹치면 CI의 이점이 줄어든다.

이 부분은 MacBook의 Docker Desktop Kubernetes 환경을 활용해 풀었다. 혼자 개발하는 환경에서는 배포할 때 MacBook이 꺼져 있을 가능성이 낮다. 그래서 이미지 배포처럼 무거운 빌드 작업은 Mac에서 처리하고, NAS의 x86 Linux VM은 일반 CI runner 운영에 집중하게 했다.

완벽한 자동화보다 현재의 병목을 가장 싼 곳에서 푸는 선택이다. 다만 ARM과 AMD64를 함께 다룬다면, 실제 배포 대상과 같은 아키텍처로 이미지를 검증하는 단계는 꼭 필요하다.

맺으며

self-hosted runner를 계속 운영한다면, 오래 살아 있는 runner 여러 대를 직접 돌리는 것보다 ARC가 더 나은 기본값이라고 생각한다. 작업이 끝난 환경을 버릴 수 있어서 상태와 용량을 다루기가 훨씬 쉽다.

그렇다고 ARC가 공짜로 편해지는 것은 아니다. PVC 캐시, runner 이미지, 베이스 이미지 갱신, private registry, 보안 검사처럼 직접 챙길 운영 항목이 생긴다. 팀 규모가 작고 CI 사용량이 크지 않다면 GitHub-hosted runner 비용을 그냥 지불하는 편이 더 싸고 편할 수도 있다.

결국 self-hosted runner는 컴퓨팅 비용을 절약하는 선택이면서, 동시에 운영 시간을 어디에 쓸지 결정하는 선택이다. 지금도 가끔은 “그냥 돈으로 해결하는 게 최선 아닌가?” 싶다. 그래도 이 과정을 거치며 빌드 환경이 어떻게 만들어지고, 어디서 느려지고, 무엇이 쌓이는지는 훨씬 잘 알게 됐다.

ps codex 로 글을 풀었습니다. https://blog.naver.com/kimjj81/224385608234

Join the Investigation

Get the latest updates on my projects and indie hacking journey directly in your inbox.

No spam. Unsubscribe anytime.