CI/CD ์ํฌํ๋ก์ฐ YAML ๊ตฌ์กฐ, ์ด๋ฒคํธ ํธ๋ฆฌ๊ฑฐ(push/PR/cron/dispatch), Matrix ๋น๋, Secrets & ํ๊ฒฝ๋ณ์, ์บ์ฑ & ์ํฐํฉํธ, ๊ถํ(Permissions) ๋ฐ ์ค์ ๋ฐฐํฌ ํ
ํ๋ฆฟ ์๋ฒฝ ๊ฐ์ด๋
## 1. ์ํฌํ๋ก์ฐ ๊ธฐ๋ณธ ๊ตฌ์กฐ & ๋๋ ํ ๋ฆฌ ๊ท๊ฒฉ
```yaml
# ๐ ์ ์ฅ์ ๋ฃจํธ ๊ธฐ์ค .github/workflows/ci.yml ํ์ผ ์์น
name: Main CI/CD Pipeline
# โก [1] ํธ๋ฆฌ๊ฑฐ ์ด๋ฒคํธ ์ ์
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
# โ๏ธ [2] ์คํ ์์
(Job) ๋ชฉ๋ก ์ ์
jobs:
build-and-test:
name: Build & Unit Test
runs-on: ubuntu-latest # ์คํ Runner ํ๊ฒฝ (ubuntu / windows / macos)
timeout-minutes: 15 # ์์
์ต๋ ์คํ ์๊ฐ ์ ํ (๋ฌดํ ๋๊ธฐ ๋ฐฉ์ง)
steps:
# ๐ฆ ์ ์ฅ์ ์ฝ๋ ์ฒดํฌ์์ (ํ์ ์ฒซ ๋จ๊ณ)
- name: Checkout Source Code
uses: actions/checkout@v4
with:
fetch-depth: 1 # ์ต์ ์ปค๋ฐ 1๊ฐ๋ง ์์ ๋ณต์ (Shallow Clone)ํ์ฌ ์๋ ์ต์ ํ
# ๐ ๏ธ ๋ฐํ์ ํ๊ฒฝ ์ค์
- name: Setup Node.js Runtime
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm' # ํจํค์ง ์์กด์ฑ ์๋ ์บ์ฑ
# ๐ป ์ ๋ช
๋ น์ด ์คํ
- name: Install & Run Tests
run: |
npm ci
npm run test:ci
npm run build
```
์ํฌํ๋ก์ฐ ํ์ผ์ ๋ฐ๋์ ํ๋ก์ ํธ ์ต์์์ `.github/workflows/` ๋๋ ํ ๋ฆฌ ๋ด์ `.yml` ๋๋ `.yaml` ํ์ฅ์๋ก ์์นํด์ผ ํฉ๋๋ค.
## 2. ์ด๋ฒคํธ ํธ๋ฆฌ๊ฑฐ & ํํฐ๋ง (Event Triggers)
```yaml
on:
# ๐ [1] ๋ธ๋์น ๋ฐ ํ๊ทธ ํธ์ ํํฐ๋ง
push:
branches:
- main
- 'releases/**' # releases/v1.0 ๋ฑ ์์ผ๋์นด๋ ๋งค์นญ
branches-ignore:
- 'docs/**' # ํน์ ๋ธ๋์น ํจํด ์ ์ธ
tags:
- 'v*.*.*' # v1.0.0 ํํ์ Git ๋ฆด๋ฆฌ์ฆ ํ๊ทธ ์์ฑ ์๋ง ํธ๋ฆฌ๊ฑฐ
paths:
- 'src/**' # src ํด๋ ๋ณ๊ฒฝ ์์๋ง ์คํ (docs, readme ๋ณ๊ฒฝ ์ ์๋ต)
- 'package.json'
paths-ignore:
- '**.md'
# ๐ฅ [2] ํ ๋ฆฌํ์คํธ(PR) ์ธ๋ถ ์ก์
ํํฐ๋ง
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches: [main]
# โฒ๏ธ [3] CRON ์ ๊ธฐ ์ค์ผ์ค๋ฌ (UTC ๊ธฐ์ค)
schedule:
- cron: '0 0 * * 1-5' # ์~๊ธ ํ๊ตญ์๊ฐ ์ค์ 9์(UTC 00:00) ๋งค์ผ ์๋ ์คํ
# ๐๏ธ [4] GitHub ์น UI ์๋ ์คํ ๋ฒํผ (ํ๋ผ๋ฏธํฐ ์
๋ ฅ ์ง์)
workflow_dispatch:
inputs:
target_env:
description: '๋ฐฐํฌ ๋์ ํ๊ฒฝ'
required: true
default: 'staging'
type: choice
options: [staging, production]
deploy_tag:
description: '๋ฐฐํฌํ ํ๊ทธ ๋ฒ์ '
required: false
type: string
```
`paths` ํํฐ๋ฅผ ํ์ฉํ๋ฉด ๋ฌธ์(`.md`) ์์ ์ด๋ CI์ ๋ฌด๊ดํ ํ์ผ ๋ณ๊ฒฝ ์ ๋ถํ์ํ ๋น๋ ๋ฆฌ์์ค ๋ญ๋น๋ฅผ ํจ๊ณผ์ ์ผ๋ก ๋ฐฉ์งํ ์ ์์ต๋๋ค.
## 3. ํ๊ฒฝ๋ณ์(Env), Contexts & ์ํธํ Secrets
```yaml
env:
GLOBAL_STAGE: 'production' # ์ ์ญ ํ๊ฒฝ๋ณ์
jobs:
deploy:
runs-on: ubuntu-latest
env:
JOB_ENV: 'live' # ํน์ Job ๋ด ํ๊ฒฝ๋ณ์
steps:
- name: Print Built-in GitHub Contexts
run: |
echo "๋ฆฌํฌ์งํ ๋ฆฌ: ${{ github.repository }}"
echo "ํธ๋ฆฌ๊ฑฐ ๋ธ๋์น: ${{ github.ref_name }}"
echo "์ปค๋ฐ SHA: ${{ github.sha }}"
echo "์คํ์: ${{ github.actor }}"
echo "๋น๋ ๋ฒํธ: #${{ github.run_number }}"
# ๐ ์ ์ฅ์ Secrets ๋ฐ ์ ์ญ ํ๊ฒฝ๋ณ์ ์ฃผ์
- name: Deploy with Secret Token
env:
DB_PASSWORD: ${{ secrets.PROD_DB_PASSWORD }} # Settings > Secrets์์ ๋ฑ๋ก
API_URL: ${{ vars.PUBLIC_API_URL }} # Settings > Variables์์ ๋ฑ๋ก
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # ์๋ ๋ฐ๊ธ ๊ธฐ๋ณธ ํ ํฐ
run: |
./deploy.sh --token "$DB_PASSWORD" --url "$API_URL"
```
๋ณด์ ํ ํฐ ๋ฐ ํจ์ค์๋๋ ์ผ๋ฐ `vars`๊ฐ ์๋ ์ํธํ ์ ์ฅ์์ธ `secrets`์ ๋ฑ๋กํด์ผ ๋ก๊ทธ์ ๋ง์คํน(`***`) ์ฒ๋ฆฌ๋ฉ๋๋ค.
## 4. ์กฐ๊ฑด๋ฌธ(if) & ์ํ ๊ฒ์ฌ ํจ์ (Conditionals)
```yaml
jobs:
notify-and-cleanup:
runs-on: ubuntu-latest
steps:
# โ
[1] ํน์ ๋ธ๋์น ๋๋ ์ด๋ฒคํธ ์กฐ๊ฑด๋ถ ์คํ
- name: Run Only on Main Branch Push
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: echo "Production ๋ฐฐํฌ ๋จ๊ณ ์คํ"
# โ [2] ์์ Step์ด ์คํจํ์ ๋๋ง ์คํ (์๋ฆผ ์ ์ก ๋ฑ)
- name: Send Slack Alert on Failure
if: failure()
run: ./send-slack-alert.sh --status "failed"
# ๐ [3] ์ฑ๊ณต/์คํจ ์ฌ๋ถ์ ๊ด๊ณ์์ด ๋ฌด์กฐ๊ฑด ํญ์ ์คํ
- name: Always Cleanup Temporary Files
if: always()
run: rm -rf /tmp/build-cache
# ๐ [4] ์๋ฌ๊ฐ ๋ฐ์ํด๋ ์ ์ฒด ์ํฌํ๋ก์ฐ๋ฅผ ์ค๋จํ์ง ์์
- name: Optional Lint Check
continue-on-error: true
run: npm run lint:lenient
```
`always()`, `failure()`, `success()`, `cancelled()` ์ํ ํจ์๋ฅผ ์ฌ์ฉํ๋ฉด ์ด์ ๋จ๊ณ์ ์คํจ ์ฌ๋ถ์ ๋ฐ๋ผ ์ฌ๋/๋์ค์ฝ๋ ์ฅ์ ์๋ฆผ์ด๋ ๋ท์ ๋ฆฌ ์คํฌ๋ฆฝํธ๋ฅผ ์์ ์ ์ผ๋ก ๊ตฌ๋ํ ์ ์์ต๋๋ค.
## 5. Matrix ์ ๋ต (๋ณ๋ ฌ ๋ฉํฐ ๋ฒ์ ๋น๋)
```yaml
jobs:
cross-test:
name: Test Node ${{ matrix.node }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false # ํ๋์ ํ์ด ์คํจํด๋ ๋ค๋ฅธ ๋ฒ์ ํ
์คํธ๋ฅผ ์ค๋จํ์ง ์๊ณ ๊ณ์ ์งํ
max-parallel: 4 # ๋์ ์คํ ์ต๋ Worker ์ ์ ํ
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
# โ ํน์ ์กฐํฉ ์ ์ฉ ํ๊ฒฝ๋ณ์/์ค์ ์ถ๊ฐ (include)
include:
- os: ubuntu-latest
node: 22
experimental: true
# โ ํน์ ์กฐํฉ ์ ์ธ (exclude)
exclude:
- os: windows-latest
node: 18
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm test
```
๋ค์ํ OS์ ๋ฐํ์ ๋ฒ์ ์กฐํฉ์ ๋ณ๋ ฌ๋ก ๋งคํธ๋ฆญ์ค ํ์ฅํ์ฌ ํฌ๋ก์ค ํ๋ซํผ ํธํ์ฑ ํ
์คํธ ์๊ฐ์ ๊ทน์ ์ผ๋ก ๋จ์ถํฉ๋๋ค.
## 6. Job ๊ฐ ์์กด์ฑ & ๋ฐ์ดํฐ ์ ๋ฌ (Needs & Outputs)
```yaml
jobs:
# 1๋จ๊ณ: ๋น๋ ๋ฐ ๊ฒฐ๊ณผ๊ฐ ๋์ถ
build:
runs-on: ubuntu-latest
outputs:
artifact_version: ${{ steps.gen-version.outputs.pkg_ver }}
image_tag: ${{ steps.gen-version.outputs.img_tag }}
steps:
- uses: actions/checkout@v4
- id: gen-version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "pkg_ver=$VERSION" >> "$GITHUB_OUTPUT"
echo "img_tag=v${VERSION}-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
# 2๋จ๊ณ: 1๋จ๊ณ๊ฐ ์ฑ๊ณตํ ํ ๊ฒฐ๊ณผ๊ฐ์ ๋ฐ์ ๋ฐฐํฌ ์คํ
deploy:
needs: [build] # build Job์ด ์ฑ๊ณตํด์ผ๋ง ์คํ๋จ
runs-on: ubuntu-latest
steps:
- name: Deploy Built Version
run: |
echo "๋ฐฐํฌํ ํจํค์ง ๋ฒ์ : ${{ needs.build.outputs.artifact_version }}"
echo "๋์ปค ์ด๋ฏธ์ง ํ๊ทธ: ${{ needs.build.outputs.image_tag }}"
```
Step ๊ฐ ์ถ๋ ฅ๊ฐ์ `echo "ํค=๊ฐ" >> "$GITHUB_OUTPUT"`์ผ๋ก ๊ธฐ๋กํ๋ฉฐ, Job ๊ฐ์๋ `outputs` ์ ์ธ ํ `needs.<job_id>.outputs.<key>`๋ก ์ฐธ์กฐํฉ๋๋ค.
## 7. ๋น๋ ์บ์ฑ ์ต์ ํ (Dependency Caching)
```yaml
jobs:
build-with-cache:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# โก [1] ๋ฒ์ฉ actions/cache@v4 ํ์ฉ (ํค ๊ธฐ๋ฐ ๋ณต์)
- name: Cache Node Modules
uses: actions/cache@v4
id: npm-cache
with:
path: ~/.npm
key: ${{ runner.os }}-build-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-npm-
# โ [2] Gradle ๋น๋ ์บ์ (Spring Boot ๋ฑ)
- 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-
# ๐ [3] ์
์
์ก์
๋ด์ฅ ์๋ ์บ์ (๊ฐ์ฅ ๊ถ์ฅ)
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm' # ๋๋ 'yarn', 'pnpm'
- run: npm ci
```
`package-lock.json`์ด๋ `build.gradle` ํ์ผ์ ํด์๊ฐ(`hashFiles`)์ ์บ์ ํค๋ก ์ง์ ํ๋ฉด ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ถ๊ฐ๊ฐ ์์ ๋ ๋ค์ด๋ก๋ ์๊ฐ์ 80% ์ด์ ๋จ์ถํ ์ ์์ต๋๋ค.
## 8. ๋น๋ ์ฐ์ถ๋ฌผ ์ํฐํฉํธ ๊ณต์ (Upload & Download)
```yaml
jobs:
build-app:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build # dist ํด๋ ์์ฑ
# โฌ๏ธ [1] ์ฐ์ถ๋ฌผ ์์ถ ์
๋ก๋ (Job ๊ฐ ์ ๋ฌ ๋๋ ์น UI ๋ค์ด๋ก๋์ฉ)
- name: Upload Build Artifacts
uses: actions/upload-artifact@v4
with:
name: production-dist
path: dist/
retention-days: 7 # ๋ณด๊ด ๊ธฐ๊ฐ 7์ผ ์ค์ (์ ์ฅ ๊ณต๊ฐ ์ ์ฝ)
release-deploy:
needs: [build-app]
runs-on: ubuntu-latest
steps:
# โฌ๏ธ [2] ์ด์ Job์์ ์์ฑํ ์ํฐํฉํธ ๋ค์ด๋ก๋
- name: Download Build Artifacts
uses: actions/download-artifact@v4
with:
name: production-dist
path: ./dist
- name: Check Downloaded Files
run: ls -la ./dist
```
Job๋ค์ ๊ฐ๊ฐ ๋
๋ฆฝ๋ ๊ฒฉ๋ฆฌ ๊ฐ์๋จธ์ ์์ ์คํ๋๋ฏ๋ก ํ์ผ ์์คํ
์ ๊ณต์ ํ ์ ์์ต๋๋ค. Job ๊ฐ์ ์์ฑ๋ ๋ฒ๋ค ํ์ผ์ `upload-artifact`์ `download-artifact`๋ฅผ ํตํด ์ ๋ฌํด์ผ ํฉ๋๋ค.
## 9. ์ต์ ๊ถํ ๋ณด์ ์ค์ (Permissions)
```yaml
# ๐ [1] ์ํฌํ๋ก์ฐ ์ ์ฒด ๊ธฐ๋ณธ ๊ถํ์ ์ฝ๊ธฐ ์ ์ฉ์ผ๋ก ์ ํ (๋ณด์ ๋ชจ๋ฒ ์ฌ๋ก)
permissions:
contents: read
jobs:
create-release-and-pr:
runs-on: ubuntu-latest
# ๐ [2] ํน์ Job์์๋ง ํ์ํ ์ธ๋ถ ๊ถํ ๋ช
์์ ๋ถ์ฌ
permissions:
contents: write # ๋ฆด๋ฆฌ์ฆ ํ๊ทธ ์์ฑ ๋ฐ ์ปค๋ฐ ํธ์
pull-requests: write # PR์ ์ฝ๋ฉํธ ๋จ๊ธฐ๊ธฐ
issues: write # ์ด์ ์์ฑ ๋ฐ ์์
packages: write # GHCR (์ปจํ
์ด๋ ๋ ์ง์คํธ๋ฆฌ) ํธ์
id-token: write # AWS, GCP OIDC ์ธ์ฆ ์ฐ๋ ์ ํ์
steps:
- uses: actions/checkout@v4
- name: Add PR Comment
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '๐ CI ๋น๋ ๋ฐ ํ
์คํธ ํต๊ณผ ์๋ฃ!'
})
```
๊ธฐ๋ณธ ๋ฐ๊ธ๋๋ `GITHUB_TOKEN`์ ๊ณผ๋ํ ๊ถํ์ด ์ฃผ์ด์ง๋ ๊ฒ์ ๋ง๊ธฐ ์ํด ์ต์๋จ์ `permissions: { contents: read }`๋ฅผ ๋๊ณ ํ์ํ Job์๋ง ์ฐ๊ธฐ ๊ถํ์ ๋ถ์ฌํ๋ ๊ฒ์ด ๋ณด์์ ํต์ฌ์
๋๋ค.
## 10. ์ฌ์ฌ์ฉ ์ํฌํ๋ก์ฐ & ๋ณตํฉ ์ก์
(Reusable Workflows)
```yaml
# ๐ .github/workflows/reusable-deploy.yml (ํธ์ถ๋ ๊ณตํต ํ
ํ๋ฆฟ)
name: Reusable Deploy Module
on:
workflow_call:
inputs:
target_env:
required: true
type: string
secrets:
DEPLOY_KEY:
required: true
jobs:
run-deploy:
runs-on: ubuntu-latest
steps:
- run: echo "Deploying to ${{ inputs.target_env }} with secret key"
# -------------------------------------------------------------
# ๐ .github/workflows/main.yml (ํธ์ถํ๋ ๋ฉ์ธ ์ํฌํ๋ก์ฐ)
jobs:
call-deploy:
uses: ./.github/workflows/reusable-deploy.yml
with:
target_env: 'production'
secrets:
DEPLOY_KEY: ${{ secrets.PROD_SERVER_KEY }}
# ๋๋ ์์ ์ํฌํ๋ก์ฐ ์ํฌ๋ฆฟ ์ผ๊ด ์์: secrets: inherit
```
`workflow_call`์ ์ฌ์ฉํ๋ฉด ์ฌ๋ฌ ์๋น์ค ๋ฆฌํฌ์งํ ๋ฆฌ๋ ํ์ดํ๋ผ์ธ์์ ์ค๋ณต๋๋ ๋ฐฐํฌ/ํ
์คํธ ๋ก์ง์ ํ๋์ ํ
ํ๋ฆฟ ํ์ผ๋ก ์ค์ ๊ด๋ฆฌํ ์ ์์ต๋๋ค.
## 11. ์๋น์ค ์ปจํ
์ด๋ ํตํฉ ํ
์คํธ (Docker Services)
```yaml
jobs:
integration-test:
runs-on: ubuntu-latest
# ๐ณ Runner ๋ฐฑ๊ทธ๋ผ์ด๋์ ํจ๊ป ๋์ธ ์๋น์ค ์ปจํ
์ด๋ ์ ์
services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_DB: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_password
ports:
- 5432:5432
# ์๋น์ค๊ฐ ์์ ํ ์ค๋น๋ ๋๊น์ง ๋๊ธฐํ๋ ํฌ์ค์ฒดํฌ ์ต์
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- name: Run Integration Tests with Database
env:
DB_HOST: localhost
DB_PORT: 5432
run: npm run test:e2e
```
`services` ํค์๋๋ฅผ ์ฌ์ฉํ๋ฉด ๋ณ๋์ ์ธ๋ถ ํด๋ผ์ฐ๋ DB ์์ด๋ ์ค์ PostgreSQL, Redis, MySQL ์ปจํ
์ด๋๋ฅผ ๊ฐ์๋จธ์ ์ ๋์ ์๋ฒฝํ ์๋ํฌ์๋(E2E) ํตํฉ ํ
์คํธ๋ฅผ ์ํํ ์ ์์ต๋๋ค.
## 12. ์ค์ ํ๋ก๋์
CI/CD ํ
ํ๋ฆฟ ๋ชจ์
```yaml
# ๐ [1] Spring Boot (Gradle) ๋น๋ & ํ
์คํธ ํ์ดํ๋ผ์ธ
name: Java Spring CI
on: [push]
jobs:
spring-ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
- name: Grant Execute Permission for Gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build -x test --parallel
- name: Run Unit Tests
run: ./gradlew test
# ๐ณ [2] Docker ์ด๋ฏธ์ง ๋น๋ & GHCR(์ปจํ
์ด๋ ๋ ์ง์คํธ๋ฆฌ) ํธ์
docker-publish:
needs: [spring-ci]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
```
`temurin` ๋ฐฐํฌํ๊ณผ `cache: 'gradle'`, ๊ทธ๋ฆฌ๊ณ Docker์ `type=gha` ์บ์ ๋ฐฑ์๋๋ฅผ ๊ฒฐํฉํ๋ฉด ์ํฐํ๋ผ์ด์ฆ ํ๊ฒฝ์์๋ 2~3๋ถ ์ด๋ด์ ์ด๊ณ ์ ๋ฐฐํฌ ํ์ดํ๋ผ์ธ์ ๊ตฌ์ถํ ์ ์์ต๋๋ค.
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์