Skip to content
Published on

CI/CD 베스트 프랙티스 2025: 팀을 위한 파이프라인 설계, 자동화, 보안까지

Authors

들어가며

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가지 핵심 지표입니다.

지표EliteHighMediumLow
배포 빈도하루 여러 번주 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)는 테스트와 보안을 개발 초기 단계로 당기는 전략입니다.

전통적 접근:
CodeBuildTestSecurityDeployMonitor
                              ↑ 여기서 문제 발견

시프트 레프트:
Code + Test + SecurityBuildDeployMonitor
↑ 여기서 문제 발견 (비용 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 ActionsJenkinsGitLab CICircleCI
호스팅SaaS/Self-hostedSelf-hostedSaaS/Self-hostedSaaS
설정 방식YAMLGroovy/YAMLYAMLYAML
생태계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:201.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
                                            ┌────▼────┐
K8sCluster                                            └─────────┘

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 시스템
import { featureFlags } from './feature-flags';

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 실패 분석 자동화

# 빌드 실패 자동 분류 스크립트
import re
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. 면접 질문 모음

기본 개념

Q1. CI와 CD의 차이점을 설명하세요.

**CI(Continuous Integration)**는 개발자들이 코드 변경을 자주 메인 브랜치에 통합하는 관행입니다. 각 통합은 자동화된 빌드와 테스트로 검증합니다.

CD는 두 가지 의미가 있습니다:

  • Continuous Delivery: 코드가 항상 배포 가능한 상태를 유지. 프로덕션 배포는 수동 승인.
  • Continuous Deployment: 모든 변경이 자동으로 프로덕션에 배포. 수동 개입 없음.

핵심 차이: CI는 "통합"에, CD는 "전달/배포"에 초점. CI 없이 CD는 불가능하지만, CI만 하고 CD는 안 할 수 있습니다.

Q2. DORA 메트릭 4가지를 설명하세요.
  1. 배포 빈도(Deployment Frequency): 프로덕션에 얼마나 자주 배포하는가
  2. 리드 타임(Lead Time for Changes): 커밋에서 프로덕션 배포까지 걸리는 시간
  3. 변경 실패율(Change Failure Rate): 배포 중 실패하거나 롤백이 필요한 비율
  4. 복구 시간(MTTR - Mean Time to Recovery): 장애 발생 후 복구까지 걸리는 시간

Elite 팀: 하루 여러 번 배포, 1시간 미만 리드 타임, 5% 미만 실패율, 1시간 미만 복구

Q3. GitOps의 핵심 원칙을 설명하세요.
  1. 선언적 기술(Declarative): 시스템 상태를 선언적으로 정의
  2. 버전 관리(Versioned): Git을 단일 진실의 소스로 사용
  3. 자동 적용(Automated): 승인된 변경이 자동으로 시스템에 적용
  4. 자가 치유(Self-Healing): 실제 상태가 선언된 상태와 다르면 자동 복구

장점: 감사 추적, 롤백 용이, PR 기반 변경 관리, 재현 가능한 환경

Q4. Blue-Green 배포와 카나리 배포의 차이를 설명하세요.

Blue-Green: 두 개의 동일한 환경(Blue/Green)을 운영. 새 버전을 Green에 배포 후 트래픽을 한 번에 전환. 롤백은 Blue로 즉시 전환.

  • 장점: 즉시 롤백, 간단한 구현
  • 단점: 리소스 2배 필요, 데이터베이스 동기화 복잡

Canary: 새 버전을 소수(1~10%)에게 먼저 배포. 메트릭 분석 후 점진적 확대.

  • 장점: 위험 최소화, 실제 트래픽으로 검증
  • 단점: 구현 복잡, 모니터링 필수
Q5. 시프트 레프트(Shift Left)란 무엇인가요?

테스트와 보안을 개발 라이프사이클의 왼쪽(초기 단계)으로 이동시키는 전략입니다.

적용 예시:

  • pre-commit hook으로 코드 린트, 포맷, 시크릿 스캔
  • PR 단계에서 SAST, SCA, 단위 테스트 실행
  • 빌드 시 컨테이너 이미지 스캔
  • IDE 플러그인으로 개발 중 실시간 피드백

효과: 결함을 일찍 발견할수록 수정 비용이 지수적으로 감소 (프로덕션 대비 10~100배 절감)

심화 질문

Q6. 플레이키 테스트(Flaky Test)를 어떻게 관리하나요?
  1. 감지: 같은 코드에서 반복 실행 시 결과가 달라지는 테스트 식별
  2. 격리: 플레이키 테스트를 별도 test suite로 분리, continue-on-error 적용
  3. 재시도: jest의 retryTimes, pytest-rerunfailures 등으로 자동 재시도
  4. 추적: 플레이키 테스트 대시보드로 빈도, 패턴 분석
  5. 근본 원인 해결: 타이밍 이슈, 공유 상태, 외부 의존성 등 원인 제거
  6. 정책: 일정 기간 내 수정 안 되면 비활성화 또는 삭제
Q7. Docker 이미지 빌드를 어떻게 최적화하나요?
  1. 멀티 스테이지 빌드: 빌드 도구를 최종 이미지에서 제외
  2. 레이어 캐싱 최적화: 자주 변경되는 파일을 뒤에 COPY
  3. 경량 베이스 이미지: alpine, distroless 사용
  4. .dockerignore: 불필요한 파일 제외
  5. BuildKit 사용: 병렬 빌드, 캐시 마운트
  6. 의존성 분리: package.json을 먼저 복사하여 npm ci 캐싱
  7. Kaniko: Docker 데몬 없이 빌드 (CI/CD 보안 향상)
Q8. 시크릿 관리 모범 사례를 설명하세요.
  1. 절대 Git에 커밋하지 않기: gitleaks, detect-secrets로 pre-commit 검사
  2. OIDC 기반 인증: 장기 시크릿 대신 임시 토큰 사용
  3. 시크릿 매니저 활용: AWS Secrets Manager, HashiCorp Vault, Doppler
  4. 최소 권한 원칙: 필요한 최소한의 권한만 부여
  5. 시크릿 로테이션: 정기적으로 시크릿 갱신 자동화
  6. 감사 로그: 시크릿 접근 기록 추적
  7. 환경 분리: dev/staging/prod 시크릿 분리
Q9. 데이터베이스 마이그레이션을 롤백 안전하게 수행하는 방법은?

Expand-Contract 패턴 사용:

Phase 1 (Expand):

  • 새 컬럼/테이블 추가
  • 앱이 이전 스키마와 새 스키마 모두 호환되도록 코드 수정
  • 새 스키마에도 데이터 쓰기 시작

Phase 2 (Migrate):

  • 기존 데이터를 새 스키마로 마이그레이션 (백그라운드)
  • 앱을 새 스키마만 사용하도록 전환

Phase 3 (Contract):

  • 이전 컬럼/테이블 제거 (별도 배포)
  • 이 단계만 롤백 불가

핵심: 각 단계가 독립적으로 롤백 가능해야 합니다.

Q10. CI/CD 파이프라인의 보안을 어떻게 강화하나요?
  1. Supply Chain Security: SBOM 생성, 이미지 서명(cosign), SLSA 준수
  2. 시크릿 관리: OIDC, Vault, 환경변수 최소화
  3. SAST/DAST/SCA: Semgrep, Trivy, Dependabot 통합
  4. 컨테이너 보안: 비루트 사용자, distroless 베이스, 이미지 스캔
  5. 정책 준수: OPA/Kyverno로 배포 정책 강제
  6. 접근 제어: 최소 권한, 브랜치 보호 규칙
  7. 감사: 모든 배포 기록 추적, 변경 이력 유지
Q11. GitHub Actions와 Jenkins의 장단점을 비교하세요.

GitHub Actions:

  • 장점: GitHub 네이티브 통합, SaaS로 유지보수 불필요, Marketplace 생태계, YAML 기반 간편 설정
  • 단점: GitHub 종속성, 커스터마이징 한계, 복잡한 워크플로 관리 어려움

Jenkins:

  • 장점: 완전한 자유도, 1800+ 플러그인, 자체 호스팅 제어, Groovy 스크립팅
  • 단점: 높은 유지보수 비용, 복잡한 설정, 보안 패치 관리, 스케일링 어려움

선택 기준: 소규모/GitHub 중심 프로젝트는 Actions, 복잡한 엔터프라이즈/멀티 SCM은 Jenkins

Q12. Argo Rollouts의 Progressive Delivery를 설명하세요.

Progressive Delivery는 새 버전을 점진적으로 배포하면서 자동화된 분석으로 안전성을 검증하는 방식입니다.

Argo Rollouts 워크플로:

  1. 카나리 10% 트래픽 할당
  2. AnalysisTemplate으로 성공률, 지연 시간 검증 (5분)
  3. 통과 시 30%로 확대, 다시 분석
  4. 60%, 100%로 점진적 확대
  5. 분석 실패 시 자동 롤백

핵심 구성 요소:

  • Rollout: 배포 전략 정의
  • AnalysisTemplate: 검증 조건 정의 (Prometheus, Datadog 등)
  • TrafficRouting: Istio, Nginx, ALB 등과 연동
Q13. 파이프라인 성능을 어떻게 최적화하나요?
  1. 병렬 처리: 독립적인 작업을 동시 실행
  2. 테스트 분할: 샤딩으로 테스트를 여러 러너에 분배
  3. 캐싱: 의존성, Docker 레이어, 빌드 결과 캐싱
  4. 선택적 실행: 변경된 파일에 따라 필요한 작업만 실행
  5. 증분 빌드: 전체 빌드 대신 변경된 부분만 빌드
  6. 리소스 최적화: 러너 크기, 동시성 제한 조정
  7. 피드백 루프 단축: 빠른 검사를 먼저, 느린 검사는 나중에
Q14. Feature Flag 기반 배포의 장단점은?

장점:

  • 배포와 릴리스 분리: 코드를 배포하되, 기능은 나중에 활성화
  • 빠른 롤백: 코드 롤백 없이 플래그만 끄면 됨
  • 점진적 출시: 사용자 비율을 점진적으로 확대
  • A/B 테스트: 기능별 실험 가능

단점:

  • 기술 부채: 오래된 플래그 정리 필요
  • 복잡성: 플래그 조합으로 테스트 경우의 수 증가
  • 코드 가독성: 조건문 증가로 코드 복잡화
  • 일관성: 사용자마다 다른 경험으로 버그 재현 어려움
Q15. 모노레포에서의 CI/CD 전략을 설명하세요.
  1. 영향 범위 분석: 변경된 파일로부터 영향받는 패키지만 빌드/테스트
  2. 도구 활용: Turborepo, Nx, Bazel 등으로 의존성 그래프 기반 빌드
  3. 캐싱: 원격 캐시(Turborepo Remote Cache)로 빌드 결과 공유
  4. 선택적 배포: 변경된 서비스만 배포
  5. 병렬 처리: 독립적인 패키지 동시 빌드/테스트
# Turborepo 예시
turbo run build --filter=...[HEAD~1]
# HEAD 이후 변경된 패키지와 의존 패키지만 빌드

13. 퀴즈

Q1. DORA 메트릭에서 Elite 팀의 배포 빈도는?

정답: 하루에 여러 번 (On-demand, multiple deploys per day)

Elite 팀은 하루에 여러 번 배포하면서도 변경 실패율 5% 미만, 복구 시간 1시간 미만을 유지합니다.

Q2. Expand-Contract 패턴에서 롤백이 불가능한 단계는?

정답: Contract 단계 (이전 컬럼/테이블 삭제)

Expand(추가)와 Migrate(마이그레이션)는 롤백 가능하지만, Contract(삭제)는 데이터가 사라지므로 롤백이 불가능합니다. 따라서 Contract는 충분한 안정화 기간 후에 별도로 진행합니다.

Q3. GitOps에서 "단일 진실의 소스"란 무엇인가요?

정답: Git 리포지토리

GitOps에서 Git 리포지토리는 시스템의 원하는 상태(Desired State)를 정의하는 유일한 소스입니다. 클러스터의 실제 상태는 항상 Git에 선언된 상태와 일치해야 하며, ArgoCD 같은 도구가 이를 자동으로 감지하고 동기화합니다.

Q4. 카나리 배포에서 자동 롤백을 트리거하는 기준은 무엇인가요?

정답: AnalysisTemplate에 정의된 메트릭 기준 (성공률, 지연 시간 등)

Argo Rollouts의 AnalysisTemplate에서 Prometheus, Datadog 등의 메트릭을 쿼리하여 성공률이 기준(예: 95%) 이하이거나, P99 지연 시간이 기준을 초과하면 자동으로 롤백을 트리거합니다.

Q5. SBOM(Software Bill of Materials)의 목적은 무엇인가요?

정답: 소프트웨어에 포함된 모든 구성 요소(라이브러리, 의존성)의 목록을 제공하여 공급망 보안을 강화하는 것

SBOM은 소프트웨어의 "재료 목록"으로, 취약점 발견 시 영향 범위를 빠르게 파악하고, 라이선스 컴플라이언스를 확인하며, 공급망 공격(예: Log4Shell)에 대한 대응을 돕습니다. Syft, Trivy 등의 도구로 자동 생성할 수 있습니다.


14. 참고 자료

공식 문서

DORA 및 DevOps

보안

배포 전략

도구 및 생태계