목차
- GitHub-hosted runner가 비싸지는 구간
- 자체 호스팅 러너를 꺼내야 하는 시점
- EC2에 러너 올리기 — 등록부터 첫 번째 장애까지
- idle 과금이라는 함정
- 운영하면서 터진 것들
- GitHub-hosted vs Self-hosted vs 서드파티 러너
"GitHub Actions는 무료니까 CI 비용 걱정 안 해도 된다"는 말을 꽤 자주 듣는다. 퍼블릭 레포에서 취미 프로젝트를 돌리는 수준이라면 맞는 말이다. 그런데 팀 단위로 프라이빗 레포를 운영하면서 GitHub Actions 자체 호스팅 러너 설정을 고민하지 않으면, 매달 수백 달러를 태우는 꼴이 된다.
월요일 오전 스탠드업에서 팀 리드가 "이번 달 Actions 비용 $380"이라고 읽었을 때, 백엔드 3명이 동시에 고개를 돌렸다. NestJS 모노레포 빌드가 점점 무거워지면서 프리 티어 분은 한참 전에 소진된 상태였다. 로컬 M1 Mac에서 3분이면 끝나는 빌드가 GitHub-hosted runner에서는 12분을 넘겼다. 그날부터 자체 호스팅 러너를 본격적으로 검토하기 시작했다.
GitHub-hosted runner가 비싸지는 구간
GitHub Actions 프리 티어는 월 2,000분(GitHub Free 기준)을 제공한다. Team 플랜이면 3,000분으로 늘어나지만, 이건 Linux 러너 기준이고 macOS 러너를 쓰면 분당 10배로 차감된다(출처: GitHub Docs — About billing for GitHub Actions, 2026년 4월 기준).
빌드 횟수보다 빌드 시간이 핵심이다. 모노레포에서 TypeScript 컴파일, 린트, 유닛 테스트, Docker 이미지 빌드까지 돌리면 한 번에 10분은 기본으로 잡아먹는다. 하루 PR이 15개쯤 올라오면 150분, 한 달이면 3,000분을 훌쩍 넘긴다.
GitHub-hosted runner 스펙은 2코어 CPU, 7GB RAM, 14GB SSD다. larger runner를 쓰면 4코어 16GB까지 올릴 수 있지만, 분당 단가가 $0.008에서 $0.016으로 두 배가 된다. CPU만 늘린다고 빌드가 선형으로 빨라지지 않는다. npm install이나 Docker layer 캐시처럼 I/O 바운드 작업은 코어 수와 무관하더라.
자체 호스팅 러너를 꺼내야 하는 시점
자체 호스팅 러너가 무조건 정답은 아니다. 세팅과 유지보수에 드는 시간을 시급으로 환산하면, 소규모 팀에서는 오히려 손해일 수 있다. 아래 3가지 중 2개 이상 해당되면 검토할 시점이다.
월 CI 비용이 $200을 넘을 때. EC2 c5.xlarge(4코어, 8GB) 온디맨드 가격이 us-east-1 기준 시간당 약 $0.17이다. 스팟 인스턴스를 쓰면 $0.05~$0.07까지 내려간다. 월 $200 미만이면 관리 비용 대비 이득이 크지 않다.
VPC 내부 리소스에 접근해야 할 때. 우리 팀의 결정적 이유가 이거였다. GitHub-hosted runner에서 프라이빗 서브넷의 RDS에 붙으려면 퍼블릭 엔드포인트를 열거나 VPN을 구성해야 한다. 보안팀이 퍼블릭 엔드포인트를 거부했고, VPN 세팅은 그 자체가 또 다른 시행착오의 시작이었다. EC2 self-hosted runner는 같은 VPC 안에 있으니 RDS, ElastiCache, 내부 API에 바로 접근 가능하다.
빌드 캐시가 병목일 때. GitHub-hosted runner는 매 빌드마다 새 VM을 띄운다. actions/cache로 캐시를 네트워크를 통해 올리고 내리는 시간이 매번 추가된다. 모노레포에서 node_modules 캐시가 1.5GB면 복원에만 2~3분 걸리더라. Self-hosted runner는 로컬 디스크에 캐시가 남아 있어서 이 시간이 거의 0이다.
EC2에 러너 올리기 — 등록부터 첫 번째 장애까지
GitHub Actions 자체 호스팅 러너 설정 과정 자체는 놀라울 정도로 간단하다. 레포 Settings → Actions → Runners → New self-hosted runner에서 OS별 스크립트를 복사해 EC2에서 실행하면 된다.
# runner 다운로드 및 설치 (Linux x64, 2026년 3월 기준)
# 최신 버전은 https://github.com/actions/runner/releases 에서 확인
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.321.0.tar.gz -L \
https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-linux-x64-2.321.0.tar.gz
tar xzf ./actions-runner-linux-x64-2.321.0.tar.gz
# 토큰은 GitHub Settings에서 발급, 1시간 후 만료되니 즉시 실행
./config.sh --url https://github.com/your-org/your-repo \
--token AXXXXXXXXXXXXXXXXXXXXXXX \
--labels ec2,linux,x64 \
--name prod-runner-01
# systemd 서비스 등록 — EC2 재부팅 후에도 자동 시작
sudo ./svc.sh install
sudo ./svc.sh start
여기까지 10분이면 끝난다. 진짜 문제는 그 다음이다.
수요일 오후, 코드리뷰를 마치고 PR을 머지했는데 워크플로우가 queued 상태에서 멈춰 있었다. runner 상태를 확인해보니 Offline. SSH로 들어가 봤더니 프로세스 자체가 죽어 있었다. journalctl -u actions.runner 로그를 뒤져보니 GitHub API와의 long poll 연결이 끊긴 후 재연결에 실패한 흔적이 남아 있었다. 원인은 EC2 보안 그룹이었다. 아웃바운드 443을 특정 IP 대역으로 제한해뒀는데, GitHub Actions는 github.com, api.github.com 외에도 *.actions.githubusercontent.com 등 여러 도메인에 접근해야 한다(출처: GitHub Docs — Self-hosted runners network requirements). 아웃바운드 HTTPS를 전체 허용으로 바꾸니 해결됐는데, 이걸로 3시간을 헤맸다.
워크플로우에서 러너를 지정하는 건 간단하다.
# .github/workflows/ci.yml
jobs:
build:
runs-on: [self-hosted, linux, x64] # 라벨 조합으로 러너 선택
steps:
- uses: actions/checkout@v4
- run: npm ci # 로컬 캐시 덕에 체감 30초 이내
- run: npm test
runs-on: ubuntu-latest 대신 runs-on: [self-hosted, linux, x64]로 바꾸면 된다. 라벨을 세분화해서 GPU 전용 러너를 분리하거나, 특정 워크플로우만 self-hosted로 돌리는 하이브리드 구성이 가능하다.
idle 과금이라는 함정
EC2에 runner를 올려놓고 가장 뼈아팠던 깨달음이 있다. 빌드가 안 돌아도 인스턴스는 돌아가고 있다는 것이다. c5.xlarge를 24시간 켜두면 하루 $4.08, 한 달이면 $122다. 실제 빌드 시간이 하루 총 2시간이라면 나머지 22시간은 순수 낭비다.
해결 방법은 크게 두 갈래로 나뉜다.
GitHub 공식 솔루션인 actions-runner-controller(ARC)는 Kubernetes 기반이다. 러너를 Pod로 띄우고 HPA로 스케일링하는 구조인데, 2023년 4월에 GitHub이 공식 인수하여 actions/actions-runner-controller 레포에서 관리되고 있다(출처: GitHub Blog — Changelog 2023-04-04). 기능은 강력하지만 EKS가 전제 조건이다. EKS를 안 쓰는 팀이 이걸 위해 EKS를 세우면 배보다 배꼽이 커진다.
우리는 Lambda + EventBridge 조합을 택했다. GitHub webhook으로 workflow_job 이벤트가 들어오면 Lambda가 EC2를 start하고, 빌드가 끝나면 10분 대기 후 stop한다. 종료(terminate)가 아니라 중지(stop)라서 AMI를 매번 새로 만들 필요가 없다. 다음 빌드 때 start만 호출하면 되고, start까지 체감 40~60초 정도 걸린다. 기존에도 큐 대기가 30초쯤 있었으니 실사용에서 차이를 거의 못 느꼈다.
이 구조를 적용하자 EC2 하루 가동 시간이 22시간에서 3시간으로 줄었다. 스팟 인스턴스까지 붙이니 월 비용이 $30~$40 수준이 됐다. GitHub Actions 과금분 $380 대비 90% 가까운 절감이다.
운영하면서 터진 것들
runner를 2주쯤 운영하니까 빌드가 갑자기 No space left on device로 실패했다. EC2 루트 볼륨을 기본 8GB로 잡아둔 게 원인이었다. Docker 이미지를 반복 빌드하면 dangling 이미지와 레이어 캐시가 쌓인다. docker system df로 확인했더니 7.2GB를 차지하고 있었다. 루트 볼륨을 30GB로 늘리고, cron으로 매일 새벽 3시에 docker system prune -af --filter "until=72h"를 실행하는 것으로 해결했다. --filter 없이 전부 날리면 다음 빌드에서 모든 레이어를 처음부터 받아야 하니, 3일 이상 된 것만 정리하는 게 낫다. CloudWatch에서 볼륨 사용률 80% 알람도 걸어뒀더니 이후로는 재발하지 않았다.
보안 쪽은 한 가지만 짚는다. Self-hosted runner는 빌드마다 환경이 초기화되지 않기 때문에 이전 빌드의 파일이나 환경 변수가 남아 있을 수 있다. GitHub 공식 문서에서도 퍼블릭 레포에는 self-hosted runner를 쓰지 말라고 권고한다(출처: GitHub Docs — Self-hosted runner security). 프라이빗 레포에서도 runner 전용 IAM 역할에 최소 권한만 부여하고, 프로세스를 root가 아닌 github-runner 같은 전용 사용자로 실행해야 한다. 완전한 격리가 필요하면 --ephemeral 플래그로 등록하면 되는데, 이 모드는 작업 하나를 수행한 후 runner가 자동으로 등록 해제된다. 캐시 이점이 사라지는 트레이드오프가 있고, S3에 캐시를 올리는 방식으로 보완할 수 있다고 하는데 아직 직접 해보지는 않았다.
GitHub-hosted vs Self-hosted vs 서드파티 러너
세 가지 선택지를 나란히 놓고 본다.
GitHub-hosted runner의 장점은 관리 부담이 제로라는 것이다. YAML에 runs-on: ubuntu-latest 한 줄이면 끝이고, 보안 패치와 디스크와 네트워크 전부 GitHub이 알아서 해준다. 스펙이 2코어 7GB로 고정적이고, VPC 내부 접근이 불가능하며, 캐시를 매번 네트워크로 주고받아야 한다는 게 약점이다. 월 CI 비용 $200 미만이면서 VPC 접근이 필요 없는 팀에게는 이게 가장 합리적인 선택이다.
EC2 Self-hosted runner는 커스터마이징 자유도가 높다. GPU를 붙일 수도 있고, 인스턴스 타입을 빌드 특성에 맞게 고를 수 있다. VPC 내부 접근이 되고, 로컬 캐시 덕에 빌드가 빠르다. 초기 세팅에 반나절, 안정화까지 1~2주 걸리고, 디스크 관리와 OS 패치를 직접 해야 한다는 부담이 있다. runner 바이너리 자체는 GitHub이 자동으로 업데이트하지만, 그 외 인프라는 전부 본인 몫이다.
BuildJet 같은 서드파티 러너 서비스도 존재한다. runs-on: buildjet-4vcpu-ubuntu-2204처럼 지정하면 되고, 가격이 GitHub-hosted보다 저렴하면서 스펙 선택지가 넓다. 다만 서드파티에 빌드 환경을 맡기는 보안 부담이 있고, VPC 접근 문제는 여전히 풀리지 않는다.
우리 팀은 하이브리드로 갔다. 린트, 타입 체크 같은 가벼운 작업은 GitHub-hosted runner에서, 테스트와 Docker 빌드처럼 무겁거나 VPC 접근이 필요한 작업은 EC2 self-hosted runner에서 처리한다. 워크플로우 파일을 분리하는 게 약간 번거롭지만, 비용 대비 효과는 확실했다. 개인적으로는 EKS를 이미 운영하는 팀이라면 actions-runner-controller가 깔끔하고, 그렇지 않다면 EC2 + Lambda auto-scaling 조합이 가장 현실적인 GitHub Actions 자체 호스팅 러너 설정이라고 본다.