이종관
글 목록으로

Blue/Green 배포 (2): Kubernetes에서의 구현

순수 Kubernetes 리소스, Argo Rollouts, Istio를 활용한 Blue/Green 배포 구현과 DB 마이그레이션 전략

2026년 1월 27일·13 min read·
infra
deployment
blue-green
kubernetes

TL;DR

  • Kubernetes에서 Blue/Green은 Service selector만으로도 가능하지만 운영 자동화가 어렵다.
  • 프로덕션에서는 Argo Rollouts로 전환/검증/롤백 자동화를 우선 고려하는 것이 안전하다.
  • Istio를 결합하면 점진 전환, 트래픽 미러링, 헤더 기반 라우팅까지 확장할 수 있다.

Kubernetes에서의 Blue/Green 개요

핵심은 "트래픽 라우팅 제어 방식"을 먼저 선택하는 것이다. 운영 성숙도에 따라 Service selector -> Argo Rollouts -> Istio 순으로 고도화하는 접근이 현실적이다.

Kubernetes는 선언적 배포 관리를 제공하지만, 기본적으로는 Rolling Update 전략을 사용한다. Blue/Green 배포를 구현하려면 추가적인 설정이 필요하다.

구현 방법 비교

  • Service Selector: 복잡도 낮음, 자동화/롤백 수동, 추천 상황은 학습·소규모
  • Argo Rollouts: 복잡도 중간, 자동화/롤백 자동, 추천 상황은 프로덕션
  • Istio: 복잡도 높음, 자동화/롤백 자동, 추천 상황은 대규모·복잡 라우팅
  • Flagger: 복잡도 중간, 자동화/롤백 자동, 추천 상황은 GitOps 환경

순수 Kubernetes로 구현

이 방식은 학습과 빠른 PoC에 가장 적합하다. 다만 전환 검증, 실패 감지, 롤백 트리거를 직접 스크립트로 관리해야 한다.

가장 기본적인 방법으로, Service의 selector를 변경하여 트래픽을 전환한다.

아키텍처

Blue Deployment

yaml
# blue-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-blue
  labels:
    app: my-app
    color: blue
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
      color: blue
  template:
    metadata:
      labels:
        app: my-app
        color: blue
        version: v1.0.0
    spec:
      containers:
      - name: app
        image: my-app:1.0.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 20

Green Deployment

yaml
# green-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-green
  labels:
    app: my-app
    color: green
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
      color: green
  template:
    metadata:
      labels:
        app: my-app
        color: green
        version: v1.1.0
    spec:
      containers:
      - name: app
        image: my-app:1.1.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 20

Service (트래픽 라우터)

yaml
# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-app
spec:
  type: ClusterIP
  selector:
    app: my-app
    color: blue  # 이 값을 green으로 변경하여 전환
  ports:
    - name: http
      port: 80
      targetPort: 8080

전환 스크립트

bash
#!/bin/bash
# switch-traffic.sh
 
NAMESPACE=${NAMESPACE:-default}
SERVICE_NAME=${SERVICE_NAME:-my-app}
TARGET_COLOR=${1:-green}
 
echo "Switching traffic to $TARGET_COLOR..."
 
# 현재 상태 확인
CURRENT_COLOR=$(kubectl get svc $SERVICE_NAME -n $NAMESPACE \
  -o jsonpath='{.spec.selector.color}')
echo "Current: $CURRENT_COLOR -> Target: $TARGET_COLOR"
 
# Green Deployment가 Ready인지 확인
READY_REPLICAS=$(kubectl get deployment my-app-$TARGET_COLOR -n $NAMESPACE \
  -o jsonpath='{.status.readyReplicas}')
DESIRED_REPLICAS=$(kubectl get deployment my-app-$TARGET_COLOR -n $NAMESPACE \
  -o jsonpath='{.spec.replicas}')
 
if [ "$READY_REPLICAS" != "$DESIRED_REPLICAS" ]; then
  echo "Error: Target deployment not ready ($READY_REPLICAS/$DESIRED_REPLICAS)"
  exit 1
fi
 
# 트래픽 전환
kubectl patch svc $SERVICE_NAME -n $NAMESPACE \
  -p "{\"spec\":{\"selector\":{\"color\":\"$TARGET_COLOR\"}}}"
 
echo "Traffic switched to $TARGET_COLOR successfully!"
 
# 전환 후 상태 확인
kubectl get svc $SERVICE_NAME -n $NAMESPACE -o wide
kubectl get endpoints $SERVICE_NAME -n $NAMESPACE

수동 구현의 한계

  • 휴먼 에러: selector 변경 시 오타 가능성
  • 자동화 어려움: CI/CD 파이프라인에서 상태 관리 복잡
  • 롤백 자동화 없음: 문제 발생 시 수동 개입 필요
  • 모니터링 연동 없음: 자동 롤백 트리거 불가

Argo Rollouts로 자동화

선언형 리소스로 배포 상태를 관리하고, 승인 지점과 분석 단계를 내장할 수 있다. 수동 스위치 대비 휴먼 에러를 줄이고, 문제 발생 시 자동 롤백 정책을 구현하기 쉽다.

Argo Rollouts는 Kubernetes용 Progressive Delivery Controller로, Blue/Green과 Canary 배포를 선언적으로 관리한다.

설치

bash
# Argo Rollouts 설치
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts \
  -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
 
# kubectl 플러그인 설치 (선택)
brew install argoproj/tap/kubectl-argo-rollouts  # macOS
# 또는
curl -LO https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64
chmod +x kubectl-argo-rollouts-linux-amd64
sudo mv kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts

Rollout 리소스

yaml
# rollout.yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: my-app
spec:
  replicas: 4
  revisionHistoryLimit: 2
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: app
        image: my-app:1.1.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
  strategy:
    blueGreen:
      # 현재 운영 트래픽을 받는 서비스
      activeService: my-app-active
      # 새 버전 미리보기용 서비스
      previewService: my-app-preview
      # 자동 승인 비활성화 (수동 승인 필요)
      autoPromotionEnabled: false
      # 전환 후 이전 ReplicaSet 유지 시간
      scaleDownDelaySeconds: 30
      # Preview ReplicaSet 최소 준비 시간
      previewReplicaCount: 2
      # 자동 롤백 (Analysis 실패 시)
      autoPromotionSeconds: 0  # 0 = 자동 승인 비활성화

Services 구성

yaml
# services.yaml
---
# Active Service (현재 운영)
apiVersion: v1
kind: Service
metadata:
  name: my-app-active
spec:
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080
---
# Preview Service (새 버전 테스트용)
apiVersion: v1
kind: Service
metadata:
  name: my-app-preview
spec:
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080

배포 워크플로우

bash
# 1. 현재 상태 확인
kubectl argo rollouts get rollout my-app -w
 
# 2. 새 버전 배포 (이미지 변경)
kubectl argo rollouts set image my-app app=my-app:1.2.0
 
# 3. Preview 환경 테스트
# my-app-preview 서비스로 내부 테스트 수행
 
# 4. 트래픽 전환 승인
kubectl argo rollouts promote my-app
 
# 5. 문제 발생 시 롤백
kubectl argo rollouts abort my-app
# 또는 이전 버전으로
kubectl argo rollouts undo my-app

대시보드에서 상태 확인

bash
# Argo Rollouts 대시보드 실행
kubectl argo rollouts dashboard
 
# 브라우저에서 http://localhost:3100 접속

Analysis를 통한 자동 검증

yaml
# analysis-template.yaml
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
spec:
  args:
  - name: service-name
  metrics:
  - name: success-rate
    interval: 30s
    count: 5
    successCondition: result[0] >= 0.95
    failureLimit: 3
    provider:
      prometheus:
        address: http://prometheus.monitoring:9090
        query: |
          sum(rate(http_requests_total{
            service="{{args.service-name}}",
            status=~"2.."
          }[5m])) /
          sum(rate(http_requests_total{
            service="{{args.service-name}}"
          }[5m]))
yaml
# rollout-with-analysis.yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: my-app
spec:
  # ... (이전 설정 동일)
  strategy:
    blueGreen:
      activeService: my-app-active
      previewService: my-app-preview
      autoPromotionEnabled: false
      # 전환 전 자동 분석
      prePromotionAnalysis:
        templates:
        - templateName: success-rate
        args:
        - name: service-name
          value: my-app-preview
      # 전환 후 자동 분석 (롤백 트리거)
      postPromotionAnalysis:
        templates:
        - templateName: success-rate
        args:
        - name: service-name
          value: my-app-active

Istio Service Mesh 연동

트래픽을 비율 단위로 제어하거나, 헤더 기반 분기와 미러링 테스트를 추가할 수 있다. 고급 라우팅이 필요한 대규모 환경에서 Blue/Green 운영 유연성이 크게 향상된다.

Istio를 사용하면 더 세밀한 트래픽 제어가 가능하다.

아키텍처

DestinationRule

yaml
# destination-rule.yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: my-app
spec:
  host: my-app
  subsets:
  - name: blue
    labels:
      version: v1.0.0
  - name: green
    labels:
      version: v1.1.0

VirtualService (Blue/Green)

yaml
# virtual-service.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: my-app
spec:
  hosts:
  - my-app
  - my-app.example.com
  gateways:
  - my-app-gateway
  http:
  # 헤더 기반 라우팅 (테스트용)
  - match:
    - headers:
        x-canary:
          exact: "true"
    route:
    - destination:
        host: my-app
        subset: green
        port:
          number: 80
  # 기본 트래픽 (Blue로)
  - route:
    - destination:
        host: my-app
        subset: blue
        port:
          number: 80
      weight: 100
    - destination:
        host: my-app
        subset: green
        port:
          number: 80
      weight: 0

점진적 전환 (Istio + Blue/Green 하이브리드)

yaml
# 단계별 전환 예시
# Step 1: 0% Green
- destination:
    host: my-app
    subset: blue
  weight: 100
- destination:
    host: my-app
    subset: green
  weight: 0
 
# Step 2: 10% Green (테스트)
- destination:
    host: my-app
    subset: blue
  weight: 90
- destination:
    host: my-app
    subset: green
  weight: 10
 
# Step 3: 100% Green (전환 완료)
- destination:
    host: my-app
    subset: blue
  weight: 0
- destination:
    host: my-app
    subset: green
  weight: 100

트래픽 미러링 (Shadow Testing)

yaml
# 실제 트래픽을 Green에 복제하여 테스트 (응답은 무시)
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: my-app
spec:
  hosts:
  - my-app
  http:
  - route:
    - destination:
        host: my-app
        subset: blue
      weight: 100
    mirror:
      host: my-app
      subset: green
    mirrorPercentage:
      value: 100.0

DB 스키마 마이그레이션 전략

Blue/Green에서 DB는 두 버전이 동시에 접근하므로 하위 호환이 핵심이다. Expand -> Migrate -> Contract 순서를 지키면 안전하게 전환하고 롤백 가능성을 확보할 수 있다.

Blue/Green 배포에서 가장 어려운 부분은 DB 스키마 변경이다. 두 버전이 동시에 같은 DB를 사용하기 때문이다.

Expand & Contract 패턴

단계별 마이그레이션 예시

Phase 1: Expand (확장)

sql
-- 새 컬럼 추가 (nullable)
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);
 
-- 기본값 설정 (기존 데이터 호환)
UPDATE users SET full_name = user_name WHERE full_name IS NULL;
python
# v2 애플리케이션: 두 컬럼 모두 지원
class User:
    def get_display_name(self):
        # 새 컬럼 우선, 없으면 기존 컬럼 사용
        return self.full_name or self.user_name
 
    def save(self):
        # 두 컬럼 모두 업데이트 (하위 호환)
        self.full_name = self.display_name
        self.user_name = self.display_name  # v1 호환

Phase 2: Migrate (마이그레이션)

python
# 배치 마이그레이션 스크립트
def migrate_user_names():
    users = User.objects.filter(full_name__isnull=True)
    for batch in chunked(users, 1000):
        for user in batch:
            user.full_name = user.user_name
        User.objects.bulk_update(batch, ['full_name'])

Phase 3: Contract (축소)

sql
-- v1이 완전히 제거된 후에만 실행
-- 충분한 모니터링 기간 후 (예: 2주)
ALTER TABLE users DROP COLUMN user_name;

마이그레이션 체크리스트

  1. 새 컬럼 추가: Blue(v1) 무시, Green(v2) 읽기/쓰기, 롤백 가능 ✅
  2. 데이터 복사: Blue(v1) 무시, Green(v2) 읽기/쓰기, 롤백 가능 ✅
  3. v2로 전환: Blue(v1) 비활성, Green(v2) 활성, 롤백 가능 ✅
  4. v1 제거: Blue(v1) 삭제, Green(v2) 활성, 롤백 가능 ⚠️
  5. 이전 컬럼 삭제: Blue(v1) 비활성, Green(v2) 활성, 롤백 가능 ❌

중요: Phase 5는 롤백이 불가능하므로, 충분한 모니터링 기간(최소 1-2주)을 거친 후 실행해야 한다.