CI/CD 파이프라인 구축
학습 목표
- CI/CD의 개념과 필요성
- Docker를 활용한 컨테이너화
- GitHub Actions로 CI 파이프라인 구축
- Kubernetes로 CD (무중단 배포)
CI/CD란?
Continuous Integration (CI)
코드 변경 시 자동으로:
- 빌드
- 테스트
- 코드 품질 검사
Continuous Deployment (CD)
테스트 통과 시 자동으로:
- 컨테이너 이미지 빌드
- Registry에 푸시
- 프로덕션 배포
┌─────────┐ ┌──────┐ ┌──────┐ ┌────────┐ ┌──────────┐
│ Push │───►│ Build│───►│ Test │───►│ Deploy │───►│Production│
│ Code │ │ │ │ │ │ │ │ │
└─────────┘ └──────┘ └──────┘ └────────┘ └──────────┘
Docker를 활용한 컨테이너화
Dockerfile 작성
# Multi-stage build로 최적화
FROM node:20-alpine AS builder
WORKDIR /app
# Dependencies 설치
COPY package*.json ./
RUN npm ci --only=production
# 소스 복사 및 빌드
COPY . .
RUN npm run build
# Production 이미지
FROM node:20-alpine
WORKDIR /app
# 보안: non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Built artifacts만 복사
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --chown=nodejs:nodejs package*.json ./
USER nodejs
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node healthcheck.js
CMD ["node", "dist/server.js"]
.dockerignore
node_modules
npm-debug.log
.git
.env
.DS_Store
*.md
tests
.github
coverage
Docker Compose (로컬 개발)
version: '3.8'
services:
product-service:
build:
context: ./services/product
dockerfile: Dockerfile
ports:
- "3001:3000"
environment:
- DATABASE_URL=postgresql://postgres:password@postgres:5432/products
- REDIS_URL=redis://redis:6379
depends_on:
- postgres
- redis
order-service:
build:
context: ./services/order
dockerfile: Dockerfile
ports:
- "3002:3000"
environment:
- DATABASE_URL=postgresql://postgres:password@postgres:5432/orders
- PRODUCT_SERVICE_URL=http://product-service:3000
depends_on:
- postgres
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:
GitHub Actions CI 파이프라인
Workflow 정의
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
NODE_VERSION: '20'
jobs:
test:
name: Test
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run type check
run: npm run type-check
- name: Run tests
run: npm test
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/test
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
build:
name: Build Docker Image
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: myorg/product-service
tags: |
type=ref,event=branch
type=sha,prefix={{branch}}-
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
security-scan:
name: Security Scan
needs: build
runs-on: ubuntu-latest
steps:
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: myorg/product-service:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
Kubernetes CD 파이프라인
Kubernetes Manifests
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: product-service
labels:
app: product
version: v1
spec:
replicas: 3
selector:
matchLabels:
app: product
template:
metadata:
labels:
app: product
version: v1
spec:
containers:
- name: product
image: myorg/product-service:latest
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: product-secrets
key: database-url
- name: REDIS_URL
valueFrom:
configMapKeyRef:
name: product-config
key: redis-url
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/ready
port: 3000
initialDelaySeconds: 10
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: product-service
spec:
selector:
app: product
ports:
- port: 80
targetPort: 3000
type: ClusterIP
---
apiVersion: v1
kind: ConfigMap
metadata:
name: product-config
data:
redis-url: "redis://redis:6379"
log-level: "info"
---
apiVersion: v1
kind: Secret
metadata:
name: product-secrets
type: Opaque
stringData:
database-url: "postgresql://user:pass@postgres:5432/products"
CD Workflow
# .github/workflows/cd.yml
name: CD Pipeline
on:
push:
branches: [ main ]
env:
CLUSTER_NAME: production-cluster
REGION: us-central1
NAMESPACE: production
jobs:
deploy:
name: Deploy to Kubernetes
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup kubectl
uses: azure/setup-kubectl@v3
- name: Configure kubectl
run: |
echo "${{ secrets.KUBECONFIG }}" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig
- name: Update deployment image
run: |
kubectl set image deployment/product-service \
product=myorg/product-service:${{ github.sha }} \
-n ${{ env.NAMESPACE }}
- name: Wait for rollout
run: |
kubectl rollout status deployment/product-service \
-n ${{ env.NAMESPACE }} \
--timeout=5m
- name: Verify deployment
run: |
kubectl get pods -n ${{ env.NAMESPACE }} -l app=product
- name: Rollback on failure
if: failure()
run: |
kubectl rollout undo deployment/product-service \
-n ${{ env.NAMESPACE }}
무중단 배포 전략
1. Rolling Update (기본)
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 추가로 생성할 Pod 수
maxUnavailable: 0 # 동시 다운 가능 Pod 수
동작:
V1: [Pod1] [Pod2] [Pod3]
↓
V1: [Pod1] [Pod2] [Pod3] [Pod4-V2] # maxSurge
↓
V1: [Pod2] [Pod3] [Pod4-V2] # Pod1 종료
↓
V1: [Pod2] [Pod3] [Pod4-V2] [Pod5-V2]
↓
V2: [Pod4-V2] [Pod5-V2] [Pod6-V2] # 완료
2. Blue-Green Deployment
# Blue (현재 버전)
apiVersion: apps/v1
kind: Deployment
metadata:
name: product-blue
spec:
replicas: 3
template:
metadata:
labels:
app: product
version: blue
spec:
containers:
- name: product
image: myorg/product-service:v1.0
---
# Green (새 버전)
apiVersion: apps/v1
kind: Deployment
metadata:
name: product-green
spec:
replicas: 3
template:
metadata:
labels:
app: product
version: green
spec:
containers:
- name: product
image: myorg/product-service:v2.0
---
# Service (트래픽 전환)
apiVersion: v1
kind: Service
metadata:
name: product-service
spec:
selector:
app: product
version: blue # ← green으로 변경하여 전환
ports:
- port: 80
targetPort: 3000
전환 명령:
# Green으로 전환
kubectl patch service product-service -p '{"spec":{"selector":{"version":"green"}}}'
# 문제 발생 시 Blue로 롤백
kubectl patch service product-service -p '{"spec":{"selector":{"version":"blue"}}}'
3. Canary Deployment (Istio)
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: product-canary
spec:
hosts:
- product-service
http:
- match:
- headers:
user-type:
exact: beta
route:
- destination:
host: product-service
subset: v2
weight: 100
- route:
- destination:
host: product-service
subset: v1
weight: 95
- destination:
host: product-service
subset: v2
weight: 5 # 5% 트래픽만 v2로
모니터링과 알림
Prometheus 메트릭
import { Counter, Histogram, register } from 'prom-client';
// 메트릭 정의
const httpRequestsTotal = new Counter({
name: 'http_requests_total',
help: 'Total HTTP requests',
labelNames: ['method', 'path', 'status'],
});
const httpRequestDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration',
labelNames: ['method', 'path'],
buckets: [0.1, 0.5, 1, 2, 5],
});
// Middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
httpRequestsTotal.inc({
method: req.method,
path: req.path,
status: res.statusCode,
});
httpRequestDuration.observe(
{ method: req.method, path: req.path },
duration
);
});
next();
});
// Metrics 엔드포인트
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
Slack 알림
# .github/workflows/notify.yml
- name: Notify Slack on Success
if: success()
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
payload: |
{
"text": "✅ Deployment Successful",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Product Service* deployed to production\n*Version:* ${{ github.sha }}\n*Branch:* ${{ github.ref }}"
}
}
]
}
- name: Notify Slack on Failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
payload: |
{
"text": "❌ Deployment Failed",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Product Service* deployment failed\n*Version:* ${{ github.sha }}\n*Logs:* ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
}
]
}
실전 체크리스트
CI 단계
- Unit tests
- Integration tests
- Lint & Type check
- Security scan (Snyk, Trivy)
- Code coverage (>80%)
Docker 이미지
- Multi-stage build
- Non-root user
- Health check 구현
- .dockerignore 설정
- 이미지 크기 최적화 (<500MB)
Kubernetes
- Resource limits 설정
- Liveness/Readiness probes
- ConfigMap/Secret으로 설정 분리
- HPA (Auto Scaling) 설정
- PDB (Pod Disruption Budget)
배포 전략
- Rolling Update 설정
- Canary 배포 고려
- Rollback 계획
- Smoke test 자동화
모니터링
- Prometheus 메트릭
- 로그 중앙화 (ELK, Loki)
- APM (Application Performance Monitoring)
- Slack/Email 알림
핵심 정리
- CI/CD로 배포 자동화 및 안정성 확보
- Docker Multi-stage build로 이미지 최적화
- GitHub Actions로 간단한 CI/CD 구축
- Kubernetes로 무중단 배포
- Rolling Update, Blue-Green, Canary 전략 선택
- Prometheus로 메트릭 수집, 알림 설정 필수