- Authors
- Name
- 서론: 왜 지금 GitHub Actions 고급 최적화인가
- 1. 매트릭스 빌드 최적화
- 2. 캐시 전략 심화
- 3. Reusable Workflow와 Composite Action
- 4. GitHub-hosted vs Self-hosted Runner 비교
- 5. OIDC 기반 클라우드 인증 (보안 하드닝 핵심)
- 6. 보안 하드닝 종합 가이드
- 7. 실전 통합 워크플로우: 프로덕션 파이프라인
- 8. 운영 주의사항과 실패 사례
- 9. 프로덕션 CI/CD 체크리스트
- 마무리
- 참고 자료
서론: 왜 지금 GitHub Actions 고급 최적화인가
2026년 현재, GitHub Actions는 전 세계에서 가장 널리 사용되는 CI/CD 플랫폼으로 자리 잡았습니다. GitHub 공식 발표에 따르면 Fortune 100 기업의 90% 이상이 GitHub Actions를 사용하고 있으며, 매일 수백만 개의 워크플로우가 실행되고 있습니다. 하지만 대부분의 팀은 기본적인 빌드-테스트-배포 파이프라인에 머물러 있습니다.
프로덕션 환경에서는 단순한 파이프라인만으로는 부족합니다. 멀티 플랫폼 지원, 빌드 시간 최적화, 비밀 관리, 공급망 보안(Supply Chain Security)까지 고려해야 합니다. 특히 2025년 말 발생한 tj-actions/changed-files 액션 공급망 공격 사건은 GitHub Actions 보안 하드닝의 중요성을 다시 한번 상기시켜 주었습니다.
이 글에서는 매트릭스 빌드 최적화, 캐시 전략, Reusable Workflow, OIDC 기반 인증, 보안 하드닝까지 프로덕션 수준의 CI/CD 파이프라인을 구축하기 위한 고급 기법을 다룹니다.
1차 출처 및 공식 문서
| 출처 | 설명 |
|---|---|
| GitHub Actions 공식 문서 | 워크플로우 문법, 이벤트, 런너 등 전체 레퍼런스 |
| GitHub Blog - Actions Security Best Practices | 공급망 보안 및 액션 보안 모범 사례 |
| OpenID Connect in GitHub Actions | OIDC 토큰 기반 클라우드 인증 공식 가이드 |
| GitHub Actions - Caching Dependencies | 의존성 캐싱 전략 공식 가이드 |
| Reusable Workflows | 워크플로우 재사용 패턴 공식 문서 |
| GitHub Actions Runner - Self-hosted | 셀프 호스트 러너 설정 및 운영 가이드 |
1. 매트릭스 빌드 최적화
1.1 기본 매트릭스 전략
매트릭스 전략(Matrix Strategy)은 여러 환경 조합에 대해 병렬로 워크플로우를 실행하는 핵심 기능입니다. OS, 언어 버전, 의존성 버전 등을 조합하여 교차 테스트를 자동화할 수 있습니다.
name: Matrix CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
max-parallel: 6
matrix:
os: [ubuntu-22.04, ubuntu-24.04, macos-14]
node-version: [20, 22]
include:
- os: ubuntu-24.04
node-version: 22
coverage: true
exclude:
- os: macos-14
node-version: 20
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
- name: Upload coverage
if: ${{ matrix.coverage }}
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
위 설정에서 fail-fast: false는 하나의 매트릭스 조합이 실패하더라도 나머지 조합의 실행을 계속하도록 합니다. 이렇게 하면 어떤 환경에서 문제가 발생하는지 한번에 파악할 수 있습니다.
1.2 동적 매트릭스 생성
변경된 파일에 따라 테스트 대상을 동적으로 결정하는 패턴은 대규모 모노레포에서 빌드 시간을 크게 절감합니다.
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Detect changed services
id: set-matrix
run: |
CHANGED=$(git diff --name-only HEAD~1 HEAD | grep -oP '^services/\K[^/]+' | sort -u | jq -R . | jq -s .)
if [ "$CHANGED" = "[]" ]; then
echo "matrix={\"service\":[\"dummy\"]}" >> $GITHUB_OUTPUT
else
echo "matrix={\"service\":$CHANGED}" >> $GITHUB_OUTPUT
fi
build:
needs: detect-changes
if: ${{ fromJson(needs.detect-changes.outputs.matrix).service[0] != 'dummy' }}
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJson(needs.detect-changes.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- name: Build service
run: |
echo "Building ${{ matrix.service }}"
cd services/${{ matrix.service }}
docker build -t ${{ matrix.service }}:${{ github.sha }} .
1.3 매트릭스 최적화 팁
- max-parallel 제한: 동시 실행 수를 제한하여 GitHub-hosted 러너의 동시성 한도를 초과하지 않도록 합니다
- fail-fast 전략 선택: 빠른 피드백이 중요하면
true, 전체 호환성 확인이 중요하면false - include로 특수 케이스 추가: 특정 조합에만 추가 환경 변수나 단계를 지정
- exclude로 불필요한 조합 제거: 지원하지 않는 조합(예: Windows + ARM)을 명시적으로 제외
2. 캐시 전략 심화
2.1 캐시 유형별 비교
| 캐시 전략 | 장점 | 단점 | 적합한 상황 |
|---|---|---|---|
| actions/cache | 범용적, 세밀한 제어 가능 | 키 관리 수동 | 커스텀 빌드 도구 |
| setup-node cache 옵션 | 설정 간단, 자동 키 생성 | Node.js 전용 | Node.js 프로젝트 |
| Docker layer caching | 이미지 빌드 시간 단축 | 캐시 크기 제한(10GB) | 컨테이너 빌드 |
| Artifact caching | Job 간 데이터 전달 | 워크플로우 내 한정 | 빌드 결과물 공유 |
2.2 고급 캐시 설정
효과적인 캐시 키 전략은 빌드 시간을 50~70% 단축할 수 있습니다.
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# 다단계 캐시 복원 전략
- name: Cache node_modules
uses: actions/cache@v4
id: npm-cache
with:
path: |
node_modules
~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# Next.js 빌드 캐시
- name: Cache Next.js build
uses: actions/cache@v4
with:
path: .next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
${{ runner.os }}-nextjs-
# Gradle 캐시 (Java/Kotlin 프로젝트)
- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Install dependencies
if: steps.npm-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Build
run: npm run build
2.3 Docker 빌드 캐시 최적화
Docker 이미지 빌드에서 BuildKit 캐시를 활용하면 빌드 시간을 크게 단축할 수 있습니다.
jobs:
docker-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push with cache
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
cache-from: type=gha와 cache-to: type=gha,mode=max를 사용하면 GitHub Actions 캐시 백엔드를 통해 Docker 레이어를 캐시합니다. mode=max는 중간 레이어까지 모두 캐시하여 후속 빌드 속도를 극대화합니다.
2.4 캐시 관리 주의사항
- 캐시 크기 제한: 저장소당 총 10GB까지 캐시를 저장할 수 있으며, 초과 시 가장 오래된 캐시부터 삭제됩니다
- 캐시 격리:
pull_request이벤트의 캐시는 기본 브랜치의 캐시를 읽을 수 있지만, 기본 브랜치에 쓸 수 없습니다 - 캐시 무효화: lock 파일(package-lock.json, go.sum 등)의 해시를 캐시 키에 포함시켜 의존성 변경 시 자동으로 캐시가 갱신되도록 합니다
- 보안: 캐시에 민감한 정보(토큰, 크레덴셜)가 포함되지 않도록 주의합니다. Fork된 저장소에서 캐시 포이즈닝(poisoning) 공격이 가능할 수 있습니다
3. Reusable Workflow와 Composite Action
3.1 Reusable Workflow
조직 전체에서 표준화된 CI/CD 파이프라인을 공유하려면 Reusable Workflow를 사용합니다. 호출하는 쪽에서는 uses 키워드로 다른 저장소의 워크플로우를 참조합니다.
호출되는 워크플로우 (.github/workflows/reusable-deploy.yml):
name: Reusable Deploy Workflow
on:
workflow_call:
inputs:
environment:
description: 'Target environment'
required: true
type: string
image-tag:
description: 'Docker image tag'
required: true
type: string
secrets:
KUBE_CONFIG:
required: true
outputs:
deploy-url:
description: 'Deployed URL'
value: ${{ jobs.deploy.outputs.url }}
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
outputs:
url: ${{ steps.deploy.outputs.url }}
steps:
- uses: actions/checkout@v4
- name: Configure kubectl
run: |
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig
- name: Deploy to Kubernetes
id: deploy
run: |
kubectl set image deployment/app \
app=${{ inputs.image-tag }} \
-n ${{ inputs.environment }}
kubectl rollout status deployment/app \
-n ${{ inputs.environment }} --timeout=300s
URL=$(kubectl get ingress app -n ${{ inputs.environment }} -o jsonpath='{.spec.rules[0].host}')
echo "url=https://$URL" >> $GITHUB_OUTPUT
호출하는 워크플로우:
name: Production Deploy
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
steps:
- uses: actions/checkout@v4
- name: Build image
id: meta
run: |
TAG="ghcr.io/${{ github.repository }}:${{ github.ref_name }}"
docker build -t $TAG .
docker push $TAG
echo "tags=$TAG" >> $GITHUB_OUTPUT
deploy-staging:
needs: build
uses: my-org/shared-workflows/.github/workflows/reusable-deploy.yml@v2
with:
environment: staging
image-tag: ${{ needs.build.outputs.image-tag }}
secrets:
KUBE_CONFIG: ${{ secrets.STAGING_KUBE_CONFIG }}
deploy-production:
needs: [build, deploy-staging]
uses: my-org/shared-workflows/.github/workflows/reusable-deploy.yml@v2
with:
environment: production
image-tag: ${{ needs.build.outputs.image-tag }}
secrets:
KUBE_CONFIG: ${{ secrets.PROD_KUBE_CONFIG }}
3.2 Composite Action
Composite Action은 여러 단계(steps)를 하나의 재사용 가능한 액션으로 묶습니다. Reusable Workflow와 달리 Job이 아닌 Step 수준에서 재사용됩니다.
# .github/actions/setup-and-test/action.yml
name: 'Setup and Test'
description: 'Install dependencies, lint, and test'
inputs:
node-version:
description: 'Node.js version'
required: false
default: '22'
working-directory:
description: 'Working directory for the project'
required: false
default: '.'
outputs:
coverage-percentage:
description: 'Test coverage percentage'
value: ${{ steps.coverage.outputs.percentage }}
runs:
using: 'composite'
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.working-directory }}
run: npm ci
- name: Lint
shell: bash
working-directory: ${{ inputs.working-directory }}
run: npm run lint
- name: Test with coverage
shell: bash
working-directory: ${{ inputs.working-directory }}
run: npm test -- --coverage --coverageReporters=text-summary
- name: Extract coverage
id: coverage
shell: bash
run: |
COVERAGE=$(cat ${{ inputs.working-directory }}/coverage/coverage-summary.json | jq '.total.lines.pct')
echo "percentage=$COVERAGE" >> $GITHUB_OUTPUT
3.3 Reusable Workflow vs Composite Action 비교
| 구분 | Reusable Workflow | Composite Action |
|---|---|---|
| 재사용 단위 | Job 전체 | Step 단위 |
| 호출 방식 | jobs.*.uses | steps.*.uses |
| secrets 전달 | 명시적으로 전달 필요 | 호출자의 컨텍스트 상속 |
| 중첩 호출 | 최대 4단계 | 최대 10단계 |
| 러너 선택 | 워크플로우 내에서 지정 | 호출자의 러너 사용 |
| 적합한 용도 | 전체 파이프라인 표준화 | 공통 setup/teardown 단계 |
4. GitHub-hosted vs Self-hosted Runner 비교
4.1 비교표
| 항목 | GitHub-hosted Runner | Self-hosted Runner |
|---|---|---|
| 비용 | 분당 과금 (Linux 0.008 USD/분) | 인프라 비용만 발생 |
| 환경 | 매번 깨끗한 VM | 지속적 환경 (캐시 유지 가능) |
| 커스터마이즈 | 제한적 (사전 설치 도구만) | 완전 자유 (GPU, 특수 HW) |
| 보안 | GitHub이 관리 | 조직이 직접 관리 |
| 네트워크 | 퍼블릭 인터넷 | 프라이빗 네트워크 접근 가능 |
| 스케일링 | 자동 | 수동 또는 오토스케일링 구성 필요 |
| 동시성 한도 | 플랜에 따라 다름 | 직접 제어 |
| 유지보수 | 불필요 | OS 패치, 러너 업데이트 필요 |
4.2 Self-hosted Runner 보안 주의사항
Self-hosted Runner를 퍼블릭 저장소에 사용하면 안 됩니다. Fork된 PR에서 악의적인 코드가 러너에서 실행될 수 있기 때문입니다. 반드시 프라이빗 저장소에서만 사용하거나, Actions Runner Controller(ARC)와 같은 도구로 임시(ephemeral) 러너를 사용하세요.
# Actions Runner Controller (ARC) - Kubernetes 기반 오토스케일링 셀프 호스트 러너
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
name: runner-deployment
spec:
replicas: 3
template:
spec:
repository: my-org/my-repo
ephemeral: true
labels:
- self-hosted
- linux
- x64
- gpu
resources:
limits:
nvidia.com/gpu: 1
memory: '16Gi'
requests:
cpu: '4'
memory: '8Gi'
---
apiVersion: actions.summerwind.dev/v1alpha1
kind: HorizontalRunnerAutoscaler
metadata:
name: runner-autoscaler
spec:
scaleTargetRef:
kind: RunnerDeployment
name: runner-deployment
minReplicas: 1
maxReplicas: 10
metrics:
- type: TotalNumberOfQueuedAndInProgressWorkflowRuns
repositoryNames:
- my-org/my-repo
5. OIDC 기반 클라우드 인증 (보안 하드닝 핵심)
5.1 왜 OIDC인가
기존 방식에서는 AWS Access Key, GCP Service Account Key 등 장기 크레덴셜을 GitHub Secrets에 저장해서 사용했습니다. 이 방식에는 여러 위험이 있습니다.
- 시크릿 유출 시 즉시 악용 가능
- 키 로테이션을 수동으로 관리해야 함
- 어떤 워크플로우가 어떤 권한을 사용했는지 추적이 어려움
OIDC(OpenID Connect) 토큰을 사용하면 GitHub Actions가 클라우드 프로바이더에 단기 토큰으로 인증합니다. 장기 크레덴셜이 필요 없어 보안이 크게 향상됩니다.
5.2 AWS OIDC 인증 설정
name: Deploy to AWS
on:
push:
branches: [main]
permissions:
id-token: write # OIDC 토큰 요청에 필요
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
role-session-name: github-actions-${{ github.run_id }}
aws-region: ap-northeast-2
- name: Deploy to ECS
run: |
aws ecs update-service \
--cluster production \
--service my-app \
--force-new-deployment
- name: Verify deployment
run: |
aws ecs wait services-stable \
--cluster production \
--services my-app
5.3 GCP OIDC 인증 설정
jobs:
deploy-gcp:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider'
service_account: 'deploy@my-project.iam.gserviceaccount.com'
- name: Deploy to Cloud Run
uses: google-github-actions/deploy-cloudrun@v2
with:
service: my-app
region: asia-northeast3
image: gcr.io/my-project/my-app:${{ github.sha }}
6. 보안 하드닝 종합 가이드
6.1 최소 권한 원칙 적용
GitHub Actions의 GITHUB_TOKEN은 기본적으로 광범위한 권한을 가집니다. 반드시 필요한 권한만 명시적으로 선언해야 합니다.
name: Secure Workflow
# 전역 수준에서 모든 권한을 최소화
permissions: {}
on:
pull_request:
branches: [main]
jobs:
lint-and-test:
runs-on: ubuntu-latest
# Job 수준에서 필요한 권한만 선언
permissions:
contents: read
checks: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
6.2 액션 버전 피닝 (Supply Chain Security)
태그(v4) 대신 커밋 SHA를 사용하여 액션을 고정합니다. 태그는 악의적으로 변경될 수 있지만 커밋 SHA는 변경이 불가능합니다.
steps:
# 위험: 태그는 변경될 수 있음
# - uses: actions/checkout@v4
# 안전: 커밋 SHA로 고정
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: '22'
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
GitHub에서 제공하는 Dependabot을 활용하면 이러한 SHA 피닝을 자동으로 관리할 수 있습니다. .github/dependabot.yml에 다음을 추가합니다.
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'weekly'
groups:
actions:
patterns:
- '*'
6.3 시크릿 관리 모범 사례
jobs:
secure-deploy:
runs-on: ubuntu-latest
environment: production # Environment Protection Rules 적용
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# 시크릿을 환경 변수로 노출할 때 주의사항
- name: Deploy with secrets
env:
# 개별 시크릿만 명시적으로 전달
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
run: |
# 시크릿이 로그에 노출되지 않도록 마스킹
echo "::add-mask::$DATABASE_URL"
echo "::add-mask::$API_KEY"
./deploy.sh
6.4 Fork PR 보안 설정
Fork된 저장소에서 오는 Pull Request는 보안 위험이 높습니다. 다음 설정을 권장합니다.
pull_request_target대신pull_request이벤트를 사용합니다.pull_request_target은 Base 저장소의 시크릿에 접근할 수 있어 위험합니다- Fork PR에서는 시크릿을 사용하는 Job을 실행하지 않도록 조건을 설정합니다
- Environment Protection Rules로 프로덕션 배포에 수동 승인을 필수화합니다
jobs:
test:
# Fork PR에서도 안전하게 테스트 실행
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: npm ci
- run: npm test
deploy-preview:
# Fork PR에서는 실행하지 않음
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Deploy preview
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: ./deploy-preview.sh
6.5 Artifact Attestation (빌드 출처 증명)
GitHub Actions는 SLSA(Supply-chain Levels for Software Artifacts) 프레임워크를 지원하여 빌드 결과물의 출처를 증명할 수 있습니다.
jobs:
build-with-attestation:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
attestations: write
packages: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Build Docker image
run: |
docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
- name: Push to GHCR
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v2
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: sha256:${{ steps.build.outputs.digest }}
push-to-registry: true
7. 실전 통합 워크플로우: 프로덕션 파이프라인
지금까지 다룬 모든 기법을 통합한 프로덕션 수준의 CI/CD 파이프라인 예시입니다.
name: Production CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
# 전역 최소 권한
permissions:
contents: read
# 동일 브랜치에서 이전 실행 취소
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run type-check
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- run: npm test -- --shard=${{ matrix.shard }}/4
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{ matrix.shard }}
path: test-results/
retention-days: 7
security-scan:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload scan results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
build-and-push:
needs: [lint, test, security-scan]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
attestations: write
outputs:
image-digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Attest build provenance
uses: actions/attest-build-provenance@v2
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
deploy:
needs: build-and-push
runs-on: ubuntu-latest
environment: production
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
aws-region: ap-northeast-2
- name: Deploy to EKS
run: |
aws eks update-kubeconfig --name production-cluster
kubectl set image deployment/app \
app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
kubectl rollout status deployment/app --timeout=300s
8. 운영 주의사항과 실패 사례
8.1 자주 겪는 실패 사례
사례 1: 캐시 포이즈닝 공격
퍼블릭 저장소에서 Fork PR이 악의적인 의존성을 캐시에 주입한 후, 이후 빌드에서 해당 캐시를 사용하여 악성 코드가 실행된 사례가 있습니다. 대응 방안으로 pull_request 이벤트에서 캐시 쓰기를 제한하고, 캐시 키에 브랜치 정보를 포함시킵니다.
사례 2: 시크릿 로그 노출
디버깅을 위해 환경 변수를 출력하는 과정에서 시크릿이 로그에 노출된 사례입니다. GitHub Actions는 등록된 시크릿을 자동으로 마스킹하지만, 시크릿을 가공(base64 인코딩 등)한 후 출력하면 마스킹이 우회됩니다. echo "::add-mask::" 명령으로 가공된 값도 반드시 마스킹해야 합니다.
사례 3: 서드파티 액션 태그 변조
2025년 말 발생한 tj-actions/changed-files 공급망 공격에서는, 공격자가 액션의 태그를 악의적인 코드를 포함한 커밋으로 재지정했습니다. 태그 대신 커밋 SHA를 사용했다면 영향을 받지 않았을 것입니다.
사례 4: concurrency 미설정으로 인한 중복 배포
빠르게 연속 Push를 했을 때 여러 배포 Job이 동시에 실행되어 롤백이 복잡해진 사례입니다. concurrency 그룹을 설정하여 동일 환경에 대한 동시 배포를 방지해야 합니다.
8.2 비용 최적화 팁
- concurrency 설정: 동일 브랜치의 이전 실행을 자동 취소하여 불필요한 실행 시간을 절감합니다
- path 필터: 관련 파일이 변경되었을 때만 워크플로우를 트리거합니다
- 테스트 샤딩: 테스트를 병렬 분할 실행하여 전체 시간을 단축합니다
- Larger Runner 활용: GitHub-hosted Larger Runner를 사용하면 빌드 시간 단축으로 전체 비용이 줄어들 수 있습니다
9. 프로덕션 CI/CD 체크리스트
보안
- 전역
permissions: {}로 최소 권한 원칙 적용 - 모든 서드파티 액션을 커밋 SHA로 피닝
- Dependabot으로 액션 업데이트 자동화
- OIDC 토큰으로 클라우드 인증 (장기 크레덴셜 제거)
- Fork PR에서 시크릿 접근 차단
- Environment Protection Rules로 프로덕션 배포 수동 승인 필수화
-
pull_request_target사용 시 보안 검토 완료
성능
- 의존성 캐시 설정 (actions/cache 또는 setup-* 내장 캐시)
- Docker BuildKit 캐시 활용 (type=gha)
- 매트릭스 빌드로 병렬 테스트 실행
- concurrency 설정으로 중복 실행 방지
- path 필터로 불필요한 워크플로우 실행 제거
- 테스트 샤딩으로 테스트 시간 분산
유지보수
- Reusable Workflow로 공통 패턴 표준화
- Composite Action으로 반복 Step 모듈화
- 워크플로우 파일에 충분한 주석 추가
- 실패 알림 설정 (Slack, Email 등)
- 정기적인 워크플로우 감사 (미사용 시크릿, 불필요한 권한 확인)
공급망 보안
- 빌드 결과물에 Artifact Attestation 적용
- SLSA 프레임워크 레벨 확인 및 목표 설정
- 컨테이너 이미지 서명 (Sigstore/Cosign)
- SBOM(Software Bill of Materials) 생성 및 관리
마무리
GitHub Actions의 고급 기능을 활용하면 단순한 빌드/테스트 자동화를 넘어 프로덕션 수준의 보안과 효율성을 갖춘 CI/CD 파이프라인을 구축할 수 있습니다. 특히 다음 세 가지를 핵심으로 기억하시기 바랍니다.
- 보안 우선: 최소 권한 원칙, SHA 피닝, OIDC 인증은 선택이 아닌 필수입니다
- 캐시 전략: 적절한 캐시 설정만으로도 빌드 시간을 50% 이상 단축할 수 있습니다
- 모듈화: Reusable Workflow와 Composite Action으로 유지보수성을 높이고 조직 전체의 표준을 확립합니다
CI/CD 파이프라인은 한 번 만들고 끝나는 것이 아니라, 지속적으로 개선하고 보안을 강화해야 하는 살아있는 시스템입니다. 정기적으로 체크리스트를 점검하고, 새로운 보안 위협에 대응하는 것이 중요합니다.