IT/CI CD

Argo Rollouts로 카나리 배포 실전 도입 가이드

gfrog 2026. 7. 16. 09:13

Deployment를 Rollout으로 마이그레이션

배포할 때마다 심박수가 올라가는 사람 있으면 손. 나만 그런 거 아닐 거다. 특히 트래픽이 몰리는 시간대에 롤아웃 눌러야 할 때는 정말 부담이다. Deployment의 기본 RollingUpdate 전략이 나쁜 건 아닌데, 문제는 "일단 다 밀리고 나면 되돌리기가 귀찮다"는 점이다. HPA에 물려 있는 상황에서 새 버전이 죽으면 롤백 중에도 오토스케일이 계속 돌아서 상황이 더 꼬인다.

그래서 우리 팀은 몇 달 전부터 Argo Rollouts를 도입해서 카나리 배포로 옮기고 있다. 이 글은 도입 과정에서 배운 것과, "이건 처음에 알았으면 좋았을 텐데" 싶은 것들을 정리한 실무 가이드다.

이달 말에 열리는 ArgoCon Japan 2026 세션 리스트를 훑어보니 올해 화두는 여전히 progressive delivery의 운영 안정화, 그리고 자동 분석(AnalysisTemplate) 기반 promotion이다. 새로운 기능이 쏟아지는 단계는 지났고, 이제는 "제대로 굴리는 법"이 중요해진 분위기다.

Deployment를 Rollout으로 바꾸는 두 가지 접근

일단 기존 Deployment를 Rollout CRD로 바꿔야 한다. 이 부분에서 두 가지 접근이 있다:

첫째, spec.workloadRef로 기존 Deployment를 그대로 참조하는 방법. Deployment 리소스를 유지한 채 Rollout이 replica count만 관리한다.

둘째, Deployment를 완전히 Rollout으로 변환하는 방법. kind: Deploymentkind: Rollout으로 바꾸고, apiVersionargoproj.io/v1alpha1로 교체한다.

우리 팀은 처음에 첫 번째 방식(workloadRef)으로 시작했다. 이유는 명확하다. 롤백이 쉬우니까. Argo Rollouts를 걷어내야 하는 상황이 오면 Rollout CR만 지우면 원래 Deployment가 그대로 남는다. 안전빵.

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: api-server
spec:
  replicas: 10
  workloadRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
    scaleDown: onsuccess
  strategy:
    canary:
      canaryService: api-server-canary
      stableService: api-server-stable
      trafficRouting:
        nginx:
          stableIngress: api-server-ingress
      steps:
        - setWeight: 10
        - pause: { duration: 5m }
        - setWeight: 25
        - pause: { duration: 5m }
        - setWeight: 50
        - pause: { duration: 10m }
        - setWeight: 100

여기서 scaleDown: onsuccess가 중요하다. 이걸 안 넣으면 Deployment가 계속 replica를 관리하려고 해서 replica가 두 배로 뜨는 참사가 벌어진다. 처음에 이거 몰라서 스테이징에서 파드가 두 배로 떠있는 걸 30분 넘게 못 알아챘다.

트래픽 라우팅 선택

카나리의 핵심은 "10%만 새 버전으로 보내기"가 실제로 되느냐다. 여기서 트래픽 라우터 선택이 갈린다.

NGINX Ingress 기반 라우팅은 편하다. nginx.ingress.kubernetes.io/canary-* 어노테이션을 Argo Rollouts가 자동으로 붙여준다. 그런데 함정이 있는데, canary용 Ingress가 별도로 하나 더 생성된다는 점이다. Ingress 컨트롤러가 이걸 동일한 host + path로 처리해서 가중치 라우팅을 한다.

서비스 메시(Istio, Linkerd)를 이미 쓰고 있다면 그쪽 라우팅을 쓰는 게 훨씬 정교하다. Istio의 경우 VirtualService의 weight를 Rollout이 직접 조작한다. header 기반 라우팅(특정 사용자만 canary로) 같은 것도 가능하다.

우리는 NGINX + AWS ALB 조합이었는데, ALB Ingress Controller의 weighted target group 기능을 Argo Rollouts가 활용해서 잘 처리해준다. 이걸 쓰면 파드 수와 무관하게 정확한 weight로 트래픽이 분산된다. Replica-based routing보다 훨씬 낫다.

AnalysisTemplate: 자동 판정이 진짜 힘

수동 promotion(pause 이후 사람이 승인)만 쓰다가 AnalysisTemplate으로 넘어오면 세상이 달라진다. Prometheus나 Datadog 쿼리로 error rate, latency를 자동으로 체크해서 실패하면 자동 롤백된다.

apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: canary-success-rate
spec:
  args:
    - name: service-name
  metrics:
    - name: error-rate
      interval: 1m
      count: 5
      successCondition: result[0] < 0.01
      failureLimit: 2
      provider:
        prometheus:
          address: http://prometheus.monitoring.svc:9090
          query: |
            sum(rate(http_requests_total{
              service="{{args.service-name}}",
              status=~"5.."
            }[2m]))
            /
            sum(rate(http_requests_total{
              service="{{args.service-name}}"
            }[2m]))

이걸 Rollout의 step 사이에 끼워 넣는다:

steps:
  - setWeight: 10
  - pause: { duration: 2m }
  - analysis:
      templates:
        - templateName: canary-success-rate
      args:
        - name: service-name
          value: api-server-canary
  - setWeight: 25
  - analysis:
      templates:
        - templateName: canary-success-rate
      args:
        - name: service-name
          value: api-server-canary

여기서 몇 가지 팁:

intervalcount를 너무 짧게 잡으면 카나리가 워밍업 되기도 전에 판정이 나버린다. 처음 도입할 때 30초 interval, 3회 count로 잡았다가 JVM 기반 서비스가 워밍업 중에 latency 튀어서 자동 롤백 지옥에 빠졌다. 지금은 처음 measurement 전에 최소 2-3분 pause를 두고, interval도 1분 이상으로 잡는다.

failureLimit은 2 이상을 추천한다. 1로 두면 노이즈 한 번에 롤백된다. 특히 upstream에 batch 트래픽이 몰려오는 API는 순간 5xx가 튀는 게 이상하지 않은데, 그때마다 롤백되면 배포가 아예 안 된다.

Progressive weight의 함정

setWeight: 10 → 25 → 50 → 100 이런 스텝을 짜다 보면 뭔가 진짜 progressive한 것 같은 착각이 든다. 근데 실제로는 각 스텝의 pause 시간이 트래픽 패턴을 다 커버할 만큼 긴가를 봐야 한다.

우리 서비스는 오전 9시-11시에 트래픽이 5배로 뛰는 패턴이다. 그런데 배포는 오전 10시에 하는 경우가 많다. 이때 setWeight 10에서 5분만 pause하면, 그 5분 동안 트래픽 패턴이 완전히 바뀌어서 canary가 받는 실제 요청 유형이 다르다. 결과적으로 canary 판정이 왜곡된다.

지금은 트래픽 피크 시간에는 pause를 10분 이상으로 늘려서 최소 두 개 이상의 트래픽 사이클을 커버하도록 한다. 조금 답답한 감은 있지만 안전하다.

Rollback 시나리오 준비

카나리 배포에서 실제 롤백은 두 가지 상황이 있다:

첫째, canary가 실패 판정을 받아 자동으로 되돌아가는 경우. 이건 Argo Rollouts가 알아서 stable 서비스로 트래픽을 100% 되돌린다.

둘째, canary가 promote까지 다 갔는데 그 후에 문제가 발견되는 경우. 이건 kubectl argo rollouts undo 명령어로 이전 revision으로 되돌린다. 이때 주의할 점은 되돌린 버전도 다시 카나리 스텝을 밟는다는 것이다. 즉 즉시 100% 되돌아가지 않는다.

만약 정말 급하게 100% 롤백이 필요하면 --full 옵션이 있긴 하지만, 우리는 이걸 잘 안 쓴다. 카나리 방식의 안전장치를 우회하는 셈이라 예상치 못한 부작용이 나올 수 있다. 대신 stable 서비스에 붙어 있는 태그를 이전 이미지로 강제로 바꾸는 임시 조치를 준비해뒀다.

도입하면서 겪은 오해들

몇 개만 정리한다:

카나리는 배포 속도를 느리게 만든다. 맞다. 5분 pause를 여러 번 걸어두면 배포 시간이 20-30분씩 걸린다. 개발자들이 "왜 이렇게 오래 걸려요"라고 물어보는 게 정상이다. 대신 새벽 3시에 페이지 안 받는 걸로 보상받는다.

모든 서비스에 다 카나리를 걸 필요는 없다. 배치 잡이나 내부용 어드민 툴에까지 카나리를 걸면 오히려 관리가 복잡해진다. 우리는 external-facing API랑 결제 관련 서비스만 카나리를 쓰고, 내부 서비스는 여전히 blue-green이거나 RollingUpdate다.

AnalysisTemplate 없이 pause만 걸어도 도움이 된다. 처음부터 자동 판정까지 다 세팅하려면 부담이 크다. 우선 수동 promotion으로 도입해서 팀원들이 익숙해진 다음에 자동 분석을 붙이는 게 현실적이다.

다음 스텝

내부적으로는 이제 header 기반 canary 라우팅(특정 QA 계정만 canary로 보내기)을 실험 중이다. 잘 되면 별도로 정리해서 공유하려고 한다. 그리고 KubeCon Japan에서 Progressive Delivery 세션 몇 개가 예정되어 있는데, 관심 있는 분들은 라이브 세션이나 녹화본을 챙겨보시면 좋을 것 같다.

혹시 다른 트래픽 라우터로 카나리 굴리시는 분 계시면 어떻게 세팅했는지 궁금하다. 특히 Envoy Gateway 쪽 경험 있으면 댓글로 남겨주시면 감사히 참고하겠다.