grep

DevOps

EKS + ALB 환경에서 Argo Rollouts 503 에러 없는 카나리 배포 적용기

Kuiper여기어때

2026년 4월 24일

원문에서 보기 ↗

1. 들어가며

안녕하세요. 숙박플랫폼개선TF팀 카이퍼입니다.

배포는 늘 긴장의 순간입니다. 특히 운영 환경에서 실시간 트래픽을 받고 있는 서비스의 새 버전을 내보낼 때, “이번에도 무사히 넘어가겠지” 라는 막연한 기대에 기대고 계시다면 이미 위험 신호일 수 있습니다.

저희 팀은 EKS + ALB 환경에서 운영되는 서비스를 개발하고 있으며, Kubernetes 인프라 자체는 SRE팀과 DevOps팀에서 구축·관리하고 있습니다. Argo Rollouts 역시 초기부터 SRE팀에서 구축해주신 환경을 활용하고 있습니다.

배경: Deployment 롤링 → Blue/Green → 그리고 다시

기존에는 Kubernetes 기본 Deployment의 롤링 업데이트를 사용하고 있었습니다. 간편하지만, 운영 관점에서 아쉬운 점이 있었습니다.

이러한 한계를 보완하기 위해 과거에 Argo Rollouts의 Blue/Green 전략을 적용했었습니다. Blue/Green은 새 버전(Green)을 미리 띄워 preview로 검증한 뒤 전환할 수 있고, 문제 시 즉각 롤백이 가능합니다. 하지만 Promote 시점에 약 30초간 503이 발생하는 문제가 확인되어, 결국 롤링 업데이트로 원복할 수밖에 없었습니다.

이후 카나리 배포가 필요한 요건이 생겼습니다. 카나리는 Blue/Green에서는 불가능했던 트래픽 비율을 단계적으로 제어 (20% → 50% → 80% → 100%)할 수 있어, 신규 버전의 영향을 최소화하면서 점진적으로 전환할 수 있습니다. 다만 과거 Blue/Green에서 겪었던 503 문제가 카나리에서도 동일하게 발생할 수 있기에, 이를 근본적으로 해결하는 방법을 찾게 되었고, 그 답이 Canary PingPong 전략이었습니다.

이 글에서는 왜 503이 발생하는지 원인을 분석하고, PingPong으로 어떻게 해결했는지, 그리고 실제 적용 과정과 테스트 결과를 공유하겠습니다.

2. Argo Rollouts 배포 전략 톺아보기

Argo Rollouts가 제공하는 대표 배포 전략 중, Blue/Green과 기본 Canary가 어떤 흐름으로 동작하는지 4단계(Phase)로 살펴보겠습니다.

Blue/Green

두 환경(Blue/Green)을 병렬로 유지하다가, Promote 시점에 active 서비스를 순간 절체하는 방식입니다.

Canary

ALB ForwardConfig의 weight를 단계적으로 증가시키며 점진적으로 트래픽을 전환하는 방식입니다.

두 전략 모두 Phase 1, 2, 4는 크게 다르지 않습니다. 핵심 차이는 Phase 3(Promote) 에서 발생하며, 이 부분이 503 문제의 원인이 됩니다. 다음 장에서 자세히 살펴보겠습니다.

3. EKS + ALB에서의 503 문제

저희가 직접 겪은 내용입니다. Blue/Green과 기본 Canary 전략을 차례로 적용해봤는데, 두 전략 모두 Promote 시점에 약 30초간 503이 발생했습니다.

Blue/Green: Promote 시 503 발생

Blue/Green에서 Promote가 수행되면 다음과 같은 일이 벌어집니다.

activeService selector v1→v2 교체
  → EndpointSlice 변경
    → ALB Controller가 감지
      → TG-Blue의 target을 v2 Pod IP로 재등록
        → v2 Pod: initial(unhealthy) 상태
          → 헬스체크 통과까지 ~30초
            → 이 구간에서 503 발생

문제의 핵심은, Service selector가 변경되는 순간 ALB Target Group의 target이 재등록 된다는 점입니다. 새로 등록된 target은 ALB 헬스체크를 통과하기 전까지 initial 상태이며, 이 동안 해당 target으로 라우팅된 요청은 503으로 응답됩니다.

기본 Canary: Promote 완료 시 동일한 503

“Canary는 점진적 전환이니까 괜찮지 않을까?” 생각하실 수 있습니다. 하지만 기본 Canary도 Promote 완료 시점에 동일한 문제가 발생합니다.

weight가 100%에 도달한 후 Promote가 수행되면, stableService의 selector가 v1→v2로 교체됩니다. 이 순간 Blue/Green과 정확히 같은 연쇄 반응이 일어납니다.

stableService selector v1→v2 교체
  → TG-Stable target 재등록
    → v2 Pod: initial(unhealthy)
      → weight 변경과 race condition
        → 503 발생

원인: ALB Pod Readiness Gate의 구조적 한계

여기서 “Readiness Gate가 있으니까 Pod가 ready 되기 전에는 트래픽이 안 가는 거 아닌가?” 라고 생각하실 수 있습니다. 맞는 말이지만, Argo Rollouts와 함께 사용할 때 구조적인 한계가 있습니다.

AWS Load Balancer Controller는 Pod 생성 시점에 해당 Pod가 Service selector와 매칭되는 경우에만 readiness gate(target-health.elbv2.k8s.aws)를 주입합니다. 한번 생성된 Pod에는 readiness gate를 추가할 수 없습니다.

Blue/Green과 기본 Canary 모두, Promote 시점에 이미 running 중인 Pod에 대해 Service selector가 변경됩니다. 이 Pod들은 생성 시점에는 해당 Service와 매칭되지 않았으므로, readiness gate가 주입되어 있지 않습니다. 결과적으로 readiness gate에 의한 보호를 받지 못합니다.

이 문제는 저희만 겪은 것이 아닙니다. 커뮤니티에서도 광범위하게 보고되고 있습니다.

정리하면, EKS + ALB 환경에서 Blue/Green이든 기본 Canary든, Promote 시점에 Service selector를 변경하는 모든 전략은 구조적으로 503 위험을 안고 있습니다.

4. 해결: Canary PingPong 전략

그렇다면 어떻게 해야 할까요? 답은 Argo Rollouts v1.2에서 도입된 Canary PingPong 전략입니다.

PingPong의 동작 흐름

두 개의 Target Group(TG-A, TG-B)을 고정하고, 배포마다 역할(stable/canary)을 교대하는 방식입니다.

핵심 원리

PingPong의 핵심은 단 하나입니다.

Promote 시 Service selector를 변경하지 않는다.

기존 전략과 PingPong의 Promote 동작을 비교해보겠습니다.

Blue/Green 및 기본 Canary (503 발생):

Promote
  → Service selector 변경 (v1 hash → v2 hash)
    → EndpointSlice 변경
      → ALB Controller → AWS target deregister/register
        → 새 target 헬스체크 대기 (~30초)
          → 503 발생

PingPong (503 없음):

Promote
  → ALB ForwardConfig weight만 교체 (TG-A:100→0, TG-B:0→100)
    → 끝.

PingPong은 두 개의 Target Group ARN을 고정하고, weight 숫자만 swap합니다. Service selector 변경이 없으므로 EndpointSlice 변경도 없고, AWS target 재등록도 발생하지 않습니다.

왜 503이 발생하지 않는가

canary 단계(Phase 2)에서 TG-B에 등록된 v2 Pod들은 이미 ALB 헬스체크를 통과한 healthy 상태입니다. Promote 시에는 이 이미 healthy인 target들에게 weight만 100%로 올려주는 것이므로, 트래픽 유실 구간이 존재하지 않습니다.

또한 Pod 생성 시점에 pingService/pongService와 이미 매칭되어 있으므로, readiness gate도 정상적으로 주입됩니다. 앞서 살펴본 구조적 한계가 원천적으로 해소됩니다.

5. PingPong 적용 가이드

실제로 PingPong을 적용하기 위해 필요한 설정들을 정리하겠습니다.

3개 Service 구성

PingPong은 3개의 Service가 필요합니다. 3개 모두 동일한 spec이며, 이름만 다릅니다. Argo Rollouts 컨트롤러가 각 Service의 selector에 rollouts-pod-template-hash를 동적으로 주입하여 Pod 매핑을 관리합니다.

# 1. Root Service — Ingress가 바라보는 진입점
apiVersion: v1
kind: Service
metadata:
  name: my-app
spec:
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080

# 2. Ping Service - 배포마다 stable/canary 교대
apiVersion: v1
kind: Service
metadata:
  name: my-app-ping
spec:
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080
# 3. Pong Service - 배포마다 stable/canary 교대
apiVersion: v1
kind: Service
metadata:
  name: my-app-pong
spec:
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080

Rollout Strategy 설정

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: my-app
spec:
  strategy:
    canary:
      # ── pingPong ──
      # 두 Service가 배포마다 stable↔canary 역할을 교대합니다.
      # selector 변경 없이 weight만 swap하여 503을 방지합니다.
      pingPong:
        pingService: my-app-ping
        pongService: my-app-pong

      # ── trafficRouting ──
      # ALB ForwardConfig을 통해 가중치 기반 트래픽 라우팅을 수행합니다.
      trafficRouting:
        alb:
          ingress: my-ingress       # Ingress 리소스 이름
          rootService: my-app       # Ingress backend가 바라보는 root Service
          servicePort: 80           # ForwardConfig에 사용되는 Service port
      # ── 롤링 제어 ──
      maxUnavailable: 0   # 배포 중 기존 stable Pod를 제거하지 않음 (zero-downtime)
      maxSurge: 1          # canary Pod를 replicas 수 초과로 1대 추가 생성
      # ── 배포 단계 ──
      # setWeight: canary Target Group에 전달할 트래픽 비율 (%)
      # pause: 다음 단계로 진행하기 전 대기
      #   - {duration: 30s} → 30초 후 자동 진행
      #   - {}              → 무기한 대기 (수동 Promote 필요)
      steps:
        - setWeight: 20
        - pause: {duration: 5m}
        - setWeight: 50
        - pause: {duration: 5m}
        - setWeight: 80
        - pause: {duration: 5m}

주요 설정 항목 상세

pingPong

trafficRouting.alb

롤링 제어

steps

운영 환경에서는 모니터링 시스템이 이상을 감지할 수 있도록 충분한 관찰 시간을 두는 것이 중요합니다. 단계별 5m ~ 10m을 권장하며, 보다 신중한 운영이 필요하다면 pause: {}로 수동 Promote하는 방식도 활용됩니다.

Ingress 설정: backend port use-annotation

PingPong이 정상 동작하려면, Ingress의 backend port를 반드시 name: use-annotation으로 설정해야 합니다. 이 설정이 없으면 ALB Controller가 ForwardConfig annotation을 무시하고 직접 서비스로 라우팅하여 가중치 전환이 동작하지 않습니다.

# 변경 전 — ALB가 ForwardConfig을 무시
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  number: 80        # ← 직접 라우팅
# 변경 후 — ALB가 ForwardConfig을 읽어 가중치 라우팅
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  name: use-annotation  # ← ForwardConfig 활성화

주의 : use-annotation은 pingPong/trafficRouting 사용 시에만 설정합니다. trafficRouting을 사용하지 않는 경우 ForwardConfig annotation이 없으므로, 기존 backend port 설정(예: number: 80, number: 8080 등 서비스에 맞는 포트)으로 되돌려야 합니다.

ALB ForwardConfig annotation 자동 주입 원리

카나리 배포가 시작되면, Argo Rollouts 컨트롤러가 Ingress에 ForwardConfig annotation을 자동으로 주입합니다. ALB Controller가 이를 읽어 ALB Listener Rule의 ForwardAction으로 변환하고, 실제 ALB 레벨에서 가중치 기반 라우팅이 수행됩니다.

# setWeight: 20 단계에서 자동 주입되는 annotation
alb.ingress.kubernetes.io/actions.my-app: |
  {
    "Type": "forward",
    "ForwardConfig": {
      "TargetGroups": [
        {"ServiceName": "my-app-ping",  "ServicePort": "80", "Weight": 80},
        {"ServiceName": "my-app-pong",  "ServicePort": "80", "Weight": 20}
      ]
    }
  }
# Promote 완료 — weight swap만 발생
alb.ingress.kubernetes.io/actions.my-app: |
  {
    "Type": "forward",
    "ForwardConfig": {
      "TargetGroups": [
        {"ServiceName": "my-app-ping",  "ServicePort": "80", "Weight": 0},
        {"ServiceName": "my-app-pong",  "ServicePort": "80", "Weight": 100}
      ]
    }
  }

Promote 전후를 비교해보면, 변경된 것은 Weight 숫자뿐입니다. ServiceName, ServicePort 등 target 식별 정보는 그대로이므로, AWS 레벨에서 target deregister/register가 발생하지 않습니다.

steps 설정 패턴

운영 상황에 맞게 steps를 다양하게 구성할 수 있습니다.

카나리 패턴

# 패턴 1: 점진적 카나리 (자동 전환)
steps:
  - setWeight: 20
  - pause: {duration: 5m}
  - setWeight: 50
  - pause: {duration: 5m}
  - setWeight: 80
  - pause: {duration: 5m}

# 패턴 2: 수동 제어 (운영 환경 권장)
# 각 단계에서 수동 Promote 필요
steps:
  - setWeight: 20
  - pause: {}           # ArgoCD UI에서 수동 Promote
  - setWeight: 50
  - pause: {}
  - setWeight: 100

블루그린 패턴

# 패턴 3: 블루그린식 즉시 전환
# 새 Pod가 ready 되면 바로 100% 전환
steps:
  - setWeight: 100

# 패턴 4: 블루그린식 수동 전환
# 새 Pod 띄운 후 검증 → 수동 전환
steps:
  - setWeight: 0
  - pause: {}           # canary Pod 검증 후 수동 Promote
  - setWeight: 100

Tip: PingPong 하나로 카나리와 Blue/Green을 모두 대체할 수 있습니다.

steps 구성만 바꾸면 카나리 점진 전환(패턴 1, 2)과 Blue/Green 즉시 전환(패턴 3, 4)을 하나의 Rollout으로 운영할 수 있습니다. Blue/Green 패턴에서도 Promote 시 selector 변경 없이 weight만 swap하므로, 기존 Blue/Green 전략에서 발생하던 503 문제가 원천적으로 없습니다.

6. 시나리오 테스트: 정말 503이 0건인가?

이론적으로 PingPong이 503을 해결한다는 것은 이해했지만, 실제 환경에서 정말 그런지 검증이 필요합니다. 저희는 몇가지 시나리오를 설계하고, Python 모니터링 스크립트(keep-alive + 멀티스레드 요청)로 실시간 503 발생 여부를 추적하며 테스트를 수행했습니다.

테스트 환경

테스트 결과

정상 배포, 롤백, 수동 제어 등 모든 정상 운영 시나리오에서 503 없이 무중단 배포를 확인했습니다.

S4: 유일하게 503이 발생한 케이스

테스트 시나리오 중 S4(가중치 전환 중 새 버전 배포)에서만 약 6초간 503이 발생했습니다. 원인을 분석해보겠습니다.

발생 조건: 카나리가 50% 상태로 진행 중일 때, 새 이미지 태그를 배포하는 경우

카나리 진행 중 (50% 상태)
  → 새 버전 배포 트리거
    → 기존 canary Pod 제거 + 새 canary Pod 생성이 동시에 발생
      → 새 Pod이 ALB health check를 통과하기까지 ~6초
        → 이 구간에서 canary Target Group에 healthy Pod가 0
          → canary로 라우팅된 50% 트래픽이 503

이 문제는 preStop hook이나 deregistration delay와는 무관합니다. Pod 교체 시 canary Target Group이 일시적으로 비어버리는 타이밍 이슈입니다.

운영 영향: 카나리가 진행 중인 상태에서 또 다른 새 버전을 배포하는 것은 비정상적인 운영 케이스입니다. 정상적인 배포 프로세스에서는 이전 카나리가 완료(Promote 또는 Abort)된 후 다음 배포를 진행하므로, 실제 운영에서 이 시나리오가 발생할 가능성은 매우 낮습니다.

Argo Rollouts는 순차 배포(하나의 stable + 하나의 preview)를 전제로 설계되어 있으며, mid-rollout 배포 시 기존 카나리가 즉시 폐기되고 새 버전으로 재시작됩니다. 이 과정에서 트래픽 라우팅 에러 등의 문제가 보고된 바 있어(#1372, #4534), CI/CD 파이프라인에서 진행 중인 Rollout 여부를 확인하는 guard 추가를 고려할 수 있습니다.

7. 마치며

이번 작업을 통해 얻은 교훈을 정리하면 다음과 같습니다.

배운 점

1. EKS + ALB 환경에서 Argo Rollouts를 사용한다면, PingPong은 선택이 아닌 필수입니다.

Blue/Green이든 기본 Canary든, Promote 시점에 Service selector를 변경하는 모든 전략은 ALB Readiness Gate와 구조적으로 호환되지 않습니다. 커뮤니티에서도 PingPong을 기본 전략으로 권장하고 있습니다.

2. use-annotation 설정을 놓치면 가중치 전환이 동작하지 않습니다.

Ingress backend port가 숫자(예: number: 80)로 되어 있으면 ALB Controller가 ForwardConfig annotation을 무시합니다. PingPong 적용 시 반드시 name: use-annotation으로 변경해야 합니다.

3. 이론적 분석만으로는 부족합니다.

503이 발생하지 않는다는 것을 이론으로 이해하는 것과, 여러가지 시나리오를 직접 테스트하여 확인하는 것은 다릅니다. 특히 Abort, 중간 배포 같은 edge case는 직접 검증해봐야 확신을 가질 수 있습니다.

감사의 말

이번 Canary PingPong 적용은 혼자서는 불가능한 작업이었습니다.

공통 helm의 PingPong Service 정의를 지원해주신 DevOps팀 아이언, 주피터, 그리고 Ingress 설정 변경과 ALB Target Group 구성을 검토하고 반영해주신 SRE팀 제이슨, 젠슨께 감사드립니다.

두 팀의 빠른 의사결정과 적극적인 지원 덕분에 검증까지 순조롭게 마무리할 수 있었습니다.

참고 자료