- Published on
GitHub Actions 고급 CI/CD — Matrix, Cache, Self-hosted Runner
- Authors
- Name
- 들어가며
- Matrix Strategy: 병렬 빌드의 힘
- Cache: 빌드 속도 최적화
- Self-hosted Runner: 커스텀 환경 구축
- 실전 통합 예제: 모노레포 CI/CD
- 보안 Best Practices
- 마무리
- 퀴즈

들어가며
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로 전달하여 해당 서비스만 빌드합니다.