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
- production2. 테스트 자동화
// 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.com3. 배포 전략 비교
| 전략 | 장점 | 단점 | 사용 사례 |
|---|---|---|---|
| 롤링 배포 | 간단, 리소스 효율적 | 두 버전 동시 실행 | 일반적인 웹 서비스 |
| 블루-그린 | 빠른 롤백, 무중단 | 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=max3. 시크릿 관리
# 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