IT Knowledge/DevOps/images/ci-cd-파이프라인-diagram.svg

CI/CD 파이프라인

📌 핵심 개념

CI (Continuous Integration): 코드 변경사항을 자동으로 빌드하고 테스트 CD (Continuous Deployment/Delivery): 검증된 코드를 자동으로 프로덕션에 배포

🎯 실제 사례로 이해하기

사례 1: 스타트업의 수동 배포 → CI/CD 전환

Before: 수동 배포 프로세스

# 금요일 오후 5시, 개발자의 배포 작업 (매번 2시간 소요)
 
# 1단계: 로컬에서 빌드
npm run build
# 오류 발생! 의존성 버전 불일치
# 30분 낭비...
 
# 2단계: 서버 접속
ssh user@production-server
cd /var/www/app
 
# 3단계: 코드 업데이트
git pull origin main
# 충돌 발생!
# 또 30분 낭비...
 
# 4단계: 수동 테스트
curl http://localhost:3000/health
# 500 에러... 롤백 시작
# 금요일 밤 10시...

After: GitHub Actions CI/CD

# .github/workflows/deploy.yml
name: CI/CD Pipeline
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
jobs:
  # 1단계: 코드 품질 검사
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: ESLint 검사
        run: |
          npm install
          npm run lint
 
  # 2단계: 테스트 실행
  test:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v3
      - name: 단위 테스트
        run: |
          npm install
          npm test
      - name: 통합 테스트
        run: npm run test:integration
 
  # 3단계: 빌드
  build:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v3
      - name: Docker 이미지 빌드
        run: |
          docker build -t myapp:${{ github.sha }} .
          docker tag myapp:${{ github.sha }} myapp:latest
 
      - name: 이미지 푸시
        run: |
          echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
          docker push myapp:${{ github.sha }}
          docker push myapp:latest
 
  # 4단계: 스테이징 배포
  deploy-staging:
    runs-on: ubuntu-latest
    needs: build
    environment: staging
    steps:
      - name: 스테이징 서버 배포
        run: |
          ssh deploy@staging-server "
            docker pull myapp:${{ github.sha }}
            docker stop myapp || true
            docker rm myapp || true
            docker run -d --name myapp -p 3000:3000 myapp:${{ github.sha }}
          "
 
      - name: 헬스 체크
        run: |
          sleep 10
          curl -f http://staging-server:3000/health || exit 1
 
  # 5단계: 프로덕션 배포 (수동 승인)
  deploy-production:
    runs-on: ubuntu-latest
    needs: deploy-staging
    environment: production
    steps:
      - name: 블루-그린 배포
        run: |
          # 그린 환경에 새 버전 배포
          kubectl set image deployment/myapp myapp=myapp:${{ github.sha }} --namespace=green
          kubectl rollout status deployment/myapp --namespace=green
 
      - name: 트래픽 전환
        run: |
          # 로드밸런서를 그린으로 전환
          kubectl patch service myapp -p '{"spec":{"selector":{"version":"green"}}}'
 
      - name: 모니터링
        run: |
          # 5분간 에러율 모니터링
          python scripts/monitor_error_rate.py --duration 300 --threshold 0.01

결과:

  • 배포 시간: 2시간 → 15분
  • 배포 성공률: 60% → 95%
  • 금요일 저녁 회식 가능! 🎉

사례 2: GitLab CI를 활용한 멀티 환경 파이프라인

시나리오: 쇼핑몰 개발팀

# .gitlab-ci.yml
variables:
  DOCKER_REGISTRY: registry.gitlab.com/myshop
 
stages:
  - validate
  - test
  - build
  - deploy-dev
  - deploy-staging
  - deploy-production
 
# 코드 검증
code-quality:
  stage: validate
  script:
    - npm run lint
    - npm run type-check
    - npm audit --audit-level=high
  only:
    - merge_requests
    - main
 
# 단위 테스트 + 커버리지
unit-test:
  stage: test
  script:
    - npm test -- --coverage
    - |
      # 커버리지 80% 미만이면 실패
      COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
      if (( $(echo "$COVERAGE < 80" | bc -l) )); then
        echo "커버리지 부족: $COVERAGE%"
        exit 1
      fi
  coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
 
# E2E 테스트
e2e-test:
  stage: test
  services:
    - postgres:14
    - redis:7
  script:
    - npm run test:e2e
  artifacts:
    when: on_failure
    paths:
      - cypress/screenshots
      - cypress/videos
 
# Docker 빌드 및 보안 스캔
build-image:
  stage: build
  script:
    # 멀티 스테이지 빌드로 이미지 크기 최적화
    - docker build -t $DOCKER_REGISTRY/app:$CI_COMMIT_SHA .
    - docker push $DOCKER_REGISTRY/app:$CI_COMMIT_SHA
 
    # 보안 취약점 스캔
    - trivy image --severity HIGH,CRITICAL $DOCKER_REGISTRY/app:$CI_COMMIT_SHA
  only:
    - main
 
# 개발 환경 자동 배포
deploy-dev:
  stage: deploy-dev
  script:
    - kubectl config use-context dev-cluster
    - helm upgrade --install myshop ./helm-chart
        --set image.tag=$CI_COMMIT_SHA
        --set environment=development
  environment:
    name: development
    url: https://dev.myshop.com
  only:
    - main
 
# 스테이징 환경 (자동 배포 + 성능 테스트)
deploy-staging:
  stage: deploy-staging
  script:
    - kubectl config use-context staging-cluster
    - helm upgrade --install myshop ./helm-chart
        --set image.tag=$CI_COMMIT_SHA
        --set environment=staging
        --set replicaCount=3
 
    # 부하 테스트
    - k6 run --vus 100 --duration 5m loadtest.js
  environment:
    name: staging
    url: https://staging.myshop.com
  only:
    - main
 
# 프로덕션 배포 (수동 승인 + 카나리 배포)
deploy-production:
  stage: deploy-production
  script:
    # 1단계: 10%의 트래픽만 새 버전으로
    - |
      kubectl config use-context production-cluster
      helm upgrade myshop ./helm-chart \
        --set image.tag=$CI_COMMIT_SHA \
        --set canary.enabled=true \
        --set canary.weight=10
 
    # 10분간 메트릭 확인
    - sleep 600
    - python scripts/check_metrics.py --duration 600
 
    # 2단계: 문제 없으면 100% 배포
    - |
      helm upgrade myshop ./helm-chart \
        --set image.tag=$CI_COMMIT_SHA \
        --set canary.enabled=false
 
    # Slack 알림
    - |
      curl -X POST $SLACK_WEBHOOK_URL -d '{
        "text": "🚀 프로덕션 배포 완료: '$CI_COMMIT_SHA'",
        "username": "GitLab CI"
      }'
  environment:
    name: production
    url: https://www.myshop.com
  when: manual  # 수동 승인 필요
  only:
    - main

사례 3: Jenkins 파이프라인 - 레거시 전환 사례

기업: 금융회사 (보안 중요)

// Jenkinsfile
pipeline {
  agent any
 
  // 환경 변수
  environment {
    APP_NAME = 'banking-api'
    DOCKER_REGISTRY = 'harbor.company.com'
    SONARQUBE_URL = 'https://sonarqube.company.com'
  }
 
  // 빌드 트리거
  triggers {
    // 깃 푸시 시 자동 실행
    pollSCM('H/5 * * * *')
  }
 
  stages {
    // 1. 체크아웃 및 변경사항 확인
    stage('Checkout') {
      steps {
        checkout scm
        script {
          env.GIT_COMMIT_MSG = sh(
            script: 'git log -1 --pretty=%B',
            returnStdout: true
          ).trim()
          echo "커밋 메시지: ${env.GIT_COMMIT_MSG}"
        }
      }
    }
 
    // 2. 보안 스캔 (SAST - Static Application Security Testing)
    stage('Security Scan') {
      parallel {
        stage('의존성 취약점 검사') {
          steps {
            sh 'npm audit --production --audit-level=high'
            sh 'snyk test --severity-threshold=high'
          }
        }
        stage('코드 보안 검사') {
          steps {
            // SonarQube로 코드 품질 및 보안 분석
            withSonarQubeEnv('SonarQube') {
              sh 'mvn sonar:sonar'
            }
            // Quality Gate 통과 확인
            timeout(time: 10, unit: 'MINUTES') {
              waitForQualityGate abortPipeline: true
            }
          }
        }
        stage('시크릿 스캔') {
          steps {
            // API 키, 패스워드 등 하드코딩 검사
            sh 'gitleaks detect --verbose'
          }
        }
      }
    }
 
    // 3. 빌드 및 테스트
    stage('Build & Test') {
      steps {
        sh 'npm install'
        sh 'npm run build'
 
        // 테스트 실행 및 결과 저장
        sh 'npm test -- --reporters=default --reporters=jest-junit'
        junit 'test-results/junit.xml'
 
        // 코드 커버리지 보고서
        publishHTML([
          allowMissing: false,
          alwaysLinkToLastBuild: true,
          keepAll: true,
          reportDir: 'coverage',
          reportFiles: 'index.html',
          reportName: 'Coverage Report'
        ])
      }
    }
 
    // 4. Docker 이미지 빌드
    stage('Build Docker Image') {
      steps {
        script {
          def imageTag = "${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER}"
          docker.build(imageTag)
 
          // 이미지 보안 스캔
          sh "trivy image --severity HIGH,CRITICAL ${imageTag}"
 
          // 레지스트리에 푸시
          docker.withRegistry("https://${DOCKER_REGISTRY}", 'harbor-credentials') {
            docker.image(imageTag).push()
            docker.image(imageTag).push('latest')
          }
        }
      }
    }
 
    // 5. 스테이징 배포 및 통합 테스트
    stage('Deploy to Staging') {
      steps {
        script {
          // Kubernetes에 배포
          sh """
            kubectl set image deployment/${APP_NAME} \
              ${APP_NAME}=${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER} \
              --namespace=staging
          """
 
          // 배포 완료 대기
          sh "kubectl rollout status deployment/${APP_NAME} --namespace=staging --timeout=5m"
 
          // 통합 테스트 실행
          sh 'npm run test:integration -- --env=staging'
 
          // 성능 테스트
          sh 'artillery run performance-test.yml --target https://staging.api.company.com'
        }
      }
    }
 
    // 6. 프로덕션 배포 승인
    stage('Approve Production') {
      when {
        branch 'main'
      }
      steps {
        script {
          // 담당자에게 이메일 발송
          emailext(
            subject: "배포 승인 요청: ${APP_NAME} v${BUILD_NUMBER}",
            body: """
              커밋: ${env.GIT_COMMIT_MSG}
              스테이징 URL: https://staging.api.company.com
              Jenkins: ${BUILD_URL}
 
              승인 후 프로덕션 배포가 진행됩니다.
            """,
            to: 'devops-team@company.com'
          )
 
          // 수동 승인 대기
          input message: '프로덕션 배포를 진행하시겠습니까?',
                ok: '배포 시작',
                submitter: 'devops-team'
        }
      }
    }
 
    // 7. 프로덕션 배포 (블루-그린)
    stage('Deploy to Production') {
      when {
        branch 'main'
      }
      steps {
        script {
          // 블루-그린 배포 구현
          def currentColor = sh(
            script: "kubectl get service ${APP_NAME} -n production -o jsonpath='{.spec.selector.color}'",
            returnStdout: true
          ).trim()
 
          def newColor = (currentColor == 'blue') ? 'green' : 'blue'
 
          // 새로운 색상 환경에 배포
          sh """
            kubectl set image deployment/${APP_NAME}-${newColor} \
              ${APP_NAME}=${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER} \
              --namespace=production
          """
 
          sh "kubectl rollout status deployment/${APP_NAME}-${newColor} --namespace=production --timeout=10m"
 
          // 스모크 테스트
          sh "curl -f http://${APP_NAME}-${newColor}.production.svc.cluster.local/health"
 
          // 트래픽 전환
          sh "kubectl patch service ${APP_NAME} -n production -p '{\"spec\":{\"selector\":{\"color\":\"${newColor}\"}}}}'"
 
          echo "트래픽이 ${currentColor}에서 ${newColor}로 전환되었습니다"
        }
      }
    }
  }
 
  // 빌드 후 처리
  post {
    success {
      slackSend(
        color: 'good',
        message: "✅ 배포 성공: ${APP_NAME} v${BUILD_NUMBER}\n커밋: ${env.GIT_COMMIT_MSG}"
      )
    }
    failure {
      slackSend(
        color: 'danger',
        message: "❌ 배포 실패: ${APP_NAME} v${BUILD_NUMBER}\n로그: ${BUILD_URL}console"
      )
 
      // 실패 시 자동 롤백
      script {
        if (env.STAGE_NAME == 'Deploy to Production') {
          sh "kubectl rollout undo deployment/${APP_NAME} --namespace=production"
        }
      }
    }
    always {
      // 빌드 아티팩트 정리
      cleanWs()
    }
  }
}

🔧 CI/CD 파이프라인 구성 요소

1. 빌드 트리거

# 예시: 다양한 트리거 조건
on:
  # PR 생성/업데이트 시
  pull_request:
    branches: [main, develop]
 
  # 특정 브랜치 푸시 시
  push:
    branches: [main]
    paths:
      - 'src/**'
      - 'package.json'
 
  # 스케줄 (매일 새벽 2시 빌드)
  schedule:
    - cron: '0 2 * * *'
 
  # 수동 트리거
  workflow_dispatch:
    inputs:
      environment:
        description: '배포 환경'
        required: true
        default: 'staging'
        type: choice
        options:
          - development
          - staging
          - production

2. 테스트 자동화

// jest.config.js - 테스트 전략
module.exports = {
  // 단위 테스트 (빠름, 많이)
  testMatch: ['**/__tests__/**/*.test.js'],
  collectCoverageFrom: ['src/**/*.js'],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },
 
  // 통합 테스트
  testEnvironment: 'node',
  setupFilesAfterEnv: ['./test/setup.js']
};
# E2E 테스트 (Playwright)
- name: E2E 테스트
  run: |
    npx playwright test
  env:
    BASE_URL: https://staging.myapp.com

3. 배포 전략 비교

전략장점단점사용 사례
롤링 배포간단, 리소스 효율적두 버전 동시 실행일반적인 웹 서비스
블루-그린빠른 롤백, 무중단2배 리소스 필요금융, 커머스
카나리점진적 검증복잡한 구성대규모 서비스
A/B 테스트비즈니스 검증 가능가장 복잡기능 실험

4. 실전 배포 스크립트

#!/bin/bash
# deploy.sh - 안전한 배포 스크립트
 
set -e  # 에러 발생 시 중단
 
# 색상 출력
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'
 
log_success() {
  echo -e "${GREEN}✓ $1${NC}"
}
 
log_error() {
  echo -e "${RED}✗ $1${NC}"
}
 
# 1. 사전 검증
echo "🔍 배포 전 검증..."
if ! kubectl get nodes &> /dev/null; then
  log_error "Kubernetes 클러스터 접근 불가"
  exit 1
fi
log_success "클러스터 연결 확인"
 
# 2. 백업
echo "💾 현재 버전 백업..."
CURRENT_VERSION=$(kubectl get deployment myapp -o jsonpath='{.spec.template.spec.containers[0].image}')
echo $CURRENT_VERSION > .last-deployed-version
log_success "백업 완료: $CURRENT_VERSION"
 
# 3. 배포
echo "🚀 배포 시작..."
kubectl set image deployment/myapp myapp=$NEW_IMAGE
 
# 4. 헬스 체크
echo "🏥 헬스 체크..."
for i in {1..30}; do
  if kubectl rollout status deployment/myapp --timeout=10s &> /dev/null; then
    log_success "배포 성공!"
    exit 0
  fi
  echo "대기 중... ($i/30)"
  sleep 10
done
 
# 5. 실패 시 롤백
log_error "배포 타임아웃! 롤백 시작..."
kubectl set image deployment/myapp myapp=$CURRENT_VERSION
kubectl rollout status deployment/myapp --timeout=5m
log_success "롤백 완료"
exit 1

📊 CI/CD 메트릭 모니터링

파이프라인 성능 추적

# metrics.py - 배포 메트릭 수집
from prometheus_client import Counter, Histogram, Gauge
 
# 배포 횟수
deploy_total = Counter('deploy_total', '총 배포 횟수', ['environment', 'status'])
 
# 배포 소요 시간
deploy_duration = Histogram('deploy_duration_seconds', '배포 소요 시간', ['environment'])
 
# 파이프라인 단계별 시간
stage_duration = Histogram('pipeline_stage_duration_seconds', '단계별 시간', ['stage'])
 
# 현재 배포 중인 수
deploying_now = Gauge('deploying_now', '현재 배포 중')
 
# 사용 예시
with deploy_duration.labels(environment='production').time():
  with deploying_now.track_inprogress():
    try:
      # 배포 로직
      deploy_application()
      deploy_total.labels(environment='production', status='success').inc()
    except Exception as e:
      deploy_total.labels(environment='production', status='failure').inc()
      raise

💡 CI/CD 베스트 프랙티스

1. 파이프라인 최적화

# ❌ 느린 파이프라인 (순차 실행)
jobs:
  test-unit:
    runs-on: ubuntu-latest
    steps: [...]
 
  test-integration:
    needs: test-unit
    runs-on: ubuntu-latest
    steps: [...]
 
# ✅ 빠른 파이프라인 (병렬 실행)
jobs:
  test:
    strategy:
      matrix:
        type: [unit, integration, e2e]
    runs-on: ubuntu-latest
    steps:
      - run: npm run test:${{ matrix.type }}

2. 캐싱 활용

# 의존성 캐싱으로 시간 절약
- name: 캐시 복원
  uses: actions/cache@v3
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
 
# Docker 레이어 캐싱
- name: Docker 빌드 (캐시 활용)
  uses: docker/build-push-action@v4
  with:
    context: .
    cache-from: type=registry,ref=myapp:cache
    cache-to: type=registry,ref=myapp:cache,mode=max

3. 시크릿 관리

# GitHub Secrets 사용
- name: AWS 배포
  env:
    AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
    AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
  run: |
    aws s3 sync ./dist s3://my-bucket

🔗 참고 자료