Post

GitHub Actions Self-Hosted Runner 구축하기

GitHub Actions Self-Hosted Runner 구축하기

logo

GitHub Actions Self-Hosted Runner 구축하기

1. 개요

GitHub Actions는 GitHub에서 제공하는 CI/CD 도구다. 개발 과정에서 반복적으로 수행되는 빌드, 테스트, 배포 작업을 자동화할 수 있다.

예를 들어, 새로운 코드를 push하거나 pull request를 생성하면 GitHub Actions가 이를 감지하여 자동으로 작업을 수행한다. 단, 이러한 이벤트 감지는 workflow 파일(.yml/.yaml)에 트리거 조건을 설정해야 동작한다.

GitHub Actions는 Public Repository의 경우 사용량 제한과 과금 없이 GitHub에서 제공하는 runner를 사용할 수 있다. 반면 Private Repository는 무료 사용 한도가 있으며, 이를 초과하면 요금이 청구된다.

아래는 2025년 7월 26일 기준으로 GitHub Docs에 명시된 요금제별 스토리지와 사용 가능한 시간이다.

참고 문서: GitHub Actions 요금 청구 정보

계획스토리지분(월)
GitHub Free500 MB2,000분
GitHub Pro1 GB3,000분
조직용 GitHub Free500 MB2,000분
GitHub Team2 GB3,000분
GitHub Enterprise Cloud50 GB50,000분

2. 월 사용량 초과 시 발생하는 비용 문제

사내에서는 GitHub Team 요금제를 사용하고 있으며, 이 요금제는 한 달에 3,000분 동안만 GitHub Actions runner를 무료로 사용할 수 있다.

우리 회사는 레거시 인프라(v1)와 현대 인프라(v2)가 공존한다. v2 인프라는 v1 대비 서버리스 아키텍처로 구성되어 고가용성과 자동 확장성을 제공한다.

기존 v1 인프라는 새로운 코드 반영 시 개발자가 직접 EC2 인스턴스에 접속하여 서버를 종료하고 새 코드를 배포하는 구조인데, 이는 휴먼 에러 위험과 수동 배포의 불편함이 발생한다.

이러한 번거로움을 해결하기 위해 v2 인프라에서는 GitHub Actions를 활용하여 빌드, 테스트, 배포를 자동화했다. 그러던 중 평소와 다름없이 PR을 올렸는데 GitHub Actions가 동작하지 않았는데, 그 원인은 3,000분이라는 제한 시간을 초과했기 때문이었다.

사용량 초과 화면

이 문제를 해결하려면 GitHub Enterprise Cloud로 업그레이드해야 하는데, 5배 이상의 비용 차이가 발생하므로 좋은 방법은 아닌 듯 했다.

  • GitHub Team: 사용자당 $4/월
  • GitHub Enterprise Cloud: 사용자당 $21/월

3. GitHub Actions runner 사용 방법

GitHub Actions를 사용하는 방법은 두 가지다.

  1. GitHub-hosted runner: GitHub에서 제공하는 runner 사용
  2. Self-hosted runner: 사용자가 직접 구성한 runner 사용

첫 번째 방법은 요금제별로 월 사용량이 제한되어 있으며, 앞서 2번 섹션에서 다룬 방법이다. 두 번째 방법인 Self-hosted runner는 사용자가 직접 구성한 runner를 GitHub Actions에 등록하는 방식이다. 단, runner가 항상 연결 상태를 유지해야 워크플로우가 동작한다.

Self-hosted runner 장단점

장점단점
커스텀 하드웨어/소프트웨어 환경 구성 가능인프라 직접 관리 필요 (보안, 패치, 자동화)
무료 사용량 무제한 (과금 없음)runner 머신 가동 비용 및 관리 부담
내부 리소스, 프라이빗 네트워크 접근 용이워크플로 보안 이슈 (정보 노출 우려)

Self-hosted runner는 GitHub Repository Settings에서 설치할 수 있고, 환경에 맞는 OS를 선택하면 순차적으로 실행해야하는 명령어들이 나열된다. 이 명령어들을 입력하면 손쉽게 설치가 가능하다.

Self-hosted runner 설치 화면

4. Self-hosted runner 1대만 사용했을 때의 문제점

4.1 로컬에 runner를 설치했을 때

처음에는 로컬에 self-hosted runner를 설치했는데, 몇 번 사용해보고 사용할 수 없음을 느꼈다.

  • 워크플로우를 실행하기 위해서는 runner가 실행 상태가 되어야 한다. 따라서 노트북이 항상 켜져 있어야 하는 상태이어야 한다.
  • 네트워크 환경에 따라 속도가 느려지거나 불안정해질 수 있다.
  • 무엇보다 job이 runner에 할당되고 실제로 처리되는 속도가 너무 느렸다.

내 노트북 사양은 다음과 같다.

  • Apple M3 Pro
  • 18GB RAM
  • macOS Sequoia

사양이 나쁘지 않은데도 워크플로우 job 처리 속도와 할당 대기 시간이 너무 느렸다.

4.2 클라우드 환경에 runner 설치했을 때

이번에는 로컬이 아닌 클라우드 환경(AWS EC2)에 runner를 설치해보았다.

인스턴스 스펙 (t3.medium):

  • vCPU: 2
  • RAM: 4GiB
  • Network: 최대 5Gbps

클라우드 환경에서 실행 중인 runner는 항상 실행 상태이므로 관리가 편했을 뿐만 아니라, job 할당 및 처리 속도도 로컬 환경보다 빨랐다.

하지만 또 다른 문제가 있었는데, 워크플로우에 여러 개의 job이 있거나, 여러 워크플로우가 동시에 실행되면 1개의 runner는 1개의 job만 처리할 수 있었다. 실행되지 못한 job들은 Queue에 쌓여 순차적으로 처리된다. 병렬로 여러 job을 동시에 처리하지 못하다보니 빌드/배포 속도가 여전히 느렸다.

4.1과 4.2를 정리해보자면:

  • self-hosted runner는 로컬 또는 클라우드 환경 어디든 설치할 수 있다.
  • runner 1개는 1개의 job만 처리한다.
  • 동시에 여러 job이 실행되면 runner 개수만큼 병렬 처리되고, 나머지는 Queue에서 대기한다.

1runner-queue

5. 멀티 러너를 사용하여 여러 개의 job을 동시에 처리하기

4번 문제를 해결하기 위해, 클라우드 환경에서는 기본적으로 인스턴스 1대만 사용하되 해당 인스턴스 내에서 여러 개의 GitHub Actions Runner를 실행하는 방식으로 접근했다. 이때 실행하는 러너의 기본 개수는 워크플로우에 정의된 Job의 개수와 동일하게 설정하였다. 이는 워크플로우 실행 시 모든 Job이 동시에 병렬 처리되기를 원했고, 그 이상은 필요하지 않다고 판단했기 때문이다.

아래는 t3.medium 인스턴스에서 3개의 러너가 실행되고, 모두 idle 상태일 때의 CPU(코어별) 및 메모리 사용량이다:

1
2
3
4
5
6
7
CPU 사용률:
  CPU 0: 0.0%
  CPU 1: 0.7%

메모리 사용량:
  사용 중: 584MB
  전체: 3.75GB

다음은 워크플로우가 실제로 동작 중일 때의 CPU 사용률 및 CPU 버스트 크레딧 사용량이다. 워크플로우 실행 시 CPU 크레딧이 사용되는 것으로 보아, 기본 제공되는 CPU 성능을 초과하여 부하가 발생함을 알 수 있다. (t3.medium 인스턴스는 기본적으로 vCPU 사용률 약 20% 정도의 성능을 제공하며, 이를 초과할 경우 버스트 크레딧을 소모하여 성능을 유지한다.)

credit credit

아래는 GitHub Actions 워크플로우가 트리거되었을 때, EC2 인스턴스에 설치된 러너들이 Job을 처리하는 흐름이다:

credit

또한, 인스턴스 리소스를 효율적으로 사용하기 위해 5분마다 동작하는 cron job을 설정하여 현재 CPU 및 메모리 사용량을 체크하고 상황에 따라 러너를 동적으로 추가하거나 종료하는 로직을 구현하였다:

  • 5분마다 cron job 실행
  • 현재 CPU 및 메모리 사용량을 확인
  • 리소스에 여유가 있을 경우 새로운 러너를 추가 (최대 5개까지 확장)
  • 리소스가 부족하거나 유휴 러너가 많은 경우 러너 종료

이와 같은 구조로 구현함으로써 여러 Job을 병렬로 처리할 수 있었고, 전체적인 빌드 및 배포 속도도 크게 개선되었다.

6. 비용 최적화

클라우드 환경에 Self-hosted runner를 구축했기에 당연히 인프라 비용이 발생한다. AWS에서는 온디맨드, 스팟, 예약 인스턴스(RI), 세이빙 플랜(SP)과 같은 다양한 옵션을 제공한다. 이 중 러너 실행 환경에 가장 적합한 방식은 스팟 인스턴스라고 판단하였다.

온디맨드 인스턴스는 시간당 비용이 고정되어 서비스 수가 늘어날수록 러너를 설치할 인스턴스도 증가하게 되어 비용이 기하급수적으로 증가할 것이다. 예약 인스턴스(RI)나 세이빙 플랜(SP)은 1년 또는 3년 단위의 장기 계약을 통해 요금을 절감할 수 있으나 단순히 워크플로우 실행을 위한 환경에 장기 계약을 맺는 것은 적절하지 않다고 판단했다.

이에 따라 선택한 스팟 인스턴스는 온디맨드와 동일하게 시간당 과금되지만 훨씬 높은 할인율을 제공하여 저렴하게 사용할 수 있다. 다만, 스팟 인스턴스는 AWS에서 언제든지 회수될 수 있다는 단점이 있으나, 단순 빌드 목적으로 사용되기 때문에 인스턴스가 회수되더라도 큰 문제가 되지 않을 것이다.

또한, 인스턴스 회수에 대비하기 위해 Auto Scaling Group(ASG)을 적용하였다. ASG의 min desired count를 1로 설정하여, 인스턴스가 회수되더라도 자동으로 새로운 스팟 인스턴스를 생성하도록 구성하였다. 이를 통해 안정적인 러너 운영이 가능하고 비용도 효과적으로 절감할 수 있다.

7. 이 모든 것을 자동화하기

EC2 인스턴스 및 GitHub에 Self-hosted runner를 생성하고 실행하는 과정을 UserData 스크립트를 통해 자동화하였다. 여러 서비스와 리전별로 runner를 생성해야 하는 상황에서 이를 수동으로 생성하는 것은 비효율적이므로 Pulumi IaC (Infrastructure as Code)를 사용하여 인프라를 코드로 관리하고 서로 다른 서비스라도 동일한 구조로 일관성 있게 배포될 수 있도록 하였다.

아래는 인프라 생성 시 자동으로 수행되는 과정이다.

graph TD

    %% 상단: AWS 자원
    ASG["Auto Scaling Group<br/>Desired Count: 1"]
    INSTANCE["EC2 Spot Instance<br/>t3.medium"]
    USERDATA["UserData Script<br/>자동 실행"]

    %% 중앙: UserData 단계 (2행 3열)
    STEP1["시스템 초기화<br/>Docker, Node.js 설치"]
    STEP2["GitHub Token 조회"]
    STEP3["3개 Runner 설치<br/>actions-runner-1,2,3"]
    STEP4["SystemD 서비스 등록"]
    STEP5["모니터링 설정<br/>5분마다 크론"]
    STEP6["서비스 시작"]

    %% 하단: GitHub
    REPO["GitHub Repository"]

    %% 상단 연결
    ASG --> INSTANCE --> USERDATA
    USERDATA --> STEP1
    USERDATA --> STEP2
    USERDATA --> STEP3
    STEP1 --> STEP4
    STEP2 --> STEP5
    STEP3 --> STEP6
    STEP4 --> REPO
    STEP5 --> REPO
    STEP6 --> REPO

    %% 스타일 정의
    classDef aws fill:#FF9900,stroke:#232F3E,stroke-width:2px,color:#fff
    classDef github fill:#181717,stroke:#333,stroke-width:2px,color:#fff
    classDef process fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#fff

    class ASG,INSTANCE,USERDATA aws
    class REPO github
    class STEP1,STEP2,STEP3,STEP4,STEP5,STEP6 process

8. 결론

8.1 완전 자동화

  • ASG가 스팟 인스턴스 자동 생성
  • UserData가 3개 Runner 자동 설치
  • 수동 개입 0%

8.2 비용 효율성

  • 스팟 인스턴스: On-Demand 대비 70% 절약
  • t3.medium: 2 vCPU, 4GB RAM으로 최적화
  • ASG: 필요시에만 확장

8.3 동적 스케일링

  • 기본 3개 Runner (워크플로우의 JOB 개수)
  • 5분마다 모니터링하여 자동 조절
  • 최대 5개까지 동적 확장

8.4 성능 향상

  • 단일 Runner: 1 job 동시 처리
  • 3개 Runner: 3 job 동시 처리 (300% 향상)

9. 아쉬웠던 점

9.1 행동으로 먼저 옮기지 않는 것

Self-hosted runner를 한 대만 실행해놓고 여러 Job을 병렬로 처리하지 못할 거라고 섣불리 판단했던 점이 아쉬움으로 남는다. 궁금한 점이나 불편한 상황이 생겼을 때, 검색부터 하기보다 직접 먼저 시도해봤다면 어땠을까 하는 생각이 든다. AI를 적극적으로 활용해야 한다는 생각과 작업을 빠르게 처리해야 한다는 압박감 때문에 자연스럽게 검색 먼저 하는 습관이 생긴 것 같다.

사내 동료분 중 한 분은 개발을 정말 잘하시는데, 그 이유 중 하나가 바로 ‘일단 해보는’ 태도에 있는 것 같다. 잘 되든 안 되든 먼저 행동으로 옮기시는 모습을 보면, 실패를 생각하지 않고 직접 부딪혀보는 습관이 실력으로 이어지는 듯하다.

This post is licensed under CC BY 4.0 by the author.