Skip to content
Published on

DevOps Progressive Delivery: Argo Rollouts 실전 운영

Authors
DevOps Progressive Delivery: Argo Rollouts 실전 운영

Progressive Delivery의 핵심 전제

Progressive Delivery는 "배포와 릴리스를 분리한다"는 아이디어에서 출발한다. 코드를 프로덕션에 배포하되, 사용자에게 노출하는 트래픽 비율을 점진적으로 늘리면서 자동 분석으로 안전성을 검증한다. 문제가 감지되면 전체 롤아웃 전에 자동으로 중단한다.

Argo Rollouts는 Kubernetes 네이티브 Progressive Delivery 컨트롤러다. 표준 Deployment를 대체하는 Rollout CRD를 제공하며, 카나리(Canary)와 블루-그린(Blue-Green) 두 가지 전략을 지원한다. Argo Rollouts 1.7에서는 AnalysisRun에 TTL 전략과 크로스 레퍼런스 기능이 추가되어 대규모 운영이 한결 수월해졌다.

Argo Rollouts 설치와 초기 설정

# Argo Rollouts 컨트롤러 설치 (Helm)
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update

helm install argo-rollouts argo/argo-rollouts \
  --namespace argo-rollouts \
  --create-namespace \
  --set controller.replicas=2 \
  --set dashboard.enabled=true \
  --set dashboard.service.type=ClusterIP \
  --version 2.40.6

# kubectl 플러그인 설치
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

# 설치 확인
kubectl argo rollouts version
# argo-rollouts: v1.7.2

카나리 배포: 완전한 Rollout 매니페스트

기본 Rollout 리소스

# rollout.yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: order-service
  namespace: production
  labels:
    app: order-service
    team: commerce
spec:
  replicas: 10
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
      annotations:
        prometheus.io/scrape: 'true'
        prometheus.io/port: '8080'
        prometheus.io/path: '/metrics'
    spec:
      containers:
        - name: order-service
          image: ghcr.io/my-org/order-service:v2.5.0
          ports:
            - containerPort: 8080
              name: http
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: '1'
              memory: 1Gi
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 15
            periodSeconds: 10
          env:
            - name: APP_ENV
              value: production
            - name: DD_SERVICE
              value: order-service
  strategy:
    canary:
      # 카나리와 안정 버전에 각각 다른 서비스 연결
      canaryService: order-service-canary
      stableService: order-service-stable
      # Istio/NGINX 트래픽 라우팅 설정
      trafficRouting:
        nginx:
          stableIngress: order-service-ingress
          additionalIngressAnnotations:
            canary-by-header: X-Canary
      # 단계별 롤아웃
      steps:
        # 1단계: 5% 트래픽, 2분간 자동 분석
        - setWeight: 5
        - analysis:
            templates:
              - templateName: success-rate-and-latency
            args:
              - name: service-name
                value: order-service
              - name: canary-hash
                valueFrom:
                  podTemplateHashValue: Latest
        - pause:
            duration: 2m

        # 2단계: 20% 트래픽, 5분간 관찰
        - setWeight: 20
        - pause:
            duration: 5m

        # 3단계: 50% 트래픽, 수동 승인 (영업시간에만)
        - setWeight: 50
        - pause: {} # 수동 승인 필요

        # 4단계: 전체 전환
        - setWeight: 100

      # 자동 롤백 조건
      abortScaleDownDelaySeconds: 30
      scaleDownDelaySeconds: 30
      scaleDownDelayRevisionLimit: 2

카나리/스테이블 서비스 분리

# services.yaml
apiVersion: v1
kind: Service
metadata:
  name: order-service-stable
  namespace: production
spec:
  selector:
    app: order-service
  ports:
    - port: 80
      targetPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: order-service-canary
  namespace: production
spec:
  selector:
    app: order-service
  ports:
    - port: 80
      targetPort: 8080
---
# NGINX Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: order-service-ingress
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/canary: 'true'
    nginx.ingress.kubernetes.io/canary-weight: '0'
spec:
  ingressClassName: nginx
  rules:
    - host: order.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: order-service-stable
                port:
                  number: 80

AnalysisTemplate: 자동 판단의 핵심

AnalysisTemplate은 카나리 배포의 성공/실패를 자동으로 판단하는 규칙이다. Prometheus, Datadog, New Relic 등 다양한 메트릭 소스를 지원한다.

Prometheus 기반 분석

# analysis-template.yaml
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate-and-latency
  namespace: production
spec:
  args:
    - name: service-name
    - name: canary-hash
  metrics:
    # 지표 1: 성공률 (99% 이상이어야 통과)
    - name: success-rate
      interval: 30s
      count: 10 # 총 10회 측정
      failureLimit: 2 # 2회까지 실패 허용
      successCondition: result[0] >= 0.99
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            sum(rate(
              http_requests_total{
                service="{{args.service-name}}",
                pod_template_hash="{{args.canary-hash}}",
                status=~"2.."
              }[2m]
            )) /
            sum(rate(
              http_requests_total{
                service="{{args.service-name}}",
                pod_template_hash="{{args.canary-hash}}"
              }[2m]
            ))

    # 지표 2: P95 지연시간 (300ms 이하여야 통과)
    - name: latency-p95
      interval: 30s
      count: 10
      failureLimit: 2
      successCondition: result[0] <= 300
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            histogram_quantile(0.95,
              sum(rate(
                http_request_duration_milliseconds_bucket{
                  service="{{args.service-name}}",
                  pod_template_hash="{{args.canary-hash}}"
                }[2m]
              )) by (le)
            )

    # 지표 3: 에러 로그 비율 (0.5% 이하여야 통과)
    - name: error-log-rate
      interval: 60s
      count: 5
      failureLimit: 1
      successCondition: result[0] <= 0.005
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            sum(rate(
              log_messages_total{
                service="{{args.service-name}}",
                pod_template_hash="{{args.canary-hash}}",
                level="error"
              }[3m]
            )) /
            sum(rate(
              log_messages_total{
                service="{{args.service-name}}",
                pod_template_hash="{{args.canary-hash}}"
              }[3m]
            ))

Datadog 기반 분석

# analysis-datadog.yaml
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: datadog-analysis
  namespace: production
spec:
  args:
    - name: service-name
    - name: dd-api-key
      valueFrom:
        secretKeyRef:
          name: datadog-credentials
          key: api-key
    - name: dd-app-key
      valueFrom:
        secretKeyRef:
          name: datadog-credentials
          key: app-key
  metrics:
    - name: error-rate
      interval: 60s
      count: 5
      failureLimit: 1
      successCondition: 'default(result, 0) < 0.01'
      provider:
        datadog:
          apiVersion: v2
          interval: 5m
          query: |
            sum:trace.http.request.errors{
              service:{{args.service-name}},
              version:canary
            }.as_rate() /
            sum:trace.http.request.hits{
              service:{{args.service-name}},
              version:canary
            }.as_rate()

블루-그린 배포 설정

카나리가 점진적 트래픽 전환이라면, 블루-그린은 전체 트래픽을 한번에 전환하되, 이전 버전을 대기 상태로 유지하여 즉시 롤백을 가능하게 한다.

# blue-green-rollout.yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: payment-service
  namespace: production
spec:
  replicas: 6
  selector:
    matchLabels:
      app: payment-service
  template:
    metadata:
      labels:
        app: payment-service
    spec:
      containers:
        - name: payment-service
          image: ghcr.io/my-org/payment-service:v3.1.0
          ports:
            - containerPort: 8080
  strategy:
    blueGreen:
      activeService: payment-service-active
      previewService: payment-service-preview
      # 전환 전 자동 분석 실행
      prePromotionAnalysis:
        templates:
          - templateName: success-rate-and-latency
        args:
          - name: service-name
            value: payment-service
      # 전환 후 안정성 분석
      postPromotionAnalysis:
        templates:
          - templateName: success-rate-and-latency
        args:
          - name: service-name
            value: payment-service
      # 이전 버전 유지 시간 (롤백 대비)
      scaleDownDelaySeconds: 600 # 10분간 이전 버전 유지
      # 수동 승인 필요 여부
      autoPromotionEnabled: false

운영 명령어 레퍼런스

# === 롤아웃 상태 확인 ===
kubectl argo rollouts get rollout order-service -n production
# 실시간 모니터링 (watch 모드)
kubectl argo rollouts get rollout order-service -n production -w

# === 수동 승인 (pause: {} 단계에서) ===
kubectl argo rollouts promote order-service -n production

# === 전체 승인 (나머지 단계 모두 건너뛰기) ===
kubectl argo rollouts promote order-service -n production --full

# === 롤백 (즉시 이전 버전으로) ===
kubectl argo rollouts abort order-service -n production

# === 재시도 (abort 후 같은 버전 재배포) ===
kubectl argo rollouts retry rollout order-service -n production

# === 특정 리비전으로 롤백 ===
kubectl argo rollouts undo order-service -n production --to-revision=3

# === AnalysisRun 결과 확인 ===
kubectl argo rollouts get rollout order-service -n production
# 또는
kubectl get analysisrun -n production -l rollouts-pod-template-hash=abc1234

# === 대시보드 접속 ===
kubectl argo rollouts dashboard -n argo-rollouts
# http://localhost:3100 에서 접속

트러블슈팅: 실제 장애 시나리오

시나리오 1: AnalysisRun이 Inconclusive 상태에서 멈춤

kubectl get analysisrun -n production
# NAME                              STATUS         AGE
# order-service-abc1234-1           Inconclusive   15m

원인: Prometheus 쿼리가 데이터를 반환하지 않아 NaN이 되었다. 카나리 트래픽이 너무 적어 메트릭이 수집되지 않았다.

# 해결: successCondition에 default() 함수 사용
successCondition: 'default(result[0], 1) >= 0.99'
# NaN인 경우 1(100% 성공)로 간주

# 또는 최소 트래픽을 보장하는 설정
steps:
  - setWeight: 5
  - pause:
      duration: 3m # 메트릭 수집을 위한 최소 대기
  - analysis:
      templates:
        - templateName: success-rate-and-latency

시나리오 2: 롤백 후 ReplicaSet이 정리되지 않음

kubectl get rs -n production | grep order-service
# order-service-abc1234   10   10   10   2d
# order-service-def5678   0    0    0    5d    # 오래된 RS
# order-service-ghi9012   0    0    0    12d   # 더 오래된 RS
# 해결: revisionHistoryLimit 설정
spec:
  revisionHistoryLimit: 3  # 최근 3개 리비전만 보관

# 수동 정리
kubectl argo rollouts rollout order-service -n production --revision 1

시나리오 3: 트래픽 라우팅이 실제로 작동하지 않음

NGINX Ingress Controller를 사용할 때 카나리 트래픽이 설정한 비율로 분배되지 않는 경우.

# Ingress annotation 확인
kubectl get ingress order-service-ingress -n production -o yaml

# NGINX Ingress Controller 로그 확인
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller --tail=50

# 흔한 원인: Ingress에 canary annotation이 누락
# nginx.ingress.kubernetes.io/canary: "true" 가 있는지 확인

# 검증: 100회 요청하여 실제 분배 비율 확인
for i in $(seq 1 100); do
  curl -s -o /dev/null -w "%{http_code}" https://order.example.com/health
done | sort | uniq -c

시나리오 4: 수동 승인(pause) 단계에서 장기간 방치

# 해결: pause에 타임아웃 추가
steps:
  - setWeight: 50
  - pause:
      duration: 4h # 4시간 후 자동 승인

# 또는 Slack 알림으로 수동 승인 유도
# ArgoCD Notification과 연동
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  annotations:
    notifications.argoproj.io/subscribe.on-rollout-step-completed.slack: platform-alerts

카나리 vs 블루-그린 선택 가이드

기준카나리블루-그린
트래픽 제어 정밀도높음 (1% 단위)없음 (전체 전환)
롤백 속도중간 (abort -> 리라우팅)매우 빠름 (서비스 전환)
리소스 비용낮음 (추가 Pod 최소)높음 (2배 Pod 필요)
분석 정확도높음 (실 트래픽 기반)제한적 (preview 환경)
적합한 서비스대부분의 웹 서비스결제, 금융 등 무중단 필수
데이터 스키마 변경복잡 (두 버전 공존)상대적으로 단순
구현 복잡도높음 (트래픽 라우팅 필요)중간

일반 규칙: 대부분의 서비스에는 카나리를 사용하되, 결제나 인증처럼 한 건의 오류도 비용이 큰 서비스에는 블루-그린을 사용한다.

AnalysisTemplate 재사용: 크로스 레퍼런스

Argo Rollouts 1.7에서 도입된 AnalysisTemplate 크로스 레퍼런스 기능으로, 공통 메트릭을 ClusterAnalysisTemplate에 정의하고 서비스별로 참조할 수 있다.

# ClusterAnalysisTemplate: 조직 공통 기준
apiVersion: argoproj.io/v1alpha1
kind: ClusterAnalysisTemplate
metadata:
  name: org-standard-analysis
spec:
  args:
    - name: service-name
  metrics:
    - name: success-rate
      interval: 30s
      count: 10
      failureLimit: 2
      successCondition: 'default(result[0], 1) >= 0.99'
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            sum(rate(http_requests_total{service="{{args.service-name}}",status=~"2.."}[2m]))
            / sum(rate(http_requests_total{service="{{args.service-name}}"}[2m]))
---
# 서비스별 추가 분석 (공통 + 커스텀)
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: order-service-analysis
  namespace: production
spec:
  args:
    - name: service-name
  metrics:
    - name: order-completion-rate
      interval: 60s
      count: 5
      failureLimit: 1
      successCondition: result[0] >= 0.95
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            sum(rate(order_completed_total{service="{{args.service-name}}"}[5m]))
            / sum(rate(order_created_total{service="{{args.service-name}}"}[5m]))

퀴즈

Q1. Argo Rollouts의 Rollout CRD가 표준 Deployment를 대체하는 이유는? 정답: ||표준 Deployment는 RollingUpdate 전략만 지원하며, 트래픽 비율 제어와 메트릭 기반 자동 롤백 기능이 없다. Rollout CRD는 카나리/블루-그린 전략, AnalysisRun을 통한 자동 판단, 트래픽 라우팅 제어를 네이티브로 제공한다.||

Q2. AnalysisRun에서 Inconclusive 상태가 발생하는 가장 흔한 원인은? 정답: ||Prometheus 쿼리가 NaN을 반환하는 경우다. 카나리 트래픽이 너무 적어 메트릭이 수집되지 않거나, 쿼리의 label selector가 카나리 Pod과 일치하지 않을 때 발생한다. successCondition에 default() 함수를 사용하여 NaN 처리를 해야 한다.||

Q3. 카나리 배포에서 setWeight: 5와 analysis 단계의 순서가 중요한 이유는? 정답: ||setWeight가 먼저 실행되어 카나리 Pod에 5% 트래픽이 흐른 후에 analysis가 시작되어야 실제 트래픽 기반 메트릭을 수집할 수 있다. analysis를 먼저 실행하면 트래픽이 없어 메트릭이 비어있는 상태에서 판단하게 된다.||

Q4. 블루-그린 배포에서 scaleDownDelaySeconds를 600으로 설정하는 이유는? 정답: ||전환 후 10분간 이전 버전 Pod을 유지하여 문제 발생 시 즉시 롤백할 수 있게 한다. 이 시간이 지나면 이전 버전 Pod이 종료되므로, 롤백 시 새로 Pod을 생성해야 하여 시간이 더 걸린다. 서비스 중요도에 따라 적절한 값을 설정한다.||

Q5. NGINX Ingress Controller와 Argo Rollouts를 연동할 때 canary-by-header 설정의 용도는?

정답: ||특정 HTTP 헤더(X-Canary)를 포함한 요청을 카나리 Pod으로 라우팅한다. QA 팀이나 내부 테스터가 헤더를 추가하여 카나리 버전을 직접 테스트할 수 있게 해준다. 비율 기반 라우팅(setWeight)과 병행하여 사용한다.||

Q6. ClusterAnalysisTemplate과 AnalysisTemplate의 차이와 사용 전략은? 정답: ||ClusterAnalysisTemplate은 클러스터 전체에서 사용할 수 있는 네임스페이스 독립적인 분석 템플릿이다. 조직 공통 SLO 기준(성공률 99%, P95 지연 300ms 이하 등)을 ClusterAnalysisTemplate으로 정의하고, 서비스별 비즈니스 메트릭은 네임스페이스별 AnalysisTemplate으로 추가하는 것이 권장된다.||

Q7. abort 후 retry와 undo --to-revision의 차이는? 정답: ||retry는 abort된 동일 버전을 다시 롤아웃한다(코드 수정 없이 일시적 문제였을 때). undo --to-revision은 특정 이전 리비전의 Pod 템플릿으로 되돌린다(코드에 문제가 있어 이전 버전으로 복귀할 때). retry는 같은 이미지를 재배포하고, undo는 이전 이미지로 전환한다.||

참고 자료