시스템

[GitHub Actions] 온디맨드 Self-Hosted Runner 구축 방안 ( 필요할때만 컴퓨팅 자원 사용)

달빛궁전- 2026. 7. 26. 23:48

배경 : GitHub Actions의 Self-Hosted Runner를 상시 또는 고정 스케줄(예: 07시 ~ 24시)로 가동하면, 실제 빌드할때만 사용이 되어, 나머지 유휴 시간의 비용이 계속 발생합니다.
물론 낮은사양의 EC2를 켜두거나, AWS EventBridge Scheduler를 사용하여 특정시간에만 동작하는것도 방법입니다. (지난번 포스팅 참조)

이번에는 아예 필요할때만 동작하는 방안에 대해 작성하였습니다.

목적 : 빌드가 있을 때만 러너 머신을 자동으로 기동하고, 잡이 끝나면 스스로 종료되도록 구성하여 유휴 비용을 제거합니다.
본 문서에서는 AWS EC2, GCP GCE, On-Premise 각각의 구축 방안을 정리합니다.

 

1. 개요

온디맨드 방식은 어떤 플랫폼이든 아래 세 가지 원리로 동작합니다.

  1. 켜기 : 꺼진 러너는 자신을 켤 수 없으므로, 러너 외부의 주체가 머신을 기동합니다. 클라우드는 GitHub 호스티드 러너(ubuntu-latest)에서 클라우드 API를 호출하고, On-Premise는 상시 가동 중인 저전력 장비가 Wake-on-LAN을 전송합니다.
  2. 큐잉 : runs-on: self-hosted 잡은 러너가 GitHub에 온라인 등록될 때까지 자동으로 큐에서 대기합니다(기본 최대 24시간). 기동 타이밍이 정밀하지 않아도 잡이 유실되지 않으므로, 이것이 온디맨드가 성립하는 핵심 전제입니다.
  3. 끄기 : 머신 내부에서 유휴 상태를 감시하다가 일정 시간(예: 15분)이 지나면 스스로 종료합니다. 외부 스케줄러로 끄면 잡 실행 도중 종료될 위험이 있으므로, 잡 프로세스 유무를 직접 확인할 수 있는 머신 내부에서 판단하는 것이 안전합니다.

 

2. 사전준비 (플랫폼 공통)

러너 머신에는 아래 두 가지를 공통으로 구성합니다.

첫째, 러너를 등록(config.sh)한 뒤 서비스로 설치합니다. 

svc.sh로 설치하면 systemd 서비스로 등록되어 부팅 시 자동으로 GitHub에 온라인 등록됩니다.

cd actions-runner
./config.sh --url https://github.com/ORG/REPO --token <등록토큰>
sudo ./svc.sh install
sudo ./svc.sh start
sudo ./svc.sh status    # 상태 확인

 

둘째, 유휴 감시 스크립트를 크론(5분 주기)으로 등록합니다. 잡 실행 중에는 Runner.Worker 프로세스가 존재한다는 점을 이용합니다.

#!/bin/bash
# /usr/local/bin/runner-idle-check.sh
BUSY_FILE=/var/run/runner-last-busy
if pgrep -f 'Runner.Worker' >/dev/null 2>&1; then
  date +%s > "$BUSY_FILE"; exit 0
fi
[ ! -f "$BUSY_FILE" ] && { date +%s > "$BUSY_FILE"; exit 0; }
NOW=$(date +%s); LAST=$(cat "$BUSY_FILE")
UPTIME=$(awk '{print int($1)}' /proc/uptime)
if [ "$UPTIME" -gt 900 ] && [ $((NOW - LAST)) -gt 900 ]; then
  shutdown -h now      # On-Premise는 systemctl suspend
fi

 

유의사항 : 부팅 경과 시간(UPTIME) 조건을 함께 확인하는 이유는, 기동 직후 "유휴 15분 경과"로 오판하여 바로 꺼지는 현상을 막기 위함입니다.

워크플로 측에는 깨우는 잡을 재사용 워크플로로 만들어, 모든 셀프호스티드 워크플로의 첫 잡으로 연결합니다.

jobs:
  start-runner:
    uses: ./.github/workflows/runner-start.yml
    secrets: inherit
    permissions:
      id-token: write
      contents: read
  build:
    needs: start-runner
    runs-on: self-hosted
    steps: ...

 

3. AWS EC2 구성

워크플로 첫 잡이 OIDC로 IAM 롤을 assume하여 EC2를 기동하고, 본 잡은 러너 등록까지 큐에서 대기한 뒤 실행되는 구조입니다.

IAM에 GitHub OIDC 자격 증명 공급자를 등록합니다.

공급자 URL : https://token.actions.githubusercontent.com
Audience   : sts.amazonaws.com

IAM 롤을 생성합니다. 신뢰 정책에는 aud 조건(StringEquals)과 sub 조건이 모두 필요하며, 권한은 ec2:StartInstances, ec2:DescribeInstances로 최소화합니다.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::<ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com"
    },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
      "StringLike":   { "token.actions.githubusercontent.com:sub": "repo:ORG/REPO:*" }
    }
  }]
}

깨우는 워크플로(runner-start.yml)를 아래와 같이 작성합니다. configure-aws-credentials는 v6이 현재 권장 버전입니다.

on:
  workflow_call:
permissions:
  id-token: write
  contents: read
env:
  INSTANCE_ID: i-xxxxxxxxxxxxxxxxx
  AWS_REGION: ap-northeast-2
jobs:
  start:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}
      - run: |
          STATE=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" \
            --query 'Reservations[0].Instances[0].State.Name' --output text)
          echo "현재 상태: $STATE"
          [ "$STATE" = "running" ] && exit 0
          [ "$STATE" = "stopping" ] && aws ec2 wait instance-stopped --instance-ids "$INSTANCE_ID"
          aws ec2 start-instances --instance-ids "$INSTANCE_ID"
          aws ec2 wait instance-running --instance-ids "$INSTANCE_ID"

EC2 내부에 2장의 공통 구성(러너 서비스 + 유휴 감시 크론)을 설치합니다. Amazon Linux 2023은 cronie가 기본 미설치이므로 dnf install cronie를 먼저 수행합니다.

동작 특성 : 내부 shutdown 시 stopped 상태가 되어 컴퓨팅 과금이 중지되고 EBS 비용만 남습니다. 디스크가 유지되므로 러너 등록 정보, 캐시, 도커 이미지가 보존되며, 콜드 스타트는 30초~1분 수준입니다.

 

4. GCP GCE 구성

구조는 EC2와 동일하며, 인증(Workload Identity Federation)과 기동 명령이 GCP 방식으로 변경됩니다.

Workload Identity 풀과 GitHub OIDC 프로바이더를 생성합니다. attribute-condition으로 조직(또는 리포지토리)을 반드시 제한합니다.

gcloud iam workload-identity-pools create "github" \
  --project="${PROJECT_ID}" --location="global" \
  --display-name="GitHub Actions Pool"

gcloud iam workload-identity-pools providers create-oidc "my-repo" \
  --project="${PROJECT_ID}" --location="global" \
  --workload-identity-pool="github" \
  --issuer-uri="https://token.actions.githubusercontent.com" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
  --attribute-condition="assertion.repository_owner == '${GITHUB_ORG}'"

서비스 계정을 생성하고 인스턴스 기동 권한을 부여합니다.

gcloud iam service-accounts create runner-starter --project="${PROJECT_ID}"
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --member="serviceAccount:runner-starter@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/compute.instanceAdmin.v1"

유의사항 : 러너 VM에 서비스 계정이 연결되어 있다면, 기동 주체에게 해당 VM 서비스 계정에 대한 roles/iam.serviceAccountUser 권한도 필요합니다.
이 권한이 없으면 instances start가 권한 오류로 실패합니다.
풀의 principalSet이 서비스 계정을 가장(impersonation)할 수 있도록 바인딩합니다. 이 단계가 빠지면 auth 액션 인증이 실패합니다.

WIF_POOL_ID=$(gcloud iam workload-identity-pools describe "github" \
  --project="${PROJECT_ID}" --location="global" --format="value(name)")

gcloud iam service-accounts add-iam-policy-binding \
  "runner-starter@${PROJECT_ID}.iam.gserviceaccount.com" \
  --project="${PROJECT_ID}" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/${WIF_POOL_ID}/attribute.repository/ORG/REPO"

깨우는 워크플로는 아래와 같습니다. auth 액션은 v3이 현재 버전이며, 잡에 id-token: write 권한이 필요합니다.

jobs:
  start:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: google-github-actions/auth@v3
        with:
          workload_identity_provider: projects/<PROJECT_NUM>/locations/global/workloadIdentityPools/github/providers/my-repo
          service_account: runner-starter@<PROJECT_ID>.iam.gserviceaccount.com
      - uses: google-github-actions/setup-gcloud@v3
      - run: gcloud compute instances start RUNNER_VM --zone=asia-northeast3-a

GCE 내부 구성은 EC2와 동일합니다(러너 서비스 + 유휴 감시 크론).



동작 특성 : GCE는 게스트 내부 shutdown 시 TERMINATED 상태가 되며, 이 상태에서는 CPU/메모리 과금이 없고 디스크와 외부 IP 등 연결 리소스만 과금됩니다. gcloud compute instances start로 재기동이 가능합니다.

 

5. On-Premise 구성

클라우드 API가 없으므로 꺼진 서버를 깨울 주체가 필요합니다. 상시 가동 중인 저전력 장비(라즈베리파이, NAS )가 깨우기를 전담하고, 빌드 서버는 평소 절전 상태로 두는 2계층 구성을 사용합니다.빌드 서버의 BIOS와 NIC에서 Wake-on-LAN을 활성화합니다.
ethtool 설정은 재부팅 시 초기화되므로 systemd 유닛 등으로 부팅 시마다 적용합니다.

sudo ethtool -s eth0 wol g

소형 에이전트에 러너를 wake-agent 라벨로 등록하고, 깨우기 잡만 전담시킵니다.

jobs:
  wake:
    runs-on: [self-hosted, wake-agent]
    steps:
      - run: wakeonlan AA:BB:CC:DD:EE:FF
  build:
    needs: wake
    runs-on: [self-hosted, build]

빌드 서버에 러너를 build 라벨로 등록하고, 공통 구성을 설치합니다. 종료 명령은 systemctl suspend로 변경합니다.
suspend 상태가 WoL 재기동이 가장 확실하며, poweroff에서의 WoL 동작은 메인보드에 따라 다르기 때문입니다.

유의사항 : 서버 유휴 전력이 미미한 경우(미니PC 등)라면 온디맨드 구성 없이 러너를 상시 두는 편이 단순합니다. 온디맨드가 유효한 경우는 고전력 워크스테이션이나 GPU 서버처럼 유휴 전기요금이 실질적인 경우입니다.

사실 저도 테스트때문에 해본거고, 온프레미스는 그냥 켜두는게 나은방향 같습니다.. 일단 WOL이 하드웨어를 좀 타기 때문에 ㅠ

 

6. 운영 시 유의사항

  • 기동과 종료의 레이스 : 깨우는 잡이 "이미 running"으로 판단하고 넘어간 직후 유휴 크론이 종료해 버리면, 셀프호스티드 잡이 큐에 계속 대기할 수 있습니다.
    잡이 오래 queued 상태라면 깨우는 워크플로를 수동 재실행하면 됩니다.
    start-instances는 이미 실행 중이어도 오류 없이 동작(멱등)하므로, 상태 확인 없이 항상 호출하는 방식으로 단순화해도 됩니다.
  • GitHub OIDC 토큰은 수명이 짧으므로(파생 자격증명 포함) 깨우는 잡은 기동 확인까지만 담당하고 길게 대기하지 않도록 합니다.
  • 러너 머신을 교체하면 워크플로에 기록한 인스턴스 ID(또는 MAC 주소)를 갱신해야 합니다.

 

8. 정리

구분 AWS EC2 GCP GCE On-Premise
깨우는 주체 GitHub 호스티드 잡 + OIDC IAM 롤 GitHub 호스티드 잡 + WIF 서비스 계정 상시 소형 에이전트 (경량 러너)
인증 configure-aws-credentials@v6 google-github-actions/auth@v3 불필요 (사내망)
기동 명령 aws ec2 start-instances gcloud compute instances start Wake-on-LAN 매직 패킷
종료 방법 내부 유휴 크론 -> shutdown 내부 유휴 크론 -> shutdown 내부 유휴 크론 -> suspend
정지 상태 stopped (EBS만 과금) TERMINATED (디스크/IP만 과금) 절전 (대기 전력)
콜드 스타트 약 30초~1분 약 30초~1분 수십 초
(suspend 복귀는 수 초)

 

AWS, GCP는 클라우드상에서 동작이기 때문에 인증차이만 있습니다.