Moment Note

GitHub Actions 셀프호스티드 러너 비용·보안 트레이드오프

CI/CD & DevOps ·

GitHub Actions를 쓰다 보면 두 갈림길을 만납니다. GitHub이 제공하는 호스티드 러너를 계속 쓸 것인가, 아니면 셀프호스티드 러너를 직접 운영할 것인가. 빌드가 느려서, 요금이 부담돼서, 혹은 특수 하드웨어(GPU·ARM·특정 네트워크)가 필요해서 셀프호스티드를 검토하게 되지만, 이 선택은 비용 절감과 운영·보안 부담의 트레이드오프입니다.

이 글에서는 언제 셀프호스티드가 실제로 이득인지, 셀프호스티드가 가진 보안 리스크와 그 완화책, 그리고 “직접 관리하는 상시 VM” 대신 요청 시마다 깨끗한 러너를 띄우는 오토스케일·일회성(ephemeral) 구성까지 단계적으로 정리합니다.

언제 셀프호스티드를 고려하나

셀프호스티드는 만능이 아닙니다. 다음 조건 중 하나라도 강하게 해당될 때 검토 가치가 있습니다.

  • 특수 하드웨어: GPU, 대용량 RAM, 특정 ARM 아키텍처, 사내망 리소스 접근이 필요할 때
  • 비용 규모: 빌드 시간이 월 수만 분에 달해 호스티드 분당 요금이 자체 인프라 상시 비용을 넘어설 때
  • 캐시·네트워크 지역성: 거대한 도커 레이어나 의존성을 로컬 캐시/미러로 붙여 매번 다운로드를 없애고 싶을 때

반대로 트래픽이 불규칙하고 팀이 작다면, 호스티드 러너의 “관리 제로” 이점이 대개 더 큽니다. 셀프호스티드는 러너 자체를 서비스처럼 운영해야 한다는 점을 잊으면 안 됩니다.

가장 큰 함정: 퍼블릭 레포 + 셀프호스티드

보안 관점에서 절대 하지 말아야 할 조합이 있습니다. 퍼블릭 저장소에 셀프호스티드 러너를 붙이는 것입니다. 외부 기여자가 포크에서 PR을 올리면 그 안의 워크플로 코드가 여러분의 러너 머신에서 실행될 수 있고, 이는 곧 사내 네트워크에서의 임의 코드 실행입니다.

# 위험: fork PR이 셀프호스티드에서 도는 트리거
on:
  pull_request_target:   # 시크릿 접근 + 베이스 컨텍스트 실행 = 특히 위험
    types: [opened, synchronize]
jobs:
  build:
    runs-on: self-hosted   # 퍼블릭 레포에서는 지뢰

GitHub 문서도 퍼블릭 레포에는 셀프호스티드를 권장하지 않습니다. 불가피하다면 최소한 포크 PR은 승인된 경우에만 실행되도록 설정하고, pull_request_target 사용을 피하며, 러너는 반드시 매 잡마다 폐기되는 일회성으로 격리해야 합니다.

일회성(ephemeral) 러너로 상태 오염 차단

상시 러너의 문제는 잡 사이에 상태가 남는다는 것입니다. 이전 잡이 남긴 파일·설치물·크리덴셜을 다음 잡이 물려받아, 악의적 코드가 다른 워크플로의 산출물을 훔치거나 캐시를 오염시킬 수 있습니다. 해법은 --ephemeral 플래그로 잡 하나 처리 후 자동 등록 해제되고 폐기되는 러너입니다.

# 러너를 일회성으로 등록(한 잡 수행 후 자동 종료)
./config.sh --url https://github.com/ORG/REPO \
  --token "$REG_TOKEN" \
  --ephemeral \
  --labels self-hosted,linux,x64,ephemeral

./run.sh   # 잡 1건 처리 후 스스로 등록 해제

일회성 러너는 컨테이너나 VM 이미지로 감싸 “매번 깨끗한 환경에서 시작”하게 만드는 것이 이상적입니다. 잡이 끝나면 인스턴스 자체를 파기하므로 상태 오염과 크리덴셜 잔존을 원천 차단합니다.

오토스케일: 필요할 때만 띄우기

상시 VM을 켜 두면 유휴 시간에도 비용이 나갑니다. 큐에 잡이 들어올 때 러너를 띄우고 끝나면 없애는 오토스케일이 비용·보안 양쪽에서 유리합니다. 쿠버네티스라면 GitHub이 제공하는 Actions Runner Controller(ARC)가 표준적 방법입니다.

# ARC: 스케일 셋 정의(요약)
apiVersion: actions.github.com/v1alpha1
kind: AutoscalingRunnerSet
metadata:
  name: linux-runners
spec:
  githubConfigUrl: https://github.com/ORG
  githubConfigSecret: gh-app-secret
  minRunners: 0        # 유휴 시 0개 -> 비용 절감
  maxRunners: 20
  template:
    spec:
      containers:
        - name: runner
          image: ghcr.io/actions/actions-runner:latest

minRunners: 0은 잡이 없을 때 러너를 0개로 줄여 유휴 비용을 없앱니다. 다만 콜드 스타트로 첫 잡의 대기 시간이 늘 수 있으니, 상시 트래픽이 있다면 최소치를 몇 개 유지해 균형을 잡습니다.

보안 하드닝 체크리스트

셀프호스티드를 운영한다면 다음을 기본으로 갖춰야 합니다. 이는 선택이 아니라 최소 요건입니다.

  • 인증에 PAT 대신 GitHub App: 짧은 수명 토큰, 세밀한 권한, 회수 용이
  • 러너 네트워크 격리: 사내 민감 시스템과 분리된 서브넷, 아웃바운드 최소 허용
  • 액션 버전 고정: 서드파티 액션은 태그가 아닌 커밋 SHA로 핀 고정(공급망 공격 방어)
  • 최소 권한 토큰: GITHUB_TOKEN 기본 권한을 read로 낮추고 필요 시에만 상향
# 워크플로 기본 권한 최소화 + 액션 SHA 핀
permissions:
  contents: read      # 기본을 읽기로

jobs:
  build:
    runs-on: [self-hosted, ephemeral]
    steps:
      # 태그 대신 커밋 SHA로 고정
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

또한 러너 호스트에는 워크플로가 필요 이상의 권한(예: 프로덕션 배포 크리덴셜)을 갖지 않도록 하고, 배포 같은 고권한 작업은 별도의 격리된 러너 풀로 분리하는 것이 안전합니다.

비용은 정말 절감되는가

셀프호스티드가 항상 싸다는 것은 착각입니다. 총소유비용(TCO)에는 인스턴스 요금뿐 아니라 운영 인력의 시간, 패치·모니터링·장애 대응, 오토스케일 인프라(ARC용 쿠버네티스) 유지 비용이 포함됩니다.

  • 호스티드가 유리한 경우: 불규칙·저볼륨 트래픽, 작은 팀, 특수 하드웨어 불필요
  • 셀프호스티드가 유리한 경우: 고볼륨 상시 빌드, GPU/ARM 필요, 강한 캐시 지역성, 운영을 감당할 인력 존재

실측 없이 결정하지 마세요. 현재 워크플로별 빌드 시간과 월간 분 사용량을 뽑아 호스티드 요금을 계산하고, 셀프호스티드의 인스턴스·운영 비용 추정치와 나란히 비교한 뒤 결정하는 것이 정석입니다.

마무리

셀프호스티드 러너는 특수 하드웨어와 고볼륨 빌드에서 분명한 이점을 주지만, 그 대가로 보안과 운영의 책임이 통째로 넘어옵니다. 퍼블릭 레포에는 붙이지 말고, 붙인다면 반드시 일회성 러너로 격리하며, 오토스케일로 유휴 비용을 없애고, GitHub App 인증·최소 권한 토큰·액션 SHA 핀·네트워크 격리를 기본으로 갖추세요. 무엇보다 “싸 보인다”가 아니라 실측한 사용량과 운영 인력을 포함한 총비용으로 판단해야 합니다. 트래픽이 불규칙하고 팀이 작다면 호스티드 러너의 관리 제로가 대개 더 나은 선택입니다.