Skip to content
Published on

GitHub Actions 고급 CI/CD — Matrix, Cache, Self-hosted Runner

Authors
  • Name
    Twitter
GitHub Actions Advanced CI/CD

들어가며

GitHub Actions는 이제 CI/CD의 사실상 표준입니다. 기본적인 빌드/테스트 파이프라인은 누구나 만들 수 있지만, Matrix Strategy, Cache, Self-hosted Runner를 제대로 활용하면 빌드 시간을 절반 이하로 줄이고 비용도 크게 절감할 수 있습니다.

이 글에서는 실무에서 바로 적용할 수 있는 고급 CI/CD 패턴을 다룹니다.

Matrix Strategy: 병렬 빌드의 힘

기본 Matrix 구성

Matrix를 사용하면 여러 환경 조합을 자동으로 병렬 실행합니다:

name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
        os: [ubuntu-latest, macos-latest]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test

이 설정은 3(Node 버전) × 2(OS) = 6개의 병렬 Job을 생성합니다.

고급 Matrix: include와 exclude

특정 조합만 추가하거나 제외할 수 있습니다:

strategy:
  fail-fast: false  # 하나가 실패해도 나머지 계속 실행
  max-parallel: 4   # 동시 실행 최대 4개
  matrix:
    node-version: [18, 20, 22]
    os: [ubuntu-latest, macos-latest, windows-latest]
    exclude:
      # Node 18 + Windows 조합 제외
      - node-version: 18
        os: windows-latest
    include:
      # 특정 조합에만 추가 변수 설정
      - node-version: 22
        os: ubuntu-latest
        coverage: true

동적 Matrix 생성

빌드 시점에 Matrix를 동적으로 결정하는 패턴:

jobs:
  prepare:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - uses: actions/checkout@v4
      - id: set-matrix
        run: |
          # 변경된 패키지만 빌드 대상으로 설정
          CHANGED=$(git diff --name-only HEAD~1 | grep "^packages/" | cut -d/ -f2 | sort -u | jq -R . | jq -s .)
          echo "matrix={\"package\":$CHANGED}" >> $GITHUB_OUTPUT

  build:
    needs: prepare
    runs-on: ubuntu-latest
    strategy:
      matrix: ${{ fromJson(needs.prepare.outputs.matrix) }}
    steps:
      - uses: actions/checkout@v4
      - run: npm run build --workspace=packages/${{ matrix.package }}

Cache: 빌드 속도 최적화

의존성 캐싱 기본

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

setup-* 액션의 내장 캐시

대부분의 setup 액션은 캐시를 내장하고 있습니다:

# Node.js - 내장 캐시
- uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: 'npm'

# Python - pip 캐시
- uses: actions/setup-python@v5
  with:
    python-version: '3.12'
    cache: 'pip'

# Go - 모듈 캐시
- uses: actions/setup-go@v5
  with:
    go-version: '1.22'
    cache: true

Docker 레이어 캐싱

Docker 빌드에서 GitHub Actions Cache를 활용하면 극적인 속도 향상이 가능합니다:

- uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: myapp:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max

캐시 전략 비교

캐시 방식장점단점
actions/cache범용, 모든 경로 캐싱 가능수동 키 관리 필요
setup-* 내장 캐시설정 간편, 자동 키 생성해당 도구만 지원
Docker GHA cache레이어 단위 캐싱, 매우 빠름Docker 빌드 전용

Self-hosted Runner: 커스텀 환경 구축

왜 Self-hosted Runner인가?

GitHub-hosted Runner의 한계:

  • 비용: 대규모 프로젝트에서 분당 과금 부담
  • 스펙 제한: 최대 7GB RAM, 14GB SSD (Standard)
  • 네트워크: 내부망 접근 불가
  • GPU 미지원: ML/AI 워크로드 불가

Runner 설치 및 등록

# Linux에 Runner 설치
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

# Runner 등록
./config.sh --url https://github.com/YOUR_ORG/YOUR_REPO \
  --token YOUR_TOKEN \
  --labels gpu,linux,x64 \
  --name my-gpu-runner

# systemd 서비스로 등록
sudo ./svc.sh install
sudo ./svc.sh start

Workflow에서 Self-hosted Runner 사용

jobs:
  gpu-test:
    runs-on: [self-hosted, gpu, linux]
    steps:
      - uses: actions/checkout@v4
      - name: GPU 테스트 실행
        run: |
          nvidia-smi
          python -m pytest tests/gpu/ -v

  deploy:
    runs-on: [self-hosted, linux]
    needs: gpu-test
    steps:
      - name: 내부망 배포
        run: |
          kubectl apply -f k8s/
          kubectl rollout status deployment/myapp

Runner를 Kubernetes에서 운영하기

Actions Runner Controller(ARC)를 사용하면 Runner를 Pod로 자동 스케일링합니다:

# ARC 설치 (Helm)
helm install arc \
  --namespace arc-systems \
  --create-namespace \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller

# Runner Scale Set 배포
helm install arc-runner-set \
  --namespace arc-runners \
  --create-namespace \
  -f values.yaml \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set

values.yaml 예시:

githubConfigUrl: "https://github.com/YOUR_ORG"
githubConfigSecret:
  github_token: "ghp_xxxxx"
maxRunners: 10
minRunners: 1
template:
  spec:
    containers:
      - name: runner
        image: ghcr.io/actions/actions-runner:latest
        resources:
          requests:
            cpu: "2"
            memory: "4Gi"
          limits:
            cpu: "4"
            memory: "8Gi"

실전 통합 예제: 모노레포 CI/CD

모든 고급 기능을 조합한 실전 파이프라인:

name: Monorepo CI/CD

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      services: ${{ steps.changes.outputs.services }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - id: changes
        run: |
          SERVICES=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} \
            | grep "^services/" | cut -d/ -f2 | sort -u \
            | jq -R . | jq -s .)
          echo "services=$SERVICES" >> $GITHUB_OUTPUT

  build-and-test:
    needs: detect-changes
    if: needs.detect-changes.outputs.services != '[]'
    runs-on: [self-hosted, linux]
    strategy:
      fail-fast: false
      matrix:
        service: ${{ fromJson(needs.detect-changes.outputs.services) }}
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'npm'

      - name: Install & Build
        run: |
          npm ci
          npm run build -w services/${{ matrix.service }}

      - name: Test
        run: npm run test -w services/${{ matrix.service }}

      - name: Docker Build & Push
        if: github.ref == 'refs/heads/main'
        uses: docker/build-push-action@v6
        with:
          context: ./services/${{ matrix.service }}
          push: true
          tags: |
            ghcr.io/${{ github.repository }}/${{ matrix.service }}:${{ github.sha }}
            ghcr.io/${{ github.repository }}/${{ matrix.service }}:latest
          cache-from: type=gha,scope=${{ matrix.service }}
          cache-to: type=gha,scope=${{ matrix.service }},mode=max

  deploy:
    needs: build-and-test
    if: github.ref == 'refs/heads/main'
    runs-on: [self-hosted, linux]
    strategy:
      max-parallel: 1  # 순차 배포
      matrix:
        service: ${{ fromJson(needs.detect-changes.outputs.services) }}
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to Kubernetes
        run: |
          kubectl set image deployment/${{ matrix.service }} \
            app=ghcr.io/${{ github.repository }}/${{ matrix.service }}:${{ github.sha }}
          kubectl rollout status deployment/${{ matrix.service }} --timeout=300s

보안 Best Practices

Secrets 관리

# Environment로 분리
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production  # 승인 필요한 환경
    steps:
      - name: Deploy
        env:
          DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
        run: ./deploy.sh

OIDC로 클라우드 인증 (시크릿 없이)

permissions:
  id-token: write
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789:role/github-actions
      aws-region: ap-northeast-2

마무리

GitHub Actions의 고급 기능을 제대로 활용하면 CI/CD 파이프라인의 속도, 비용, 유연성 모두를 개선할 수 있습니다. Matrix로 병렬 빌드, Cache로 속도 최적화, Self-hosted Runner로 커스텀 환경까지 — 이 세 가지를 조합하면 엔터프라이즈급 파이프라인을 구축할 수 있습니다.

퀴즈

Q1: Matrix Strategy에서 3개 Node 버전 × 2개 OS 조합을 설정하면 몇 개의 Job이 생성되나? 6개. Matrix는 모든 조합의 카테시안 곱을 생성합니다. 3 × 2 = 6개의 병렬 Job.

Q2: Matrix에서 fail-fast: false의 의미는? 하나의 Job이 실패해도 나머지 Job을 중단하지 않고 계속 실행합니다. 기본값은 true로, 하나라도 실패하면 전체 취소됩니다.

Q3: actions/cache의 key에 hashFiles를 사용하는 이유는? package-lock.json 등 의존성 파일의 해시값을 키로 사용하여, 의존성이 변경될 때만 캐시를 갱신하고 그렇지 않으면 기존 캐시를 재사용하기 위함입니다.

Q4: Docker build에서 cache-from: type=gha는 무엇을 의미하나? GitHub Actions의 캐시 백엔드를 Docker 빌드 레이어 캐시로 사용한다는 의미입니다. 별도 레지스트리 없이 빌드 캐시를 저장/복원할 수 있습니다.

Q5: Self-hosted Runner를 Kubernetes에서 자동 스케일링하려면 어떤 도구를 사용하나? Actions Runner Controller(ARC). Helm으로 설치하며, Runner를 Pod로 관리하고 워크로드에 따라 자동 스케일링합니다.

Q6: 동적 Matrix를 생성하려면 어떤 패턴을 사용하나? 첫 번째 Job에서 outputs로 JSON 배열을 출력하고, 두 번째 Job에서 fromJson()으로 파싱하여 matrix에 전달하는 패턴입니다.

Q7: OIDC를 사용한 클라우드 인증의 장점은? 장기 시크릿(Access Key)을 저장할 필요 없이, GitHub Actions가 발급하는 임시 토큰으로 클라우드에 인증합니다. 시크릿 유출 위험이 없고 자동 만료됩니다.

Q8: 모노레포에서 변경된 서비스만 빌드하는 핵심 기법은? git diff로 변경된 파일 경로를 분석하여 영향받는 서비스 목록을 추출하고, 이를 동적 Matrix로 전달하여 해당 서비스만 빌드합니다.