들어가며
2025년, CI/CD는 더 이상 선택이 아닌 필수입니다. Google의 DORA(DevOps Research and Assessment) 보고서에 따르면, Elite 수준의 팀은 하루에 여러 번 배포하면서도 변경 실패율 5% 미만을 유지합니다. 반면 Low 수준의 팀은 한 달에 한 번 배포하며 실패율이 46%에 달합니다.
이 격차의 핵심은 **파이프라인 설계**에 있습니다. 단순히 CI/CD 도구를 도입하는 것이 아니라, 테스트 자동화, 보안 통합, 점진적 배포, 그리고 관찰 가능성까지 포함하는 종합적인 전략이 필요합니다.
이 글에서는 CI/CD 파이프라인을 설계하고 운영하는 데 필요한 모든 것을 다룹니다. 플랫폼 비교부터 파이프라인 설계 원칙, 테스트 전략, Docker 빌드 최적화, GitOps, 보안, 배포 전략, 롤백, 그리고 모니터링까지 실전 중심으로 정리했습니다.
1. 2025년 CI/CD 현황
1.1 DORA 메트릭으로 보는 팀 성과
DORA 메트릭은 소프트웨어 딜리버리 성과를 측정하는 4가지 핵심 지표입니다.
| 지표 | Elite | High | Medium | Low |
|------|-------|------|--------|-----|
| 배포 빈도 | 하루 여러 번 | 주 1회~월 1회 | 월 1회~6개월 1회 | 6개월 이상 |
| 리드 타임 (커밋→배포) | 1시간 미만 | 1일~1주 | 1주~1개월 | 1개월~6개월 |
| 변경 실패율 | 0~5% | 5~10% | 10~15% | 46~60% |
| 복구 시간 (MTTR) | 1시간 미만 | 1일 미만 | 1일~1주 | 6개월 이상 |
1.2 시프트 레프트 전략
시프트 레프트(Shift Left)는 테스트와 보안을 개발 초기 단계로 당기는 전략입니다.
전통적 접근:
Code → Build → Test → Security → Deploy → Monitor
↑ 여기서 문제 발견
시프트 레프트:
Code + Test + Security → Build → Deploy → Monitor
↑ 여기서 문제 발견 (비용 10x 절감)
핵심 원칙:
- **커밋 전 검증**: pre-commit hook으로 린트, 포맷, 시크릿 스캔
- **PR 단계 테스트**: 단위 테스트 + 통합 테스트 + SAST 자동 실행
- **빌드 시 보안**: 컨테이너 이미지 스캔, 의존성 취약점 검사
- **배포 전 검증**: 스모크 테스트, 카나리 분석
1.3 2025년 주요 트렌드
- **플랫폼 엔지니어링**: 개발자 셀프 서비스 플랫폼으로 CI/CD 표준화
- **AI 기반 CI/CD**: 테스트 실패 예측, 자동 롤백 결정, 플레이키 테스트 탐지
- **eBPF 기반 관찰 가능성**: 파이프라인 성능 모니터링의 새 패러다임
- **Supply Chain Security**: SBOM, SLSA, Sigstore 기반 소프트웨어 공급망 보안
2. CI/CD 플랫폼 비교
2.1 주요 플랫폼 비교표
| 기능 | GitHub Actions | Jenkins | GitLab CI | CircleCI |
|------|---------------|---------|-----------|----------|
| 호스팅 | SaaS/Self-hosted | Self-hosted | SaaS/Self-hosted | SaaS |
| 설정 방식 | YAML | Groovy/YAML | YAML | YAML |
| 생태계 | Marketplace 15,000+ | Plugin 1,800+ | Built-in 통합 | Orbs 3,000+ |
| 컨테이너 지원 | 네이티브 | 플러그인 | 네이티브 | 네이티브 |
| 셀프러너 | 지원 | 기본 | 지원 | 지원 |
| 가격 | 2,000분 무료/월 | 무료(OSS) | 400분 무료/월 | 6,000크레딧 무료/월 |
| 학습 곡선 | 낮음 | 높음 | 중간 | 낮음 |
| 캐싱 | 10GB/리포 | 플러그인 | 네이티브 | 네이티브 |
2.2 GitHub Actions
.github/workflows/ci.yml
name: CI Pipeline
on:
pull_request:
branches: [main]
push:
branches: [main]
concurrency:
group: ci-${{ '{{' }} github.ref {{ '}}' }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
needs: lint
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm test -- --shard=${{ '{{' }} matrix.shard {{ '}}' }}/4
build:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
2.3 Jenkins Pipeline
// Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
environment {
DOCKER_REGISTRY = 'registry.example.com'
IMAGE_NAME = 'myapp'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Lint & Test') {
parallel {
stage('Lint') {
steps {
sh 'npm run lint'
}
}
stage('Unit Test') {
steps {
sh 'npm test -- --coverage'
}
post {
always {
junit 'reports/junit.xml'
publishHTML([
reportDir: 'coverage',
reportFiles: 'index.html',
reportName: 'Coverage Report'
])
}
}
}
}
}
stage('Build & Push') {
steps {
script {
def image = docker.build("${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER}")
docker.withRegistry("https://${DOCKER_REGISTRY}", 'registry-credentials') {
image.push()
image.push('latest')
}
}
}
}
}
post {
failure {
slackSend(
channel: '#ci-alerts',
color: 'danger',
message: "Build FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
}
}
2.4 GitLab CI
.gitlab-ci.yml
stages:
- lint
- test
- build
- deploy
variables:
DOCKER_HOST: tcp://docker:2376
lint:
stage: lint
image: node:20-alpine
cache:
key: npm-cache
paths:
- node_modules/
script:
- npm ci
- npm run lint
test:
stage: test
image: node:20-alpine
parallel: 4
script:
- npm ci
- npm test -- --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
coverage: '/Statements\s*:\s*(\d+\.?\d*)%/'
artifacts:
reports:
junit: reports/junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
build:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker build -t myapp:latest .
- docker push myapp:latest
only:
- main
3. 파이프라인 설계 원칙
3.1 빠른 피드백 루프
개발자가 PR을 올리고 결과를 기다리는 시간은 직접적으로 생산성에 영향을 미칩니다.
목표 시간:
├── 린트 + 포맷 체크: 30초 이내
├── 단위 테스트: 2분 이내
├── 통합 테스트: 5분 이내
├── 빌드: 3분 이내
└── 전체 파이프라인: 10분 이내
현실 (최적화 전): 30분+
현실 (최적화 후): 8~10분
3.2 병렬 처리
병렬 파이프라인 예시
jobs:
1단계: 린트/보안은 독립적으로 병렬 실행
lint:
runs-on: ubuntu-latest
...
security-scan:
runs-on: ubuntu-latest
...
2단계: 테스트는 shard로 병렬 분할
test:
needs: [lint]
strategy:
matrix:
shard: [1, 2, 3, 4]
...
3단계: 빌드는 테스트 통과 후
build:
needs: [test, security-scan]
...
3.3 캐싱 전략
npm 캐싱 (GitHub Actions)
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ '{{' }} hashFiles('**/package-lock.json') {{ '}}' }}
restore-keys: |
npm-
Docker 레이어 캐싱
- uses: docker/build-push-action@v5
with:
cache-from: type=gha
cache-to: type=gha,mode=max
Gradle 캐싱
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ '{{' }} hashFiles('**/*.gradle*') {{ '}}' }}
3.4 멱등성 (Idempotency)
파이프라인은 같은 입력에 대해 항상 같은 결과를 내야 합니다.
나쁜 예: 타임스탬프 기반 태그 (재실행 시 다른 결과)
IMAGE_TAG: my-app:build-20250323-142000
좋은 예: 커밋 SHA 기반 태그 (항상 동일)
IMAGE_TAG: my-app:abc1234
좋은 예: 시맨틱 버전 (결정적)
IMAGE_TAG: my-app:v1.2.3
4. CI에서의 테스트 전략
4.1 테스트 피라미드
/ E2E \ 느리지만 높은 신뢰도
/ (5~10%) \
/ Integration \ 중간 속도, 중간 신뢰도
/ (15~25%) \
/ Unit Tests \ 빠르고 많이
/ (65~80%) \
/________________________\
4.2 테스트 분할 (Test Splitting)
Jest 테스트 샤딩
test:
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx jest --shard=${{ '{{' }} matrix.shard {{ '}}' }}/4
Cypress 병렬 실행
e2e:
strategy:
matrix:
container: [1, 2, 3]
steps:
- uses: cypress-io/github-action@v6
with:
record: true
parallel: true
group: 'e2e-tests'
4.3 플레이키 테스트 관리
플레이키(Flaky) 테스트는 같은 코드에서 때때로 성공하고 때때로 실패하는 테스트입니다.
// 플레이키 테스트 감지 및 격리 전략
// jest.config.js
module.exports = {
// 실패 시 자동 재시도
retryTimes: 2,
// 플레이키 테스트 리포터
reporters: [
'default',
['jest-flaky-reporter', {
outputFile: 'flaky-tests.json',
threshold: 3 // 3번 이상 플레이키하면 보고
}]
]
};
CI에서 플레이키 테스트 격리
test-stable:
runs-on: ubuntu-latest
steps:
- run: npx jest --testPathIgnorePatterns="flaky"
test-flaky:
runs-on: ubuntu-latest
continue-on-error: true # 실패해도 파이프라인 계속
steps:
- run: npx jest --testPathPattern="flaky" --retries=3
4.4 테스트 커버리지 게이트
커버리지 임계값 설정
test:
steps:
- run: npx jest --coverage
- name: Check coverage threshold
run: |
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.statements.pct')
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "Coverage $COVERAGE% is below 80% threshold"
exit 1
fi
5. Docker 빌드 최적화
5.1 멀티 스테이지 빌드
Stage 1: 의존성 설치
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production
Stage 2: 빌드
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
Stage 3: 프로덕션 이미지
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
보안: non-root 사용자
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER nextjs
EXPOSE 3000
CMD ["npm", "start"]
5.2 레이어 캐싱 최적화
나쁜 예: 소스 변경 시 npm ci 재실행
COPY . .
RUN npm ci
RUN npm run build
좋은 예: 의존성 파일만 먼저 복사
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
5.3 BuildKit과 Buildx
GitHub Actions에서 BuildKit 사용
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
로컬에서 BuildKit 사용
DOCKER_BUILDKIT=1 docker build .
5.4 Kaniko (Docker 데몬 없는 빌드)
Kubernetes에서 Kaniko로 이미지 빌드
apiVersion: v1
kind: Pod
metadata:
name: kaniko-build
spec:
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:latest
args:
- "--dockerfile=Dockerfile"
- "--context=git://github.com/myorg/myapp"
- "--destination=registry.example.com/myapp:latest"
- "--cache=true"
- "--cache-repo=registry.example.com/myapp/cache"
5.5 이미지 크기 최적화
이미지 크기 비교:
├── node:20 → 1.1GB
├── node:20-slim → 220MB
├── node:20-alpine → 140MB
├── distroless/nodejs → 120MB
└── 멀티스테이지 최적화 → 80~100MB
6. GitOps와 ArgoCD
6.1 GitOps 원칙
GitOps는 Git 리포지토리를 단일 진실의 소스(Single Source of Truth)로 사용하는 운영 모델입니다.
GitOps 워크플로:
1. 개발자가 Git에 변경 Push
2. CI가 이미지 빌드 및 테스트
3. CI가 배포 매니페스트의 이미지 태그 업데이트
4. ArgoCD가 Git과 클러스터 상태 비교
5. 차이가 있으면 자동 동기화 (또는 수동 승인)
6. 클러스터가 Git 상태와 일치
┌────────┐ Push ┌────────┐ Detect ┌────────┐
│ Dev │ ──────────> │ Git │ <────────> │ ArgoCD │
└────────┘ └────────┘ └───┬────┘
│ Sync
┌────▼────┐
│ K8s │
│ Cluster │
└─────────┘
6.2 ArgoCD App of Apps 패턴
apps/root-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/gitops-config
targetRevision: main
path: apps
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
apps/api-service.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: api-service
spec:
project: default
source:
repoURL: https://github.com/myorg/gitops-config
path: services/api
targetRevision: main
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
6.3 Argo Rollouts (점진적 배포)
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: api-service
spec:
replicas: 10
strategy:
canary:
canaryService: api-canary
stableService: api-stable
trafficRouting:
istio:
virtualService:
name: api-vsvc
steps:
- setWeight: 10
- pause:
duration: 5m
- analysis:
templates:
- templateName: success-rate
- setWeight: 30
- pause:
duration: 5m
- analysis:
templates:
- templateName: success-rate
- setWeight: 60
- pause:
duration: 5m
- setWeight: 100
AnalysisTemplate
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
metrics:
- name: success-rate
interval: 60s
successCondition: result[0] >= 0.95
provider:
prometheus:
address: http://prometheus:9090
query: |
sum(rate(http_requests_total{status=~"2.*",app="api-service",version="canary"}[5m]))
/
sum(rate(http_requests_total{app="api-service",version="canary"}[5m]))
7. CI/CD 보안
7.1 보안 스캔 통합
CI/CD 보안 레이어:
┌─────────────────────────────────────────────┐
│ Layer 1: Pre-commit │
│ - Secret scanning (gitleaks, detect-secrets)│
│ - Lint (security rules) │
├─────────────────────────────────────────────┤
│ Layer 2: PR / Build │
│ - SAST (Semgrep, CodeQL, SonarQube) │
│ - SCA (Dependabot, Snyk, Trivy) │
│ - License compliance │
├─────────────────────────────────────────────┤
│ Layer 3: Container Build │
│ - Image scanning (Trivy, Grype) │
│ - Base image policy (distroless, alpine) │
│ - SBOM generation (Syft) │
├─────────────────────────────────────────────┤
│ Layer 4: Deploy │
│ - Policy enforcement (OPA/Kyverno) │
│ - Signing (cosign, Sigstore) │
│ - Runtime security (Falco) │
└─────────────────────────────────────────────┘
7.2 시크릿 관리
GitHub Actions에서 시크릿 사용
deploy:
steps:
- name: Deploy
env:
AWS_ACCESS_KEY_ID: ${{ '{{' }} secrets.AWS_ACCESS_KEY_ID {{ '}}' }}
AWS_SECRET_ACCESS_KEY: ${{ '{{' }} secrets.AWS_SECRET_ACCESS_KEY {{ '}}' }}
run: |
aws ecs update-service --cluster prod --service api
OIDC 기반 인증 (시크릿 없는 방식 - 권장)
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions
aws-region: ap-northeast-2
7.3 SBOM과 Supply Chain Security
Syft로 SBOM 생성
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: myapp:latest
format: spdx-json
output-file: sbom.spdx.json
cosign으로 이미지 서명
- name: Sign image
run: |
cosign sign --key env://COSIGN_PRIVATE_KEY myapp:latest
cosign으로 서명 검증
- name: Verify signature
run: |
cosign verify --key cosign.pub myapp:latest
7.4 시크릿 스캔 자동화
pre-commit 설정
.pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
CI에서 gitleaks 실행
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ '{{' }} secrets.GITHUB_TOKEN {{ '}}' }}
8. 배포 전략 비교
8.1 전략 비교표
| 전략 | 다운타임 | 위험도 | 리소스 비용 | 롤백 속도 | 복잡도 |
|------|---------|--------|------------|----------|--------|
| Recreate | 있음 | 높음 | 1x | 느림 | 낮음 |
| Rolling Update | 없음 | 중간 | 1x~1.25x | 중간 | 낮음 |
| Blue-Green | 없음 | 낮음 | 2x | 즉시 | 중간 |
| Canary | 없음 | 매우 낮음 | 1.1x | 즉시 | 높음 |
| A/B Testing | 없음 | 매우 낮음 | 1.1x | 즉시 | 매우 높음 |
8.2 Blue-Green 배포
Kubernetes Blue-Green 배포
service.yaml
apiVersion: v1
kind: Service
metadata:
name: api-service
spec:
selector:
app: api
version: green # blue에서 green으로 전환
ports:
- port: 80
targetPort: 8080
Blue-Green 전환 과정:
1. Blue(v1) 운영 중 → Green(v2) 배포
2. Green 헬스체크 및 스모크 테스트
3. Service selector를 Green으로 전환
4. 문제 시 Blue로 즉시 롤백
5. 안정화 후 Blue 리소스 정리
[Users] → [LB] → [Blue v1] ✓ Active
[Green v2] ← 준비 중
[Users] → [LB] → [Blue v1] ← 대기
[Green v2] ✓ Active
8.3 카나리 배포
Istio VirtualService로 카나리 배포
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: api-service
spec:
hosts:
- api-service
http:
- route:
- destination:
host: api-service
subset: stable
weight: 90
- destination:
host: api-service
subset: canary
weight: 10
8.4 Feature Flags
// LaunchDarkly 또는 자체 Feature Flag 시스템
async function handleRequest(req: Request) {
const userId = req.user.id;
if (await featureFlags.isEnabled('new-checkout-flow', userId)) {
return newCheckoutFlow(req);
}
return legacyCheckoutFlow(req);
}
Feature Flag 기반 배포:
1. 코드에 새 기능을 플래그로 감싸서 배포
2. 내부 사용자에게만 활성화
3. 점진적으로 비율 확대 (1% → 5% → 25% → 100%)
4. 문제 시 플래그만 끄면 즉시 비활성화
5. 배포와 릴리스를 분리
9. 롤백 전략
9.1 자동 롤백
Argo Rollouts 자동 롤백
spec:
strategy:
canary:
steps:
- setWeight: 10
- analysis:
templates:
- templateName: error-rate-check
분석 실패 시 자동 롤백
abortScaleDownDelaySeconds: 30
Kubernetes Deployment 자동 롤백
apiVersion: apps/v1
kind: Deployment
spec:
progressDeadlineSeconds: 300 # 5분 내 완료 안 되면 실패
minReadySeconds: 30
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0
9.2 서킷 브레이커 패턴
// 배포 서킷 브레이커
class DeploymentCircuitBreaker {
private errorThreshold = 0.05; // 5% 에러율
private windowSize = 300; // 5분 윈도우
async shouldRollback(metrics: DeploymentMetrics): Promise<boolean> {
const errorRate = metrics.errors / metrics.totalRequests;
const p99Latency = metrics.p99LatencyMs;
return (
errorRate > this.errorThreshold ||
p99Latency > 3000 // 3초 초과
);
}
async executeRollback(deployment: string) {
console.log(`Rolling back ${deployment}`);
// kubectl rollout undo deployment/api-service
await exec(`kubectl rollout undo deployment/${deployment}`);
// 알림 전송
await notify({
channel: '#deployments',
message: `Auto-rollback triggered for ${deployment}`,
severity: 'critical'
});
}
}
9.3 데이터베이스 마이그레이션 롤백
안전한 DB 마이그레이션 전략:
1. Expand-Contract 패턴
Phase 1 (Expand): 새 컬럼 추가, 양쪽 모두 쓰기
Phase 2 (Migrate): 기존 데이터 마이그레이션
Phase 3 (Contract): 이전 컬럼 제거
2. 롤백 가능한 마이그레이션만 적용
- 컬럼 추가 (롤백 가능)
- 인덱스 추가 (롤백 가능)
- 컬럼 삭제 (롤백 불가 → Expand-Contract 사용)
- 타입 변경 (롤백 불가 → 새 컬럼 추가 후 전환)
-- 안전한 마이그레이션 예시
-- Step 1: 새 컬럼 추가 (롤백 가능)
ALTER TABLE users ADD COLUMN email_verified BOOLEAN DEFAULT FALSE;
-- Step 2: 데이터 마이그레이션 (백그라운드)
UPDATE users SET email_verified = TRUE
WHERE verified_at IS NOT NULL;
-- Step 3: 앱 코드에서 새 컬럼 사용 전환
-- Step 4: 이전 컬럼 삭제 (별도 마이그레이션)
-- ALTER TABLE users DROP COLUMN verified_at;
10. 파이프라인 헬스 모니터링
10.1 핵심 메트릭
파이프라인 헬스 대시보드:
┌─────────────────────────────────────────┐
│ Build Time Trend │
│ ██████████████ 8m (avg) │
│ Target: < 10m │
├─────────────────────────────────────────┤
│ Success Rate │
│ ████████████████████ 94% │
│ Target: > 95% │
├─────────────────────────────────────────┤
│ Flaky Test Rate │
│ ██ 3% │
│ Target: < 2% │
├─────────────────────────────────────────┤
│ Mean Time to Recovery (MTTR) │
│ ████ 25min │
│ Target: < 30min │
└─────────────────────────────────────────┘
10.2 빌드 시간 추적
빌드 시간을 Datadog에 보고
- name: Report build metrics
if: always()
run: |
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
curl -X POST "https://api.datadoghq.com/api/v1/series" \
-H "DD-API-KEY: $DD_API_KEY" \
-d "{
\"series\": [{
\"metric\": \"ci.build.duration\",
\"points\": [[$END_TIME, $DURATION]],
\"tags\": [
\"repo:myapp\",
\"branch:$GITHUB_REF_NAME\",
\"status:$JOB_STATUS\"
]
}]
}"
10.3 실패 분석 자동화
빌드 실패 자동 분류 스크립트
from enum import Enum
class FailureCategory(Enum):
FLAKY_TEST = "flaky_test"
DEPENDENCY = "dependency"
COMPILATION = "compilation"
INFRASTRUCTURE = "infrastructure"
TIMEOUT = "timeout"
UNKNOWN = "unknown"
def categorize_failure(log: str) -> FailureCategory:
patterns = {
FailureCategory.FLAKY_TEST: [
r"retry.*failed",
r"intermittent",
r"flaky"
],
FailureCategory.DEPENDENCY: [
r"npm ERR!.*404",
r"Could not resolve dependencies",
r"ECONNRESET"
],
FailureCategory.COMPILATION: [
r"error TS\d+",
r"SyntaxError",
r"TypeError"
],
FailureCategory.INFRASTRUCTURE: [
r"runner.*offline",
r"disk space",
r"out of memory"
],
FailureCategory.TIMEOUT: [
r"timed out",
r"deadline exceeded"
]
}
for category, regexes in patterns.items():
for pattern in regexes:
if re.search(pattern, log, re.IGNORECASE):
return category
return FailureCategory.UNKNOWN
11. 실전 파이프라인 통합 예시
11.1 풀스택 CI/CD 파이프라인
.github/workflows/production.yml
name: Production Pipeline
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
packages: write
jobs:
Phase 1: 코드 품질
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run type-check
Phase 2: 보안 스캔
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep
uses: returntocorp/semgrep-action@v1
- name: Run Trivy (filesystem)
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
severity: 'HIGH,CRITICAL'
Phase 3: 테스트
test:
needs: quality
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: testdb
POSTGRES_PASSWORD: testpass
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm test -- --shard=${{ '{{' }} matrix.shard {{ '}}' }}/4
env:
DATABASE_URL: postgresql://postgres:testpass@localhost:5432/testdb
Phase 4: 빌드 및 푸시
build:
needs: [test, security]
runs-on: ubuntu-latest
outputs:
image-tag: ${{ '{{' }} steps.meta.outputs.tags {{ '}}' }}
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 {{ '}}' }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/myorg/myapp
- uses: docker/build-push-action@v5
with:
push: true
tags: ${{ '{{' }} steps.meta.outputs.tags {{ '}}' }}
cache-from: type=gha
cache-to: type=gha,mode=max
Phase 5: 배포 매니페스트 업데이트 (GitOps)
update-manifest:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
repository: myorg/gitops-config
token: ${{ '{{' }} secrets.GITOPS_TOKEN {{ '}}' }}
- name: Update image tag
run: |
cd services/api
kustomize edit set image myapp=${{ '{{' }} needs.build.outputs.image-tag {{ '}}' }}
- name: Commit and push
run: |
git config user.name "CI Bot"
git config user.email "ci@example.com"
git add .
git commit -m "chore: update api-service image"
git push
12. 면접 질문 모음
기본 개념
**CI(Continuous Integration)**는 개발자들이 코드 변경을 자주 메인 브랜치에 통합하는 관행입니다. 각 통합은 자동화된 빌드와 테스트로 검증합니다.
**CD**는 두 가지 의미가 있습니다:
- **Continuous Delivery**: 코드가 항상 배포 가능한 상태를 유지. 프로덕션 배포는 수동 승인.
- **Continuous Deployment**: 모든 변경이 자동으로 프로덕션에 배포. 수동 개입 없음.
핵심 차이: CI는 "통합"에, CD는 "전달/배포"에 초점. CI 없이 CD는 불가능하지만, CI만 하고 CD는 안 할 수 있습니다.
1. **배포 빈도(Deployment Frequency)**: 프로덕션에 얼마나 자주 배포하는가
2. **리드 타임(Lead Time for Changes)**: 커밋에서 프로덕션 배포까지 걸리는 시간
3. **변경 실패율(Change Failure Rate)**: 배포 중 실패하거나 롤백이 필요한 비율
4. **복구 시간(MTTR - Mean Time to Recovery)**: 장애 발생 후 복구까지 걸리는 시간
Elite 팀: 하루 여러 번 배포, 1시간 미만 리드 타임, 5% 미만 실패율, 1시간 미만 복구
1. **선언적 기술(Declarative)**: 시스템 상태를 선언적으로 정의
2. **버전 관리(Versioned)**: Git을 단일 진실의 소스로 사용
3. **자동 적용(Automated)**: 승인된 변경이 자동으로 시스템에 적용
4. **자가 치유(Self-Healing)**: 실제 상태가 선언된 상태와 다르면 자동 복구
장점: 감사 추적, 롤백 용이, PR 기반 변경 관리, 재현 가능한 환경
**Blue-Green**: 두 개의 동일한 환경(Blue/Green)을 운영. 새 버전을 Green에 배포 후 트래픽을 한 번에 전환. 롤백은 Blue로 즉시 전환.
- 장점: 즉시 롤백, 간단한 구현
- 단점: 리소스 2배 필요, 데이터베이스 동기화 복잡
**Canary**: 새 버전을 소수(1~10%)에게 먼저 배포. 메트릭 분석 후 점진적 확대.
- 장점: 위험 최소화, 실제 트래픽으로 검증
- 단점: 구현 복잡, 모니터링 필수
테스트와 보안을 개발 라이프사이클의 왼쪽(초기 단계)으로 이동시키는 전략입니다.
적용 예시:
- pre-commit hook으로 코드 린트, 포맷, 시크릿 스캔
- PR 단계에서 SAST, SCA, 단위 테스트 실행
- 빌드 시 컨테이너 이미지 스캔
- IDE 플러그인으로 개발 중 실시간 피드백
효과: 결함을 일찍 발견할수록 수정 비용이 지수적으로 감소 (프로덕션 대비 10~100배 절감)
심화 질문
1. **감지**: 같은 코드에서 반복 실행 시 결과가 달라지는 테스트 식별
2. **격리**: 플레이키 테스트를 별도 test suite로 분리, continue-on-error 적용
3. **재시도**: jest의 retryTimes, pytest-rerunfailures 등으로 자동 재시도
4. **추적**: 플레이키 테스트 대시보드로 빈도, 패턴 분석
5. **근본 원인 해결**: 타이밍 이슈, 공유 상태, 외부 의존성 등 원인 제거
6. **정책**: 일정 기간 내 수정 안 되면 비활성화 또는 삭제
1. **멀티 스테이지 빌드**: 빌드 도구를 최종 이미지에서 제외
2. **레이어 캐싱 최적화**: 자주 변경되는 파일을 뒤에 COPY
3. **경량 베이스 이미지**: alpine, distroless 사용
4. **.dockerignore**: 불필요한 파일 제외
5. **BuildKit 사용**: 병렬 빌드, 캐시 마운트
6. **의존성 분리**: package.json을 먼저 복사하여 npm ci 캐싱
7. **Kaniko**: Docker 데몬 없이 빌드 (CI/CD 보안 향상)
1. **절대 Git에 커밋하지 않기**: gitleaks, detect-secrets로 pre-commit 검사
2. **OIDC 기반 인증**: 장기 시크릿 대신 임시 토큰 사용
3. **시크릿 매니저 활용**: AWS Secrets Manager, HashiCorp Vault, Doppler
4. **최소 권한 원칙**: 필요한 최소한의 권한만 부여
5. **시크릿 로테이션**: 정기적으로 시크릿 갱신 자동화
6. **감사 로그**: 시크릿 접근 기록 추적
7. **환경 분리**: dev/staging/prod 시크릿 분리
**Expand-Contract 패턴** 사용:
Phase 1 (Expand):
- 새 컬럼/테이블 추가
- 앱이 이전 스키마와 새 스키마 모두 호환되도록 코드 수정
- 새 스키마에도 데이터 쓰기 시작
Phase 2 (Migrate):
- 기존 데이터를 새 스키마로 마이그레이션 (백그라운드)
- 앱을 새 스키마만 사용하도록 전환
Phase 3 (Contract):
- 이전 컬럼/테이블 제거 (별도 배포)
- 이 단계만 롤백 불가
핵심: 각 단계가 독립적으로 롤백 가능해야 합니다.
1. **Supply Chain Security**: SBOM 생성, 이미지 서명(cosign), SLSA 준수
2. **시크릿 관리**: OIDC, Vault, 환경변수 최소화
3. **SAST/DAST/SCA**: Semgrep, Trivy, Dependabot 통합
4. **컨테이너 보안**: 비루트 사용자, distroless 베이스, 이미지 스캔
5. **정책 준수**: OPA/Kyverno로 배포 정책 강제
6. **접근 제어**: 최소 권한, 브랜치 보호 규칙
7. **감사**: 모든 배포 기록 추적, 변경 이력 유지
**GitHub Actions**:
- 장점: GitHub 네이티브 통합, SaaS로 유지보수 불필요, Marketplace 생태계, YAML 기반 간편 설정
- 단점: GitHub 종속성, 커스터마이징 한계, 복잡한 워크플로 관리 어려움
**Jenkins**:
- 장점: 완전한 자유도, 1800+ 플러그인, 자체 호스팅 제어, Groovy 스크립팅
- 단점: 높은 유지보수 비용, 복잡한 설정, 보안 패치 관리, 스케일링 어려움
선택 기준: 소규모/GitHub 중심 프로젝트는 Actions, 복잡한 엔터프라이즈/멀티 SCM은 Jenkins
Progressive Delivery는 새 버전을 점진적으로 배포하면서 자동화된 분석으로 안전성을 검증하는 방식입니다.
Argo Rollouts 워크플로:
1. 카나리 10% 트래픽 할당
2. AnalysisTemplate으로 성공률, 지연 시간 검증 (5분)
3. 통과 시 30%로 확대, 다시 분석
4. 60%, 100%로 점진적 확대
5. 분석 실패 시 자동 롤백
핵심 구성 요소:
- Rollout: 배포 전략 정의
- AnalysisTemplate: 검증 조건 정의 (Prometheus, Datadog 등)
- TrafficRouting: Istio, Nginx, ALB 등과 연동
1. **병렬 처리**: 독립적인 작업을 동시 실행
2. **테스트 분할**: 샤딩으로 테스트를 여러 러너에 분배
3. **캐싱**: 의존성, Docker 레이어, 빌드 결과 캐싱
4. **선택적 실행**: 변경된 파일에 따라 필요한 작업만 실행
5. **증분 빌드**: 전체 빌드 대신 변경된 부분만 빌드
6. **리소스 최적화**: 러너 크기, 동시성 제한 조정
7. **피드백 루프 단축**: 빠른 검사를 먼저, 느린 검사는 나중에
**장점:**
- 배포와 릴리스 분리: 코드를 배포하되, 기능은 나중에 활성화
- 빠른 롤백: 코드 롤백 없이 플래그만 끄면 됨
- 점진적 출시: 사용자 비율을 점진적으로 확대
- A/B 테스트: 기능별 실험 가능
**단점:**
- 기술 부채: 오래된 플래그 정리 필요
- 복잡성: 플래그 조합으로 테스트 경우의 수 증가
- 코드 가독성: 조건문 증가로 코드 복잡화
- 일관성: 사용자마다 다른 경험으로 버그 재현 어려움
1. **영향 범위 분석**: 변경된 파일로부터 영향받는 패키지만 빌드/테스트
2. **도구 활용**: Turborepo, Nx, Bazel 등으로 의존성 그래프 기반 빌드
3. **캐싱**: 원격 캐시(Turborepo Remote Cache)로 빌드 결과 공유
4. **선택적 배포**: 변경된 서비스만 배포
5. **병렬 처리**: 독립적인 패키지 동시 빌드/테스트
Turborepo 예시
turbo run build --filter=...[HEAD~1]
HEAD 이후 변경된 패키지와 의존 패키지만 빌드
13. 퀴즈
**정답: 하루에 여러 번 (On-demand, multiple deploys per day)**
Elite 팀은 하루에 여러 번 배포하면서도 변경 실패율 5% 미만, 복구 시간 1시간 미만을 유지합니다.
**정답: Contract 단계 (이전 컬럼/테이블 삭제)**
Expand(추가)와 Migrate(마이그레이션)는 롤백 가능하지만, Contract(삭제)는 데이터가 사라지므로 롤백이 불가능합니다. 따라서 Contract는 충분한 안정화 기간 후에 별도로 진행합니다.
**정답: Git 리포지토리**
GitOps에서 Git 리포지토리는 시스템의 원하는 상태(Desired State)를 정의하는 유일한 소스입니다. 클러스터의 실제 상태는 항상 Git에 선언된 상태와 일치해야 하며, ArgoCD 같은 도구가 이를 자동으로 감지하고 동기화합니다.
**정답: AnalysisTemplate에 정의된 메트릭 기준 (성공률, 지연 시간 등)**
Argo Rollouts의 AnalysisTemplate에서 Prometheus, Datadog 등의 메트릭을 쿼리하여 성공률이 기준(예: 95%) 이하이거나, P99 지연 시간이 기준을 초과하면 자동으로 롤백을 트리거합니다.
**정답: 소프트웨어에 포함된 모든 구성 요소(라이브러리, 의존성)의 목록을 제공하여 공급망 보안을 강화하는 것**
SBOM은 소프트웨어의 "재료 목록"으로, 취약점 발견 시 영향 범위를 빠르게 파악하고, 라이선스 컴플라이언스를 확인하며, 공급망 공격(예: Log4Shell)에 대한 대응을 돕습니다. Syft, Trivy 등의 도구로 자동 생성할 수 있습니다.
14. 참고 자료
공식 문서
- [GitHub Actions 공식 문서](https://docs.github.com/en/actions)
- [Jenkins Pipeline 문서](https://www.jenkins.io/doc/book/pipeline/)
- [GitLab CI/CD 문서](https://docs.gitlab.com/ee/ci/)
- [ArgoCD 공식 문서](https://argo-cd.readthedocs.io/)
- [Argo Rollouts 문서](https://argoproj.github.io/argo-rollouts/)
DORA 및 DevOps
- [DORA State of DevOps Report](https://dora.dev/)
- [Accelerate (DORA 저자 도서)](https://itrevolution.com/product/accelerate/)
- [Google Cloud DORA Metrics](https://cloud.google.com/blog/products/devops-sre/the-2024-dora-report-is-here)
보안
- [SLSA Supply Chain Security](https://slsa.dev/)
- [Sigstore / cosign 문서](https://docs.sigstore.dev/)
- [Trivy 취약점 스캐너](https://aquasecurity.github.io/trivy/)
- [Semgrep SAST](https://semgrep.dev/docs/)
배포 전략
- [Kubernetes Deployment Strategies](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/)
- [Flagger Progressive Delivery](https://flagger.app/)
- [LaunchDarkly Feature Flags](https://launchdarkly.com/)
도구 및 생태계
- [BuildKit 문서](https://docs.docker.com/build/buildkit/)
- [Kaniko 빌드 도구](https://github.com/GoogleContainerTools/kaniko)
- [Turborepo 모노레포](https://turbo.build/)
현재 단락 (1/926)
2025년, CI/CD는 더 이상 선택이 아닌 필수입니다. Google의 DORA(DevOps Research and Assessment) 보고서에 따르면, Elite 수준의 팀은...