GitHub Actions 비용 절감 — 과금 구조부터 self-hosted runner 손익 계산까지

목차

GitHub Actions 청구서가 한 달 820달러를 찍은 뒤 GitHub Actions 비용 절감 작업에 들어갔다. 결과는 230달러대까지 내려왔지만, 그 과정에서 self-hosted runner가 만능이 아니라는 것도 같이 배웠다.

물론, 배경부터 짚으면, 다루는 환경은 Python+Node 모노레포다. PR마다 lint, type check, 단위 테스트, 통합 테스트, 도커 이미지 빌드까지 돈다. 처음엔 ubuntu-latest 하나로 충분했다. 인원이 늘면서 PR 수가 두 배로 뛰자 청구서도 같이 따라 올라갔다. 무료 한도 2000분은 진작에 넘긴 상태였다.

선택지는 셋이었다. (1) GitHub-hosted runner를 유지하되 워크플로 자체를 최적화한다. (2) self-hosted runner를 EC2에 올린다. (3) BuildJet, Namespace, Blacksmith 같은 third-party hosted runner로 갈아탄다. 셋 다 한 번씩 검토했고, 결과적으로 (1)을 기본으로 두고 (2)를 부분 도입했다.

청구서를 뜯어보면 보이는 것

GitHub Actions 과금은 크게 세 항목으로 갈린다. runner 사용 시간(분 단위), storage(아티팩트와 캐시), outbound data transfer다. 대부분 사람이 분 단위 요금만 본다. 그런데 의외로 storage 항목이 야금야금 깎아먹는 경우가 꽤 있다.

작성 시점(2026-05) 기준으로 Linux ubuntu-latest 2-core runner는 1분에 0.008달러다. Windows는 2배, macOS는 10배다. 유닛 자체로는 작아 보이지만, PR 한 번에 매트릭스 빌드 5개가 각 10분씩 돌면 한 PR에 4달러다. 하루 PR 30개면 120달러다. 한 달이면 3600달러로 뛴다. 무서운 건 이 숫자가 점진적으로 늘어난다는 것이다. 처음엔 PR이 적고 매트릭스도 단순해서 잘 못 느낀다.

storage는 아티팩트가 90일 동안 남는 게 기본값이다. CI 캐시도 7일 보관에 10GB 한도가 무료지만, 그 이상은 GB-day 과금이다. 큰 도커 빌드 캐시를 안 지우면 한 달에 50달러쯤 슬쩍 추가된다. 한 분기를 모르고 흘려보낸 항목이 바로 이 부분이었다.

또한, 자세한 단가는 공식 문서의 Per-minute rates 표를 참고하면 된다. 단, 이 페이지는 자주 갱신되니 작성 시점 기준이라는 점을 염두에 두면 된다.

후보 셋을 두고 잡은 비교 기준

결국, 기술 선택할 때 비교 기준을 먼저 정해놓지 않으면 후보별 장단점만 잔뜩 나열하다가 결정을 못 내린다. 이번엔 네 가지 축으로 좁혔다.

  • 분당 비용: 같은 워크로드를 돌렸을 때 청구서.
  • 운영 부담: 누가 OS 패치, 보안 업데이트, runner 버전 업그레이드를 떠안는가.
  • 빌드 속도: 캐시 hit ratio 포함한 실제 PR 평균 시간.
  • 보안 표면: secret 노출, 임의 코드 실행 위험, 네트워크 격리.

이 네 가지를 동등하게 두지 않았다. 인프라 인력이 5명짜리 작은 팀이라 운영 부담을 분당 비용보다 약간 더 무겁게 봤다. 이게 결과를 갈랐다. 보수적으로 가는 입장에선 "잘 되고 있는 GitHub-hosted를 굳이 왜 건드리나"라는 질문에 답할 수 있어야 움직이기 때문이다.

GitHub-hosted runner — 가격 외에 따져야 할 것

결국, GitHub-hosted runner의 강점은 명확하다. 셋업 0분, 격리된 깨끗한 VM, 매번 새 환경. PR 트리거되면 큐 대기 시간이 보통 5초 이내다. runner 인프라를 GitHub이 관리하니까 워크플로 파일만 신경 쓰면 된다.

문제는 비용이다. ubuntu-latest는 분당 0.008달러로 싸 보이지만, 빌드를 잘게 쪼개서 매트릭스로 돌리면 청구서가 빠르게 부푼다. 2-core 머신이라 큰 빌드는 느리고, 빠르게 하려고 larger runner(8-core, 16-core)를 쓰면 분당 단가가 4배~16배로 뛴다.

캐시만 잘 잡아도 30%는 빠진다

처음 한 일은 코드 변경 없이 청구서 자체를 줄이는 작업이었다. 가장 큰 효과를 본 건 의존성 캐시였다. actions/setup-pythonactions/setup-node 모두 cache 옵션을 켜면 lock 파일 hash 기반으로 의존성을 재사용한다. 우리 워크플로는 한동안 이걸 안 켜고 매번 새로 받았다. 켠 뒤 lint 작업 평균 시간이 2분 30초에서 1분 10초로 줄었다.

도커 빌드 쪽은 BuildKit의 cache export를 GHA cache에 저장하도록 바꿨다(type=gha). 이게 효과가 의외로 컸다. 변경 없는 base layer를 다시 안 굽기 때문에 PR 평균 도커 빌드 시간이 7분대에서 2분대로 떨어졌다.

특히, 이 두 가지만 했는데 한 달 청구서가 820달러에서 540달러로 내려왔다. 처음엔 self-hosted부터 검토했는데, 지나고 보면 캐시 정리가 우선순위 1번이었어야 했다.

매트릭스를 줄이는 것도 비용이다

그런데, Python 3.10/3.11/3.12 + Node 18/20 매트릭스를 모든 PR에서 다 돌릴 필요가 있을까. PR 단계에서는 가장 보수적인 조합 1개만 돌리고, main merge 후 nightly로 전체 매트릭스를 돌리도록 분리했다. 검증의 깊이는 살리되 빈도를 줄이는 방향이다. 이걸로 또 100달러쯤 더 깎였다.

self-hosted runner — 손익 분기점은 의외로 빨리 온다

캐시·매트릭스 정리가 끝나고 나서야 self-hosted를 검토했다. 핵심 질문은 "월 X달러부터 self-hosted가 이득인가"다. 손익 분기점을 계산할 때 사람들이 자주 빠뜨리는 게 운영 시간 비용이다. EC2 인스턴스 비용은 작지만, runner 끊겼을 때 대응하는 시간을 5명 팀의 시간당 비용으로 환산하면 만만치 않다.

거칠게 계산해봤다. c6i.xlarge spot instance가 시간당 0.07달러 수준(2026-05, ap-northeast-2 기준이며 spot 가격은 변동성이 있다). 24시간 돌리면 한 달 50달러다. 같은 사양의 GitHub-hosted larger runner는 4-core가 분당 0.016달러, 한 달 풀가동(43200분)이면 691달러다. 산술적으로는 14배 차이가 난다.

그런데 이 계산이 함정인 게, GitHub Actions 청구는 "실제 사용한 분"만 청구한다. self-hosted는 idle 시간도 EC2 비용이 나간다. 그래서 워크로드 패턴이 결정적이다. 거의 항상 큐에 작업이 차 있는 팀이라면 self-hosted가 압도적이고, 하루에 몇 번만 도는 팀이라면 GitHub-hosted가 낫다.

비교표 — 한 팀의 워크로드 기준 한 달 추정 비용

옵션 분당/시간당 단가 월 사용량 환산 운영 시간 비용 추정 합계
GitHub-hosted (캐시 최적화 전) 분당 $0.008 약 820달러 0달러 820달러
GitHub-hosted (캐시 최적화 후) 분당 $0.008 약 440달러 0달러 440달러
self-hosted EC2 spot (c6i.xlarge ×2) 시간당 $0.07 약 100달러 월 4시간 × 50달러 = 200달러 300달러
third-party (BuildJet 4-core 추정) 분당 $0.004 약 220달러 0달러 220달러

운영 시간 비용은 보수적으로 잡았다. 월 4시간이면 runner offline 알림 한 번, OS 패치 한 번, runner 버전 업그레이드 한 번 정도다.

spot instance를 쓸 때 진짜 신경 써야 할 것

self-hosted를 EC2 spot으로 굴리면 단가가 떨어지는 대신 termination 처리가 필요하다. spot이 회수될 때 2분 사전 통지가 오는데, 이때 runner를 graceful하게 deregister 안 하면 GitHub 쪽에 죽은 runner가 계속 남는다. 그러면 다음 작업이 죽은 runner에 배정되어 영영 안 돌고 큐가 쌓인다. 이 문제는 actions-runner-controller(ARC)를 쓰면 어느 정도 해결된다. ARC 자체는 또 Kubernetes를 깐 팀에서나 의미가 있다는 게 함정이다.

게다가, ARC 없이 EC2 user data로 lifecycle hook + deregister 스크립트를 돌리는 방식을 골랐다. K8s를 깔 만한 규모가 아니어서다. 셋업 자체는 반나절 정도, 안정화까지는 두 주쯤 걸렸다.

third-party hosted runner — 검토했지만 안 쓴 이유

특히, BuildJet, Namespace, Blacksmith 같은 서비스가 비슷한 약속을 한다. "GitHub-hosted보다 절반 가격, 두 배 속도." 분당 단가는 GitHub의 절반 수준이고, 캐시 인프라도 자체 SSD를 쓴다고 한다. 워크플로에서 runs-on: 한 줄만 바꾸면 마이그레이션이 끝난다는 점도 매력적이다.

또한, 다만 두 가지가 걸렸다. 하나는 보안 표면이다. private repo 코드를 third-party 인프라에서 빌드한다는 게 보안 정책 검토에서 막혔다. 둘째는 의존성이 또 하나 늘어난다는 점이다. GitHub-hosted를 self-hosted로 바꾸는 건 우리 인프라 안으로 들어오는 방향이지만, third-party는 또 다른 외부 벤더가 추가된다. 보수적인 입장에서는 "잘 되고 있는 GitHub Actions에 외부 벤더를 더 끼워넣을 이유가 있는가"라는 질문에 답하기가 어려웠다.

따라서, 가격만 보면 여전히 매력적이다. 같은 보수적 기준에 덜 까다로운 팀이라면 충분히 후보다.

한 팀의 최종 구성과 월 청구서

결과적으로 단일 옵션을 고르지 않고 섞었다. PR 단계의 가벼운 lint와 type check는 GitHub-hosted ubuntu-latest로 그대로 둔다. 캐시는 다 켜져 있다. 무거운 도커 빌드와 통합 테스트는 self-hosted runner 라벨(self-hosted, linux, x64, builder)로 보내서 EC2 spot 인스턴스 두 대가 처리한다. main 브랜치 nightly 매트릭스는 GitHub-hosted larger runner로 한 번에 끝낸다.

월 청구서는 GitHub Actions 쪽 230달러대, AWS EC2 spot 비용 60달러 정도로 안정화된 상태다. 합쳐도 이전 청구서의 35% 수준이다. 운영 부담은 처음 두 주 빼면 거의 없는 수준으로 떨어졌다(여담이지만 이 두 주가 가장 힘들었다).

물론, GitHub Actions usage 메트릭을 매주 확인하기로 했다. 새 워크플로가 추가되면서 다시 청구서가 슬쩍 오를 수 있어서다. 한 번 줄여놓고 끝나는 작업이 아니다.

어떤 상황에 self-hosted가 맞는가

그런데, 판단 기준을 실용적으로 정리한다.

특히, self-hosted로 가도 좋다:

  • 월 GitHub Actions 청구서가 500달러 이상이고, 그중 절반 이상이 큰 도커 빌드나 통합 테스트 같은 무거운 작업이다.
  • 큐에 작업이 거의 항상 차 있다(idle 시간이 적다).
  • 이미 AWS나 GCP 인프라를 운영 중이고, EC2 한두 대 추가 운영이 부담스럽지 않다.
  • 사내 네트워크에만 접근 가능한 리소스(내부 npm registry, private artifact server)에 빌드가 닿아야 한다.

결국, 그냥 GitHub-hosted를 유지해라:

  • 청구서가 월 200달러 이하다. 캐시·매트릭스 최적화부터 해라.
  • 인프라 팀이 따로 없거나, 5명 미만이라 새로 인프라를 떠안기 어렵다.
  • 워크로드가 띄엄띄엄 들어오고 idle 시간이 길다. EC2 spot의 단가 이점이 idle 비용에 잠식된다.

예를 들어, 오늘 당장 손댈 수 있는 것 세 가지만 꼽으면 이렇다. (1) actions/setup-*의 cache 옵션이 다 켜져 있는지 확인해라. (2) 도커 빌드에 BuildKit GHA cache(type=gha)를 적용해라. (3) PR 트리거 매트릭스를 가장 보수적인 1개로 줄이고 전체 매트릭스는 nightly로 분리해라. 이 세 가지만 해도 청구서가 30~50% 빠지는 경우가 흔하다. self-hosted 검토는 그 다음이다.

관련 글