Istio + Argo Rollouts로 구성하는 카나리 배포: 파드 메트릭 격리부터 헤더 기반 테스트 라우팅까지
카나리 배포를 처음 도입했을 때 저도 비슷한 실수를 했습니다. Kubernetes 기본 Deployment로 레플리카 비율을 나눠서 "10% 카나리"라고 부르면 된다고 생각했거든요. 그런데 파드가 3개면 1개가 카나리인데, 그게 정확히 33%입니다. 여기서 문제가 끝나면 좋았을 텐데, 카나리와 stable 파드의 메트릭이 뒤섞이면서 성공률이 이상하게 측정됐습니다. 오염된 집계치를 기준으로 AnalysisTemplate 롤백 조건이 트리거됐고, 실제로는 아무 문제 없는 배포가 의미 없는 롤백으로 끝난 경험이 있습니다.
Istio의 VirtualService 가중치와 Argo Rollouts를 결합하면, 파드 수와 무관하게 트래픽 비율을 정밀하게 제어하면서 카나리 파드에 실제로 도달한 요청만 골라내 메트릭 분석까지 자동화됩니다. 여기에 setHeaderRoute를 더하면 일반 사용자는 stable 버전을 그대로 사용하면서 QA 팀만 헤더 하나로 카나리를 미리 검증하는 구성도 가능합니다.
이 글에서는 정밀한 트래픽 분배, 파드 레벨 메트릭 격리, 헤더 기반 테스트 라우팅 이 세 가지를 실제 YAML과 PromQL을 중심으로 풀어보겠습니다. Kubernetes를 운영 중이고 Prometheus로 메트릭을 수집하고 있으며, Istio나 Argo Rollouts를 도입했거나 도입을 고려 중인 분들을 대상으로 합니다. Kubernetes 기본 개념(Deployment, Service, ReplicaSet)은 알고 계신다는 전제로 진행합니다.
핵심 개념
왜 레플리카 비율이 아닌 VirtualService 가중치인가
Kubernetes 기본 방식은 파드 수의 비율로 트래픽이 결정됩니다. 정확히 5%만 카나리로 보내고 싶다면 파드를 20개 띄워야 한다는 계산이 나옵니다. 카나리 파드 2개 vs stable 파드 38개 같은 비율은 비용도 문제지만, 현실적으로 유지하기 어렵습니다.
Istio의 VirtualService는 이 문제를 레이어를 바꿔서 해결합니다. Envoy 사이드카가 각 파드에 주입되어 있고, VirtualService의 weight 값이 Envoy의 라우팅 결정을 직접 제어합니다. 파드가 몇 개이든 트래픽 비율은 YAML에 쓴 숫자 그대로입니다.
VirtualService: Istio에서 HTTP/gRPC 트래픽의 라우팅 규칙을 정의하는 CRD입니다. 어떤 요청을 어디로 얼마만큼 보낼지를 선언하며, Argo Rollouts가 카나리 단계를 진행하면서 이 리소스의
weight값을 동적으로 수정합니다.
전체 아키텍처 흐름
글을 읽기 전에 전체 그림을 한 번 보면 각 구성 요소의 역할이 훨씬 명확하게 들어옵니다.
┌────────────────────────────────────┐
일반 요청 ─────────▶│ Istio Ingress Gateway │
X-Canary: true ───▶│ (Envoy Proxy) │
└─────────────┬──────────────────────┘
│ VirtualService
┌───────┴─────────┐
weight │ 90% │ 10% (또는 헤더 매칭)
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Stable Service │ │ Canary Service │
└────────┬────────┘ └────────┬────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Stable Pods │ │ Canary Pods │
│ [Envoy Sidecar] │ │ [Envoy Sidecar] │
└────────┬────────┘ └────────┬────────┘
│ 메트릭 │ 메트릭
└──────────┬──────────┘
▼
┌────────────┐
│ Prometheus │◀── AnalysisTemplate 쿼리
└────────────┘
▲
┌─────────────┴───────────────┐
│ Argo Rollouts Controller │
│ (자동 프로모션 / 롤백 판단) │
└─────────────────────────────┘각 구성 요소의 역할을 정리하면 이렇습니다.
| 구성 요소 | 역할 |
|---|---|
Rollout CRD |
카나리 단계(steps), 분석 연결, 트래픽 가중치 정의 |
VirtualService |
HTTP 라우팅 가중치(stable/canary) 동적 수정 |
DestinationRule |
stable·canary 서브셋을 파드 레이블로 구분 |
AnalysisTemplate |
Prometheus 쿼리로 자동 프로모션/롤백 판단 |
setHeaderRoute |
특정 헤더가 있는 요청만 카나리 파드로 라우팅 |
AnalysisTemplate: Argo Rollouts에서 카나리 파드의 성능을 자동으로 평가하는 CRD입니다. Prometheus, Datadog, New Relic 등 다양한 메트릭 제공자를 지원하며, 조건을 만족하지 못하면 자동으로 롤백합니다.
파드 레벨 메트릭 격리의 원리
Istio 사이드카는 각 파드에서 수신하는 모든 요청에 대해 Prometheus 메트릭을 노출합니다. 핵심은 이 메트릭에 destination_workload 레이블이 붙는다는 점입니다.
# 카나리 워크로드에 도달한 요청만 필터링
istio_requests_total{destination_workload="my-service-canary"}my-service-canary는 Argo Rollouts가 생성하는 카나리 ReplicaSet의 이름입니다. 이 레이블 덕분에 전체 서비스의 메트릭이 아니라 "카나리 파드에 실제로 전달된 요청"만 쿼리할 수 있습니다.
| Istio 레이블 | 의미 |
|---|---|
destination_workload |
요청을 받은 워크로드 이름 (카나리/stable 구분 핵심) |
source_workload |
요청을 보낸 워크로드 이름 |
reporter |
"source" 또는 "destination" — 중복 집계를 막으려면 "destination"으로 고정 |
response_code |
HTTP 응답 코드 |
실전 적용
예시 1: stable/canary Service와 DestinationRule 준비
Argo Rollouts의 Istio 통합은 두 개의 별도 Kubernetes Service 오브젝트를 필요로 합니다. stableService와 canaryService는 Rollout manifest에서 참조하는 이름인데, 이 Service를 사전에 직접 생성해야 한다는 점이 처음에 놓치기 쉬운 포인트입니다. 이게 없으면 Rollout 컨트롤러가 오류를 내면서 배포가 진행되지 않습니다.
# 사전 준비: stable과 canary를 위한 별도 Service 생성
apiVersion: v1
kind: Service
metadata:
name: my-service-stable
spec:
selector:
app: my-service
ports:
- port: 80
targetPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: my-service-canary
spec:
selector:
app: my-service
ports:
- port: 80
targetPort: 8080처음에는 두 Service의 selector가 동일해도 됩니다. Argo Rollouts 컨트롤러가 롤아웃이 시작되면 각 Service의 selector에 rollouts-pod-template-hash 레이블을 자동으로 추가해서 stable 파드와 canary 파드를 각각의 Service로 분리해줍니다.
다음으로 DestinationRule로 stable과 canary 서브셋을 정의합니다.
# DestinationRule — stable/canary 서브셋 정의
# rollouts-pod-template-hash 값은 Argo Rollouts가 자동으로 관리하므로
# 수동으로 해시값을 채우거나 편집할 필요가 없습니다
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: my-service-dr
spec:
host: my-service
subsets:
- name: stable
labels:
rollouts-pod-template-hash: stable # Rollouts가 실제 해시값으로 자동 패치
- name: canary
labels:
rollouts-pod-template-hash: canary # Rollouts가 실제 해시값으로 자동 패치# VirtualService — Argo Rollouts가 배포 단계마다 weight 값을 자동 수정
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: my-service-vs
spec:
hosts:
- my-service # spec.hosts는 필수 필드
http:
- name: primary
route:
- destination:
host: my-service
subset: stable
weight: 100
- destination:
host: my-service
subset: canary
weight: 0초기에는 weight: stable 100, canary 0으로 두어도 됩니다. Argo Rollouts가 setWeight 스텝에 따라 알아서 수정해줘서, 실제로 VirtualService를 직접 건드릴 일이 거의 없습니다. 솔직히 처음에는 이걸 직접 관리해야 하나 걱정했는데, 컨트롤러가 전부 처리해줘서 오히려 편했습니다.
예시 2: Rollout + AnalysisTemplate으로 자동 프로모션/롤백
Rollout 리소스에서 카나리 단계와 분석을 정의합니다. 아래는 핵심 전략 부분만 발췌한 예시입니다(전체 스펙에는 selector와 template 필드도 포함되어야 합니다).
# Rollout — 카나리 단계, 트래픽 라우팅, 분석 연결 (전략 부분 발췌)
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: my-service
spec:
# selector, replicas, template 등 기본 필드는 Deployment와 동일한 형태로 작성
strategy:
canary:
stableService: my-service-stable # 앞서 생성한 Service 참조
canaryService: my-service-canary # 앞서 생성한 Service 참조
trafficRouting:
istio:
virtualService:
name: my-service-vs
routes:
- primary
steps:
- setWeight: 10
- pause: {duration: 5m}
- analysis:
templates:
- templateName: success-rate
args:
- name: canary-workload
value: my-service-canary
- setWeight: 50
- pause: {duration: 10m}
- setWeight: 100카나리 파드만의 성공률을 측정하는 AnalysisTemplate은 다음과 같이 작성합니다.
# AnalysisTemplate — 카나리 파드에 도달한 요청의 성공률만 측정
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
args:
- name: canary-workload
metrics:
- name: success-rate
interval: 1m
successCondition: result[0] >= 0.95
failureLimit: 3 # 누적 3회 실패 시 롤백 (연속 실패 기준은 consecutiveErrorLimit 사용)
provider:
prometheus:
address: http://prometheus:9090
query: |
(
sum(rate(istio_requests_total{
destination_workload="{{args.canary-workload}}",
reporter="destination",
response_code!~"5.*"
}[2m]))
/
sum(rate(istio_requests_total{
destination_workload="{{args.canary-workload}}",
reporter="destination"
}[2m]))
) or on() vector(1)reporter="destination" 조건을 빼면 source 쪽에서 보고하는 메트릭과 중복 집계될 수 있습니다. 처음에 이 조건 없이 쿼리를 썼다가 성공률이 이상하게 나왔던 경험이 있어서, 꼭 명시하는 것을 권장합니다.
PromQL 핵심 함수 간단 정리:
rate(metric[2m])는 지난 2분간의 초당 평균 증가율을 계산합니다.sum()은 레이블 조합별로 분리된 시계열을 합칩니다.or on() vector(1)은 트래픽이 없어 결과가NaN이 될 때 기본값 1(100% 성공)을 반환해 초기 트래픽 없는 단계에서 불필요한 롤백이 트리거되는 것을 막아줍니다.
| 쿼리 요소 | 설명 |
|---|---|
destination_workload="{{args.canary-workload}}" |
카나리 파드에 도달한 요청만 필터링 |
reporter="destination" |
수신 측 집계로 고정해 중복 방지 |
response_code!~"5.*" |
5xx 제외해 성공 요청 비율 계산 |
or on() vector(1) |
트래픽 없을 때 NaN 대신 기본값 반환 |
failureLimit: 3 |
누적 3회 실패 시 자동 롤백 트리거 |
예시 3: setHeaderRoute로 QA 팀만 카나리 테스트
이 패턴이 개인적으로 가장 실용적이라고 생각합니다. 운영 중인 서비스에 영향을 주지 않고 QA 팀이 카나리 파드를 먼저 검증할 수 있거든요.
# Rollout steps — 헤더 라우트 먼저 설정하고, 검증 후 가중치 트래픽 시작
steps:
- setHeaderRoute:
name: canary-header-route
match:
- headerName: X-Canary
headerValue:
exact: "true"
- pause: {} # QA가 헤더로 카나리 테스트, 수동 승인 대기
- setWeight: 10 # 헤더 라우트 유지한 채 일반 트래픽 10%도 카나리로
- pause: {duration: 10m}
- analysis:
templates:
- templateName: success-rate
args:
- name: canary-workload
value: my-service-canary
- setWeight: 50
- pause: {duration: 10m}
- setHeaderRoute:
name: canary-header-route # match 없이 같은 이름 → 해당 헤더 라우트 삭제
- setWeight: 100마지막에서 두 번째 스텝에서 match 없이 동일한 name을 사용하는 게 처음에는 직관적이지 않게 느껴질 수 있습니다. Argo Rollouts는 이를 "해당 이름의 라우트를 managedRoutes 목록에서 제거하라"는 명령으로 해석합니다. 결과적으로 VirtualService에서 헤더 조건 라우트가 사라지는 방식입니다.
# QA 팀 — X-Canary 헤더를 붙여 카나리 파드로 직접 요청
curl -H "X-Canary: true" https://my-service.example.com/api/health
# 일반 사용자 — 헤더 없이 → stable 파드로 라우팅
curl https://my-service.example.com/api/healthpause: {} 스텝에서 멈춰 있을 때 QA 팀이 E2E 테스트를 마치면, kubectl argo rollouts promote my-service 명령으로 다음 단계로 넘어갈 수 있습니다. ArgoCD를 사용 중이라면 UI의 프로모션 버튼을 누르면 됩니다.
예시 4: 카나리 파드 P99 레이턴시와 업스트림 에러율 쿼리
성공률 외에도 레이턴시와 카나리가 의존하는 서비스에 미치는 영향을 함께 모니터링하면 훨씬 안전합니다. 운영을 이어가다 보니, 이 쿼리들을 추가하고 나서야 성공률만으로는 잡히지 않던 문제들이 보이기 시작했습니다.
# 카나리 파드의 P99 응답 시간 (히스토그램 버킷 기반)
histogram_quantile(0.99,
sum(rate(istio_request_duration_milliseconds_bucket{
destination_workload="my-service-canary",
reporter="destination"
}[5m])) by (le)
)# 카나리 파드가 보낸 요청 중 5xx 발생율 (카나리→의존 서비스 영향 측정)
(
sum(rate(istio_requests_total{
source_workload="my-service-canary",
response_code=~"5.*"
}[2m]))
/
sum(rate(istio_requests_total{
source_workload="my-service-canary"
}[2m]))
) or on() vector(0)두 번째 쿼리는 source_workload를 기준으로 합니다. 카나리 파드 자체가 받는 오류가 아니라, 카나리 파드가 다른 서비스를 호출할 때 발생하는 오류를 잡아냅니다. 새 버전이 DB나 외부 API를 다르게 호출하는 경우, 이 쿼리가 없으면 문제를 놓칠 수 있습니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 레플리카 수 독립 트래픽 제어 | 카나리 파드 1개로도 정확히 5%, 10% 트래픽 분배 가능 |
| 파드 레벨 메트릭 정밀도 | destination_workload 레이블로 카나리 파드에만 도달한 요청의 에러율·레이턴시 독립 측정 |
| 무중단 QA 테스트 | setHeaderRoute로 실 서비스 영향 없이 카나리 검증 후 점진적 전환 |
| 자동 롤백 | AnalysisTemplate 실패 조건 충족 시 Argo Rollouts가 자동으로 stable 복귀 |
| GitOps 친화 | 모든 설정이 Kubernetes manifest로 선언되어 ArgoCD와 자연스럽게 통합 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 리소스 복잡도 | VirtualService, DestinationRule, Rollout, AnalysisTemplate 등 다수 리소스 동시 관리 | Helm chart 또는 Kustomize로 템플릿화해 일관성 유지 |
| 메트릭 카디널리티 | Istio 레이블 조합 증가로 Prometheus 스토리지·쿼리 비용 증가 | Sidecar 리소스로 메트릭 스코프 제한, 불필요한 레이블 exclude 설정 |
| ArgoCD diff 충돌 | Argo Rollouts가 VirtualService를 동적 수정하므로 ArgoCD가 drift로 감지 | ignoreDifferences에 VirtualService의 spec.http 경로 추가 필수 |
| 서브셋 해시 의존성 | DestinationRule의 rollouts-pod-template-hash는 롤아웃마다 변경 |
Argo Rollouts가 자동 관리하므로 수동 수정 금지 — 컨트롤러에 위임 |
| DB 마이그레이션 결합 | 스키마 변경 수반 시 카나리/stable 파드가 동시에 동일 DB 공유 | 스키마 변경과 코드 배포를 별도 단계로 분리 (expand/contract 패턴) |
| mTLS 스크래핑 이슈 | PeerAuthentication STRICT 모드에서 Prometheus 스크래핑이 차단될 수 있음 | Prometheus에 Istio 인증서 마운트하거나 포트 15090(Envoy stats)을 직접 스크래핑 |
DB 마이그레이션 이슈는 처음 맞닥뜨렸을 때 꽤 당황스러웠습니다. 카나리 파드가 새 컬럼을 쓰는 순간 stable 파드가 알 수 없는 필드라며 오류를 내기 시작한다는 걸, 직접 겪고 나서야 expand/contract 패턴의 필요성을 실감했습니다. mTLS 스크래핑 이슈도 비슷한 맥락입니다 — PeerAuthentication을 STRICT로 바꿨더니 Prometheus 수집이 조용히 끊겨 버린 경험이 있었습니다.
ignoreDifferences: ArgoCD에서 특정 필드의 변경을 drift로 감지하지 않도록 설정하는 옵션입니다. Argo Rollouts가 VirtualService의
weight값을 자동 수정하기 때문에, ArgoCD가 이를 "원하지 않는 변경"으로 감지해 계속 sync를 시도하는 현상을 방지합니다.
실무에서 가장 흔한 실수
-
reporter레이블 없이 PromQL 작성:reporter="destination"없이 쿼리하면 source와 destination 쪽 메트릭이 중복 집계되어 성공률이 실제보다 낮게 나옵니다. AnalysisTemplate 쿼리에는 항상 이 조건을 포함하는 것을 권장합니다. -
canaryService와stableServiceService 오브젝트를 생성하지 않은 채 Rollout 배포: Rollout manifest에서 이 두 필드를 선언하면 동일한 이름의 Kubernetes Service가 실제로 존재해야 합니다. 없으면 Rollout 컨트롤러가 오류를 내면서 배포가 진행되지 않습니다. 예시 1에서 소개한 Service를 미리 적용해두는 것이 필요합니다. -
setHeaderRoute제거 스텝 누락: 롤아웃 마지막 단계에서 헤더 라우트를 제거하지 않으면 이후에도X-Canary: true헤더가 있는 요청이 (이미 stable이 된) 서브셋으로만 계속 라우팅됩니다. 스텝 마지막에match없는setHeaderRoute를 넣어 명시적으로 제거하는 것이 필요합니다.
마치며
여기까지 따라오셨다면 이제 파드 수에 종속되지 않는 정밀한 카나리 배포를 구성할 기반이 생겼습니다. VirtualService가 트래픽 비율을 결정하고, AnalysisTemplate이 카나리 파드 전용 메트릭으로 자동 판단을 내리며, setHeaderRoute가 그 사이에 QA 팀의 안전한 검증 창구를 열어줍니다. 구성 요소가 많아 처음엔 복잡해 보이지만, 각 역할이 명확히 분리되어 있어서 익숙해지면 배포 과정 전체가 훨씬 투명하게 보이기 시작합니다.
지금 바로 시작해볼 수 있는 3단계를 소개합니다.
-
로컬에서 전체 흐름 확인해보기 (Kubernetes 기본이 익숙하고 로컬 클러스터를 쉽게 띄울 수 있는 분):
kind또는minikube에 Istio와 Argo Rollouts를 설치하고 예시 manifest를 순서대로 적용해보면, 컨트롤러가 VirtualService를 자동으로 수정하는 과정을 직접 확인할 수 있습니다.kubectl get vs my-service-vs -o yaml -w로 변화를 실시간으로 볼 수 있습니다. -
기존 서비스에 AnalysisTemplate 먼저 연결해보기 (이미 Istio와 Prometheus를 운영 중인 분): 카나리 전략을 전환하기 전에, 현재 서비스에 대한
istio_requests_total쿼리를 Prometheus에서 먼저 확인해보면 좋습니다.destination_workload레이블이 어떻게 나오는지 확인한 뒤, AnalysisTemplate을dryRun: true모드로 연결하면 실제 롤백 없이 분석 로직을 검증할 수 있습니다. -
ArgoCD
ignoreDifferences설정 미리 추가하기 (ArgoCD로 GitOps를 운영 중인 분): 프로덕션 적용 전에 ArgoCD Application의ignoreDifferences에 VirtualServicespec.http경로를 추가해두면, 첫 배포 때 ArgoCD가 sync를 반복하는 혼란을 미리 막을 수 있습니다.
참고 자료
- Argo Rollouts — Istio 트래픽 관리 | 공식 문서
- Argo Rollouts — Analysis 개요 | 공식 문서
- Demo: An Automated Canary Deployment on Kubernetes with Argo Rollouts, Istio and Prometheus | CNCF Blog
- Progressive Delivery with Service Mesh: Argo Rollouts + Istio | InfraCloud
- Progressive Delivery with Argo Rollouts: Canary with Analysis | InfraCloud
- Canary Deployment in Kubernetes using Argo Rollouts and Istio | Deckhouse Blog
- Argo Rollouts를 이용한 카나리 Progressive Delivery | Tetrate 공식 문서
- Prometheus 메트릭 쿼리 | Istio 공식 문서
- Under the Hood: Argo Rollouts 1.8 with Kubernetes 1.33 and Prometheus 3.1 | earezki.com
- How to Perform Canary with Argo Rollouts and Istio Service Mesh | OpsMx