11강 60분

CI/CD 파이프라인 구축

마이크로서비스를 위한 CI/CD 파이프라인을 구축하고 GitHub Actions, Docker, Kubernetes를 활용한 자동화 배포를 구현합니다.

CI/CD 파이프라인 구축

학습 목표

  • CI/CD의 개념과 필요성
  • Docker를 활용한 컨테이너화
  • GitHub Actions로 CI 파이프라인 구축
  • Kubernetes로 CD (무중단 배포)

CI/CD란?

Continuous Integration (CI)

코드 변경 시 자동으로:

  1. 빌드
  2. 테스트
  3. 코드 품질 검사

Continuous Deployment (CD)

테스트 통과 시 자동으로:

  1. 컨테이너 이미지 빌드
  2. Registry에 푸시
  3. 프로덕션 배포
┌─────────┐    ┌──────┐    ┌──────┐    ┌────────┐    ┌──────────┐
│  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로 메트릭 수집, 알림 설정 필수