IT/CI CD

Argo Rollouts canary + AnalysisTemplate, 메트릭으로 자동 롤백시키기

gfrog 2026. 6. 1. 22:18
반응형

배포 자동화는 다들 잘 해놨는데, 정작 "이 배포 잘 된 거 맞아?"를 판단하는 건 사람이 대시보드 보고 있다. 우리도 그랬다. ArgoCD가 알아서 sync까지는 해주는데, P99가 튀거나 에러율이 올라가면 누군가는 새벽에 깨서 롤백을 해야 했다.

올해 초 Argo Rollouts을 본격적으로 도입했고, AnalysisTemplate으로 Prometheus 메트릭을 보고 자동 롤백까지 시키는 데까지 왔다. 이 글은 그동안 정리해둔 셋업 노트다. 처음 도입하는 팀이 보면 30분 안에 동작하는 canary는 만들 수 있게 썼다.

왜 Rollout인가 (Deployment로는 안 되나)

솔직히 Deployment + RollingUpdate로도 canary 비슷한 흉내는 낼 수 있다. 그런데 두 가지가 안 된다. 하나는 트래픽 비중을 정밀하게 조정하는 것 — maxSurge로는 10%, 25%, 50%처럼 단계별로 천천히 올릴 수가 없다. 다른 하나는 메트릭 기반 자동 판단이다. Deployment는 "Pod가 Ready인가"만 본다. P99가 800ms를 찍고 있어도 모른다.

Argo Rollouts는 이 둘을 다 해결한다. CRD가 Rollout이라는 점만 빼면 Deployment랑 거의 비슷하게 생겼다.

가장 단순한 canary 셋업

먼저 동작하는 최소 구성부터 보자. 컨트롤러는 헬름으로 설치한다.

helm repo add argo https://argoproj.github.io/argo-helm
helm install argo-rollouts argo/argo-rollouts -n argo-rollouts --create-namespace

Rollout 매니페스트는 이렇게 생겼다.

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: web-api
spec:
  replicas: 10
  strategy:
    canary:
      steps:
      - setWeight: 10
      - pause: { duration: 5m }
      - setWeight: 30
      - pause: { duration: 10m }
      - setWeight: 60
      - pause: { duration: 10m }
  selector:
    matchLabels:
      app: web-api
  template:
    metadata:
      labels:
        app: web-api
    spec:
      containers:
      - name: web-api
        image: registry.local/web-api:v1.4.2
        ports:
        - containerPort: 8080

이 상태에서 이미지 태그만 바꿔 kubectl apply하면, 새 버전 Pod가 10% 비중으로 먼저 뜨고 5분 대기 → 30%로 늘고 10분 대기 → 이런 식으로 흘러간다. 트래픽 분할은 기본적으로 Pod 개수 비율로 한다. Replica가 10개니까 10%면 새 버전 1개, 구버전 9개다.

여기서 끝나면 그냥 단계가 있는 RollingUpdate랑 크게 다를 게 없다. 핵심은 다음 단계다.

AnalysisTemplate으로 메트릭 기반 자동 판단

pause 자리에 사람이 보고 판단하는 대신, 메트릭을 보고 자동으로 판단하게 만든다. AnalysisTemplate을 먼저 만든다.

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

count: 5 + interval: 1m이면 1분 간격으로 5번 측정한다. successCondition이 5번 중 한 번이라도 깨지면 (failureLimit: 1) 분석은 실패로 처리된다. 그 시점에 Rollout이 자동으로 중단되고 stable 버전으로 트래픽이 돌아간다.

Rollout 매니페스트에 이걸 묶는다.

strategy:
  canary:
    steps:
    - setWeight: 10
    - pause: { duration: 2m }
    - analysis:
        templates:
        - templateName: web-api-success-rate
        args:
        - name: service-name
          value: web-api
    - setWeight: 30
    - analysis:
        templates:
        - templateName: web-api-success-rate
        args:
        - name: service-name
          value: web-api
    - setWeight: 60

이러면 각 단계마다 5분 동안 성공률을 보다가, 99% 밑으로 떨어지면 자동 abort 된다. P99 latency, error budget burn rate도 같은 방식으로 추가하면 된다. 우리 팀은 success-rate + P99 + custom business metric(주문 성공률) 세 개를 묶어서 본다.

실무에서 걸리는 것들

처음에 셋업하고 한 달쯤 운영하면서 걸린 것들을 정리해둔다.

카나리 메트릭 라벨링. Prometheus 쿼리에서 canary와 stable을 구분하려면 Pod에 라벨이 박혀 있어야 한다. Rollouts가 자동으로 rollouts-pod-template-hash 라벨을 박아주긴 하지만, 애플리케이션 메트릭에는 자기들이 보고 있는 라벨이 따로 있는 경우가 많다. 우리는 Rollout의 canaryMetadata.labelsversion=canary를 박고, ServiceMonitor의 relabel 설정에서 이걸 메트릭 라벨로 끌어올렸다. 이거 빼먹으면 canary와 stable 메트릭이 섞여서 분석이 무의미해진다.

트래픽 분할 정밀도. Pod 개수 비율로만 분할하면 replicas: 4에서 setWeight: 10은 의미가 없다 (반올림하면 0이거나 1). 진짜 10% 트래픽을 보내려면 서비스 메시(Istio, Linkerd) 또는 Ingress(NGINX, ALB)와 연동해서 weight를 명시적으로 분할해야 한다. 우리는 Istio VirtualService와 묶어서 쓴다.

strategy:
  canary:
    canaryService: web-api-canary
    stableService: web-api-stable
    trafficRouting:
      istio:
        virtualService:
          name: web-api
          routes: [primary]

이 설정이 있으면 Rollouts가 알아서 VirtualService의 weight를 갱신해준다. Pod 수와 무관해진다.

PDB와의 상호작용. canary 단계에서 새 버전이 죽으면 stable 버전 Pod도 같이 evict 되는 게 아니냐는 질문을 받은 적이 있는데, PDB는 selector 기준으로 동작하니까 app: web-api로만 묶여 있으면 canary와 stable이 같은 PDB를 공유한다. 보통 이게 맞다. 다만 minAvailable을 너무 빡빡하게 잡으면 카나리 단계에서 stable 쪽 Pod를 줄이지 못해서 weight가 안 올라가는 경우가 있다. 한 번 데어봤다.

abort 후 다음 배포. 분석 실패로 abort 되면 Rollout은 Degraded 상태가 된다. 이 상태에서 새 이미지로 다시 apply 해도 곧바로 다음 시도가 들어가진 않는다. kubectl argo rollouts retry rollout web-api로 명시적으로 retry 시키거나, 아예 새 revision으로 가야 한다. CI/CD 파이프라인에서 이 케이스를 처리해두지 않으면 사람이 직접 retry 누르는 일이 생긴다.

다음 단계로 가려면

여기까지가 우리가 한 달 정도 운영하면서 안정화한 셋업이다. 이다음으로는 두 가지를 더 보고 있다. 하나는 ArgoCon 발표에서 봤던 "비교 분석" — canary와 stable의 메트릭을 절대값이 아니라 차이로 보는 방식이다. P99가 200ms든 500ms든 상관없이 stable 대비 30% 이상 늘면 abort. 트래픽 패턴이 시간대마다 다른 서비스에서는 절대값 임계치보다 이게 더 안정적이다. 다른 하나는 experiment 리소스로 다크 런칭하는 거다. 실 트래픽은 안 받고 섀도우 트래픽만 보내서 비교하는 패턴.

이 글에서는 거기까지는 안 다뤘다. 일단 동작하는 canary부터 만들고, 한두 달 운영하면서 메트릭 임계치를 튜닝하는 게 먼저다. 도입 첫 주에는 임계치가 너무 빡빡해서 정상 배포도 자꾸 abort 되는 일이 생기는데, 그건 정상이다.

반응형