배경: AWS Spot Instance를 사용하는 환경에서는 특정 가용 영역(Zone)의 모든 노드가 갑자기 중단될 수 있습니다.
문제 시나리오:
왜 Zone별 Rollout이 필요한가?
Rollout당 독립적인 PDB 관리
각 Zone의 Rollout이 자체 PDB를 관리
Zone C가 완전히 사라져도 Zone A, B의 PDB는 영향 없음
Zone 단위 복구
Zone C가 복구되면 해당 Rollout만 재시작
다른 Zone의 배포 상태에 영향 없음
Spot Instance 중단 대응
특정 Zone의 Spot Instance가 모두 중단되어도
다른 Zone의 서비스는 계속 운영
Istio locality failover로 자동 트래픽 전환
PDB 설정 예시 (Zone별):
장점:
Zone C 전체 중단 시에도 Zone A, B의 PDB는 정상 동작
각 Zone이 독립적으로 복구 가능
Canary 배포도 Zone별로 독립적으로 진행
요구사항
Zone별 독립 배포: 3개의 가용 영역(a, b, c)에 각각 독립적인 Canary 배포
Zone 격리: 각 zone의 트래픽은 기본적으로 해당 zone 내에서만 처리
Failover 전용: 장애 발생 시에만 다른 zone으로 트래픽 전환 (a→b, b→c, c→a)
통합 호출: 클라이언트는 단일 서비스 이름으로 호출
Spot Instance 대응: Zone 단위 중단에도 서비스 연속성 보장
일반적인 문제
문제: 여러 Argo Rollouts가 같은 VirtualService를 참조하면 충돌 발생
해결책: Zone별 별도 route를 사용한 분리
중요: Argo Rollouts는 지정된 route 이름의 전체 destinations 배열을 관리합니다. 따라서 여러 Rollout이 같은 route 이름을 참조하면, 각 Rollout이 서로의 설정을 덮어쓰게 됩니다. subset을 다르게 설정해도 충돌이 발생합니다.
아키텍처 개요
전체 구조
핵심 컴포넌트
단일 VirtualService: 모든 zone의 트래픽 라우팅 규칙 정의
Zone별 Rollout: 각 zone에서 독립적인 Canary 배포 관리
Subset 기반 분리: 각 Rollout은 고유한 subset 쌍 관리 (stable-a/canary-a 등)
Locality-aware DestinationRule: 자동 zone-local 라우팅 및 failover
핵심 설계 결정
1. 단일 VirtualService + Zone별 Route 분리
왜 이 방식이 필요한가?
Argo Rollouts는 지정된 route 이름의 전체 destinations 배열을 덮어쓰는 방식으로 동작합니다. 따라서 각 Zone의 Rollout이 독립적인 route 이름을 관리해야 충돌이 발생하지 않습니다:
핵심 원리:
각 Rollout은 서로 다른 route 이름을 참조 (zone-a-route, zone-b-route, zone-c-route)
각 route는 sourceLabels match를 통해 해당 Zone의 트래픽만 처리
Locality-aware 라우팅이 자동으로 zone-local 엔드포인트를 우선 선택
2. Locality-aware 라우팅
기본 동작:
Zone A의 클라이언트 → Zone A의 Pod (100%)
Zone B의 클라이언트 → Zone B의 Pod (100%)
Zone C의 클라이언트 → Zone C의 Pod (100%)
Failover 시:
Zone A 장애 → Zone B로 자동 전환
Zone B 장애 → Zone C로 자동 전환
Zone C 장애 → Zone A로 자동 전환
3. 통합 서비스 호출
클라이언트는 단일 DNS 이름 사용:
구현 가이드
1. 공통 Service 생성
중요: selector에 zone 레이블을 포함하지 마세요 (모든 zone의 Pod 선택)
2. Zone별 Rollout Service
각 Rollout이 관리하는 stable/canary Service:
3. Zone별 Route가 있는 단일 VirtualService
모든 zone의 트래픽을 처리하는 단일 VirtualService (Zone별 route 분리):
중요 변경사항:
❌ 이전: 모든 Zone이 같은 primary route 공유 → 충돌 발생
✅ 수정: 각 Zone이 독립적인 route 이름 사용 (zone-a-route, zone-b-route, zone-c-route)
✅ 추가: sourceLabels.topology.kubernetes.io/zone match로 Zone별 트래픽 분리
동작 방식:
Zone A의 파드에서 발생한 요청 → zone-a-route 매칭
Rollout A는 zone-a-route의 weight만 수정 (다른 Zone 영향 없음)
Locality-aware 라우팅이 자동으로 zone-local 엔드포인트 우선 선택
4. DestinationRule with Locality Settings
5. Zone별 Rollout 설정
Zone A Rollout
Zone B Rollout
Zone C Rollout
트래픽 흐름
정상 상태 (Zone-local 트래픽)
Failover 시나리오
Canary 배포 중 트래픽 흐름
문제 해결
1. VirtualService 충돌 오류
증상:
원인: 여러 Rollout이 같은 route를 동시에 수정 시도
해결:
2. Cross-zone 트래픽 발생
증상: Failover가 아닌데도 다른 zone으로 트래픽 전송
원인: distribute 설정이 잘못됨
해결:
3. Failover가 작동하지 않음
증상: Zone 장애 시에도 다른 zone으로 failover되지 않음
원인: Outlier detection이 비활성화되어 있거나 설정이 너무 느림
해결:
4. Rollout이 멈춤
증상: Canary 배포가 진행되지 않음
확인:
5. 디버깅 명령어
모범 사례
1. Rollout 동기화
문제: 여러 zone의 Rollout을 동시에 배포하면 복잡도 증가
권장:
2. Canary 분석
각 zone별로 독립적인 분석 수행:
3. 점진적 Rollout 단계
4. 자동 롤백
5. 모니터링 및 알림
Prometheus Alerts:
6. 배포 체크리스트
성능 고려사항
리소스 요구사항
Control Plane:
Istiod: CPU 500m, Memory 2GB (추가 VirtualService/DestinationRule로 인한 부하)
Data Plane:
Envoy Sidecar: CPU 100-500m, Memory 50-150MB (zone 정보 및 locality 라우팅 오버헤드)
Argo Rollouts Controller:
CPU 100m, Memory 128MB (3개 Rollout 관리)
네트워크 오버헤드
Zone-local 트래픽: 추가 latency 1-2ms (Envoy overhead)
Cross-zone 트래픽 (failover 시): 추가 latency 5-10ms (zone 간 네트워크)
# Zone A - PDB (Rollout별 독립)
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: test-a-pdb
namespace: default
spec:
minAvailable: 1 # Zone A에서 최소 1개
selector:
matchLabels:
app: test
zone: a
---
# Zone B - PDB
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: test-b-pdb
namespace: default
spec:
minAvailable: 1 # Zone B에서 최소 1개
selector:
matchLabels:
app: test
zone: b
---
# Zone C - PDB
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: test-c-pdb
namespace: default
spec:
minAvailable: 1 # Zone C에서 최소 1개
selector:
matchLabels:
app: test
zone: c
# ❌ 잘못된 접근: 모든 Rollouts가 같은 route 수정 시도
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: test-a
spec:
strategy:
canary:
trafficRouting:
istio:
virtualService:
name: test # 모든 zone의 Rollout이 같은 VirtualService 참조
routes:
- primary # 같은 route를 동시에 수정 시도 → 충돌!
# 이렇게 호출
curl http://test.default.svc.cluster.local:8080
# Istio가 자동으로 zone-local 엔드포인트로 라우팅
apiVersion: v1
kind: Service
metadata:
name: test
namespace: default
spec:
selector:
app: test # zone 레이블 없음 - 모든 zone의 Pod 선택
ports:
- name: http
port: 8080
targetPort: 8080
# Zone A - Stable Service
apiVersion: v1
kind: Service
metadata:
name: test-stable-a
namespace: default
spec:
selector:
app: test
zone: a # Zone A의 stable Pod만 선택
ports:
- name: http
port: 8080
targetPort: 8080
---
# Zone A - Canary Service
apiVersion: v1
kind: Service
metadata:
name: test-canary-a
namespace: default
spec:
selector:
app: test
zone: a # Zone A의 canary Pod만 선택
ports:
- name: http
port: 8080
targetPort: 8080
---
# Zone B - Stable Service
apiVersion: v1
kind: Service
metadata:
name: test-stable-b
namespace: default
spec:
selector:
app: test
zone: b
ports:
- name: http
port: 8080
targetPort: 8080
---
# Zone B - Canary Service
apiVersion: v1
kind: Service
metadata:
name: test-canary-b
namespace: default
spec:
selector:
app: test
zone: b
ports:
- name: http
port: 8080
targetPort: 8080
---
# Zone C - Stable Service
apiVersion: v1
kind: Service
metadata:
name: test-stable-c
namespace: default
spec:
selector:
app: test
zone: c
ports:
- name: http
port: 8080
targetPort: 8080
---
# Zone C - Canary Service
apiVersion: v1
kind: Service
metadata:
name: test-canary-c
namespace: default
spec:
selector:
app: test
zone: c
ports:
- name: http
port: 8080
targetPort: 8080
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: test
namespace: default
spec:
hosts:
- test
- test.default.svc.cluster.local
http:
# Zone A route (Rollout A가 관리)
- name: zone-a-route
match:
- sourceLabels:
topology.kubernetes.io/zone: us-east-1a
route:
- destination:
host: test
subset: stable-a
weight: 90
- destination:
host: test
subset: canary-a
weight: 10
# Zone B route (Rollout B가 관리)
- name: zone-b-route
match:
- sourceLabels:
topology.kubernetes.io/zone: us-east-1b
route:
- destination:
host: test
subset: stable-b
weight: 90
- destination:
host: test
subset: canary-b
weight: 10
# Zone C route (Rollout C가 관리)
- name: zone-c-route
match:
- sourceLabels:
topology.kubernetes.io/zone: us-east-1c
route:
- destination:
host: test
subset: stable-c
weight: 90
- destination:
host: test
subset: canary-c
weight: 10
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: test
namespace: default
spec:
host: test
trafficPolicy:
loadBalancer:
localityLbSetting:
enabled: true
# 각 zone은 기본적으로 로컬 트래픽만 처리
distribute:
- from: us-east-1/us-east-1a/*
to:
"us-east-1/us-east-1a/*": 100 # Zone A → Zone A (100%)
- from: us-east-1/us-east-1b/*
to:
"us-east-1/us-east-1b/*": 100 # Zone B → Zone B (100%)
- from: us-east-1/us-east-1c/*
to:
"us-east-1/us-east-1c/*": 100 # Zone C → Zone C (100%)
# Failover 설정: a→b, b→c, c→a
failover:
- from: us-east-1/us-east-1a
to: us-east-1/us-east-1b # Zone A 장애 시 Zone B로
- from: us-east-1/us-east-1b
to: us-east-1/us-east-1c # Zone B 장애 시 Zone C로
- from: us-east-1/us-east-1c
to: us-east-1/us-east-1a # Zone C 장애 시 Zone A로
# 빠른 장애 감지를 위한 Outlier Detection
outlierDetection:
consecutiveErrors: 3 # 3번 연속 실패 시
interval: 10s # 10초마다 확인
baseEjectionTime: 30s # 30초간 제외
maxEjectionPercent: 100 # 최대 100% 제외 가능
# 각 zone별 stable/canary subset 정의
subsets:
# Zone A subsets
- name: stable-a
labels:
app: test
zone: a
- name: canary-a
labels:
app: test
zone: a
# Zone B subsets
- name: stable-b
labels:
app: test
zone: b
- name: canary-b
labels:
app: test
zone: b
# Zone C subsets
- name: stable-c
labels:
app: test
zone: c
- name: canary-c
labels:
app: test
zone: c
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: test-a
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: test
zone: a
template:
metadata:
labels:
app: test
zone: a
spec:
# Zone A에만 Pod 배포
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- us-east-1a
containers:
- name: app
image: myapp:v1
ports:
- containerPort: 8080
env:
- name: ZONE
value: "a"
strategy:
canary:
# Zone A 전용 Service
canaryService: test-canary-a
stableService: test-stable-a
trafficRouting:
istio:
virtualService:
name: test # 공통 VirtualService
routes:
- zone-a-route # Zone A 전용 route
destinationRule:
name: test # 공통 DestinationRule
canarySubsetName: canary-a # Zone A 전용 subset
stableSubsetName: stable-a # Zone A 전용 subset
steps:
- setWeight: 10
- pause: {duration: 5m}
- setWeight: 20
- pause: {duration: 5m}
- setWeight: 50
- pause: {duration: 5m}
- setWeight: 80
- pause: {duration: 5m}
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: test-b
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: test
zone: b
template:
metadata:
labels:
app: test
zone: b
spec:
# Zone B에만 Pod 배포
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- us-east-1b
containers:
- name: app
image: myapp:v1
ports:
- containerPort: 8080
env:
- name: ZONE
value: "b"
strategy:
canary:
# Zone B 전용 Service
canaryService: test-canary-b
stableService: test-stable-b
trafficRouting:
istio:
virtualService:
name: test # 공통 VirtualService
routes:
- zone-b-route # Zone B 전용 route
destinationRule:
name: test # 공통 DestinationRule
canarySubsetName: canary-b # Zone B 전용 subset
stableSubsetName: stable-b # Zone B 전용 subset
steps:
- setWeight: 10
- pause: {duration: 5m}
- setWeight: 20
- pause: {duration: 5m}
- setWeight: 50
- pause: {duration: 5m}
- setWeight: 80
- pause: {duration: 5m}
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: test-c
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: test
zone: c
template:
metadata:
labels:
app: test
zone: c
spec:
# Zone C에만 Pod 배포
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- us-east-1c
containers:
- name: app
image: myapp:v1
ports:
- containerPort: 8080
env:
- name: ZONE
value: "c"
strategy:
canary:
# Zone C 전용 Service
canaryService: test-canary-c
stableService: test-stable-c
trafficRouting:
istio:
virtualService:
name: test # 공통 VirtualService
routes:
- zone-c-route # Zone C 전용 route
destinationRule:
name: test # 공통 DestinationRule
canarySubsetName: canary-c # Zone C 전용 subset
stableSubsetName: stable-c # Zone C 전용 subset
steps:
- setWeight: 10
- pause: {duration: 5m}
- setWeight: 20
- pause: {duration: 5m}
- setWeight: 50
- pause: {duration: 5m}
- setWeight: 80
- pause: {duration: 5m}
Error: VirtualService update conflict
# ✅ 각 Rollout이 고유한 subset 관리하도록 설정
spec:
strategy:
canary:
trafficRouting:
istio:
destinationRule:
canarySubsetName: canary-a # Zone별로 다른 subset
stableSubsetName: stable-a
# ✅ 올바른 distribute 설정
distribute:
- from: us-east-1/us-east-1a/*
to:
"us-east-1/us-east-1a/*": 100 # 100% local만
# Rollout 상태 확인
kubectl argo rollouts get rollout test-a -n default
# VirtualService 가중치 확인
kubectl get virtualservice test -n default -o yaml | grep weight
# DestinationRule subset 확인
kubectl get destinationrule test -n default -o yaml
# 1. Pod가 올바른 zone에 배포되었는지 확인
kubectl get pods -l app=test -o wide
kubectl get nodes --show-labels | grep topology.kubernetes.io/zone
# 2. Locality 라우팅 설정 확인
istioctl proxy-config endpoint <pod-name> --cluster "outbound|8080||test.default.svc.cluster.local"
# 3. VirtualService 동기화 확인
istioctl proxy-config route <pod-name> --name 8080
# 4. Outlier detection 상태 확인
kubectl exec <pod-name> -c istio-proxy -- curl localhost:15000/clusters | grep outlier
# 5. Argo Rollouts 로그 확인
kubectl logs -n argo-rollouts deployment/argo-rollouts
# Zone별 순차 배포
kubectl argo rollouts promote test-a -n default
# 5분 대기 및 모니터링
kubectl argo rollouts promote test-b -n default
# 5분 대기 및 모니터링
kubectl argo rollouts promote test-c -n default
spec:
strategy:
canary:
analysis:
templates:
- templateName: success-rate-zone-a
startingStep: 2 # 두 번째 단계부터 분석 시작
trafficRouting:
istio:
virtualService:
name: test
destinationRule:
name: test
canarySubsetName: canary-a
stableSubsetName: stable-a
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: zone-aware-rollout-alerts
spec:
groups:
- name: rollout
rules:
# Zone A Canary 실패율 높음
- alert: HighErrorRateZoneA
expr: |
sum(rate(istio_requests_total{
destination_service="test.default.svc.cluster.local",
response_code=~"5..",
destination_pod_label_zone="a"
}[5m])) /
sum(rate(istio_requests_total{
destination_service="test.default.svc.cluster.local",
destination_pod_label_zone="a"
}[5m])) > 0.05
for: 2m
annotations:
summary: "Zone A Canary has high error rate"
# Cross-zone 트래픽 발생 (예상치 못한)
- alert: UnexpectedCrossZoneTraffic
expr: |
sum(rate(istio_requests_total{
destination_service="test.default.svc.cluster.local",
source_workload_zone="a",
destination_pod_label_zone!="a"
}[5m])) > 0
for: 5m
annotations:
summary: "Unexpected cross-zone traffic from Zone A"