마지막 setWeight: 100을 왜 지워야 하는가
2부에서는 rollbackWindow가 정상적인 Canary의 안전장치를 어떻게 깨뜨리는지 확인했습니다.
정상 배포에서는 새 ReplicaSet이 준비되지 않았을 때 Controller가 이전 traffic weight를 유지합니다.
문제는 rollbackWindow가 step index를 끝으로 보내면서 그 “이전 weight”가 마지막 setWeight: 100이 된다는 점이었습니다.
결과적으로 새 ReplicaSet의 최종 목표는 20대인데 실제 Available은 2대뿐인 순간에 traffic 100%가 먼저 전달될 수 있었습니다.
이번 글에서는 이 경로를 어떻게 막을 것인지 다룹니다.
결론은 단순합니다.
Canary steps 마지막의
setWeight: 100을 제거합니다.
중요한 것은 한 줄을 지우는 행위보다 왜 그것만으로 정상 배포는 그대로 유지하면서 롤백 경로만 안전하게 바뀌는가입니다.
그리고 사고 당시 canary가 왜 하필 2대였는지, minPodsPerReplicaSet이 어떤 배경에서 들어왔는지도 함께 살펴봅니다.
1. 먼저 결론
이번 문제에 대한 처방은 다음과 같습니다.
- Canary steps 마지막의
setWeight: 100을 제거합니다. - 정상 배포는 그대로 100%까지 진행됩니다.
- ReplicaSet의 최종 파드 수도 달라지지 않습니다.
rollbackWindow에서는 역탐색이 마지막 100 대신 그 앞의 낮은 weight를 사용하게 됩니다.- 다만 남겨 둔 마지막 weight가 초기 canary 파드 수로 감당 가능한 값인지는 별도로 확인해야 합니다.
- 서비스가 자체
steps를 선언하면 base 설정을 상속하지 않으므로 base와 서비스 override를 각각 수정해야 합니다. dynamicStableScale,setCanaryScale,minPodsPerReplicaSet조정만으로 이 문제를 해결하려고 해서는 안 됩니다.- Controller 수정이 upstream에 들어가기 전까지는 Rollback Drill과 CI 검증을 같이 유지해야 합니다.
핵심은 마지막 setWeight: 100이 정상 배포에서 100%에 도달하기 위해 필요한 step이 아니라는 점입니다.
1부에서 확인했듯 모든 step을 통과하면 Controller는 최종 weight를 100%로 계산합니다.
따라서 아래 두 설정은 정상 배포의 최종 목적지가 같습니다.
steps:
- setWeight: 5
- pause:
duration: 10m
- setWeight: 100steps:
- setWeight: 5
- pause:
duration: 10m차이는 rollbackWindow에서 나타납니다.
첫 번째 설정은 역탐색이 100을 찾지만 두 번째 설정은 5를 찾습니다.
2. 처방 — 마지막 setWeight: 100을 지운다
정상 배포에서는 마지막 setWeight: 100이 없어도 모든 step을 통과한 뒤 최종적으로 100% traffic에 도달합니다.
따라서 다음 설정에서
steps:
- setWeight: 5
- pause:
duration: 10mpause가 끝나면 Rollout은 그대로 전체 traffic 전환과 promotion을 진행할 수 있습니다.
ReplicaSet의 최종 목표도 그대로입니다.
최종 Desired Pods = 20
최종 Traffic = 100%즉 마지막 setWeight: 100을 지운다고 해서 파드가 덜 뜨거나 정상 배포가 5%에서 멈추는 것은 아닙니다.
달라지는 것은 새 ReplicaSet이 아직 준비되지 않은 rollbackWindow 경로에서 Controller가 어떤 값을 “이전 weight”로 선택하느냐입니다.
기존 설정에서는 step 끝에서 역탐색하면 바로 setWeight: 100을 만났습니다.
5% → pause → 100%
↑
rollback 역탐색100을 제거하면 다음과 같이 바뀝니다.
5% → pause
↑
rollback 역탐색따라서 롤백 대상 ReplicaSet이 아직 초기 파드 수만 준비된 상태라면 traffic도 기존의 낮은 weight에 머뭅니다.
파드가 모두 준비되고 promotion 조건까지 충족한 뒤 최종 100% 전환이 이루어집니다.
대가는 무엇인가
대가가 없는 변경은 아닙니다.
사고 당시처럼 롤백 대상 ReplicaSet이 2대만 준비되어 있다면 마지막 weight가 5%일 경우 롤백이 완료될 때까지 traffic의 5%만 복구 버전으로 향합니다.
복구 버전 5%
장애 버전 95%즉 100%를 2대에 몰아 주는 사고는 막지만, 전체 ReplicaSet이 준비되는 동안에는 장애 버전에 traffic이 일부 남습니다.
하지만 promotion 자체가 새 ReplicaSet의 전체 가용성을 기다리기 때문에 롤백 완료 시각이 늦어지는 것은 아닙니다.
달라지는 것은 준비 구간의 traffic 분배입니다.
따라서 이 처방의 목표는
복구가 끝날 때까지 기다리는 시간을 줄이는 것
이 아니라,
복구가 끝나기 전에 준비되지 않은 ReplicaSet으로 traffic이 먼저 몰리는 것을 막는 것
입니다.
안전 한계는 마지막으로 남은 weight가 정한다
마지막 100을 지웠다고 해서 어떤 weight를 남겨도 안전한 것은 아닙니다.
초기 canary 파드 수가 감당할 수 있는 traffic이어야 합니다.
단순화하면 다음 관계로 볼 수 있습니다.
안전한 전체 Pod 규모
≈ minPodsPerReplicaSet / 남는 traffic 비율minPodsPerReplicaSet: 2라면:
| 마지막으로 남는 weight | 초기 2대로 대응 가능한 전체 규모 |
|---|---|
| 1% | 약 200 Pods |
| 5% | 약 40 Pods |
| 10% | 약 20 Pods |
| 50% | 약 4 Pods |
예를 들어 전체 Replica가 100대까지 올라갈 수 있는 서비스라면 마지막 weight를 5%로 남기는 것만으로는 충분하지 않을 수 있습니다.
이 경우에는
- 마지막 rung을 더 낮추거나
- 초기 Canary Pod 수를 늘리거나
- 별도의 Rollback 정책을 적용해야 합니다.
따라서 처방의 핵심은 단순히 100을 삭제한다가 아니라,
rollbackWindow가 역탐색해 가져갈 마지막 weight를 초기 ReplicaSet 용량에 맞는 값으로 제한한다
입니다.
적용이 두 갈래로 갈리는 이유
1부에서 살펴봤듯 Helm values의 리스트는 merge되지 않고 통째로 교체됩니다.
따라서 base에서
steps:
- setWeight: 5
- pause:
duration: 10m으로 변경해도 서비스에서
steps:
- setWeight: 1
- pause:
duration: 5m
- setWeight: 5
- pause:
duration: 5m
- setWeight: 100처럼 자체 steps를 선언하면 base 변경은 적용되지 않습니다.
따라서 적용 작업은 두 갈래입니다.
base steps 수정
+
서비스별 steps override 수정서비스별 형태도 구분해야 합니다.
[1, 5, 100]이나 [5, 100]처럼 앞에 낮은 rung이 있는 경우에는 마지막 100을 제거하면 됩니다.
반면
10 → 50 → 100처럼 마지막 앞 단계가 이미 높은 경우에는 100만 제거해도 rollbackWindow가 50을 선택합니다.
이 경우에는 앞 단계까지 같이 조정해야 합니다.
[100] 하나만 존재하는 경우도 사고 경로에는 노출됩니다.
정상 배포에서는 전체 ReplicaSet이 준비된 뒤 100%로 전환되지만 rollbackWindow에서 step을 끝으로 넘기면 역탐색이 그 하나뿐인 100을 집을 수 있기 때문입니다.
따라서 이 형태 역시 제거 대상입니다.
반대로 steps: []처럼 setWeight 자체가 없다면 이번 역탐색 문제에는 직접 노출되지 않습니다.
이 규칙은 사람이 매번 확인하기보다 CI에서 검증하는 것이 좋습니다.
예를 들어 다음을 검사할 수 있습니다.
마지막 setWeight == 100 → 실패
또는
마지막 setWeight가 초기 canary capacity보다 큰 경우 → 실패3. 왜 다른 대안은 답이 아닌가
마지막 setWeight: 100 제거 외에도 몇 가지 옵션을 생각할 수 있습니다.
하지만 각각 해결하는 문제가 다릅니다.
| 대안 | 이번 문제 해결 여부 | 이유 |
|---|---|---|
dynamicStableScale | X | rollbackWindow의 weight 선택 자체를 막지 못함 |
setCanaryScale | 부분적 | 미리 Pod를 늘릴 수 있지만 step skip 이후에는 보호하지 못함 |
minPodsPerReplicaSet 인상 | 조건부 | 충분히 크게 잡으면 막을 수 있지만 Canary 비용이 커짐 |
| step analysis | X | rollbackWindow가 Analysis 자체를 skip |
마지막 setWeight: 100 제거 | O | 역탐색이 선택할 값을 직접 낮춤 |
dynamicStableScale
dynamicStableScale은 traffic weight에 맞춰 기존 stable ReplicaSet의 크기도 함께 줄이는 기능입니다.
이 기능은 리소스를 절약할 수 있지만 이번 문제의 해결책은 아닙니다.
핵심 사고 경로는
rollbackWindow
→ step index 끝으로 이동
→ 마지막 setWeight 역탐색
→ 100% 선택입니다.
dynamicStableScale을 켠다고 이 선택 과정이 사라지는 것은 아닙니다.
오히려 stable ReplicaSet까지 줄어들면 문제가 발생했을 때 즉시 돌아갈 warm standby가 없어질 수 있습니다.
현재 구성처럼 stable을 유지하는 방식에서는 canary 쪽에 문제가 생겨도 traffic을 기존 stable로 빠르게 돌릴 수 있습니다.
따라서 이번 문제를 해결하기 위해 dynamicStableScale을 활성화하는 것은 적절하지 않습니다.
setCanaryScale
setCanaryScale을 사용하면 traffic weight와 별개로 canary ReplicaSet을 미리 확장할 수 있습니다.
예를 들어:
- setCanaryScale:
replicas: 20처럼 롤백 대상 파드를 먼저 띄우는 방법을 생각할 수 있습니다.
하지만 rollbackWindow는 step index 자체를 끝으로 이동시킵니다.
따라서 일반적인 step 처리를 건너뛴 뒤에는 이 설정이 항상 보호막으로 남아 있다고 볼 수 없습니다.
정상 배포에서 canary를 미리 warm-up 하는 용도로는 유효하지만 이번 rollbackWindow 경로를 직접 막는 해결책은 아닙니다.
minPodsPerReplicaSet을 늘리면 되지 않을까
수학적으로는 가능합니다.
예를 들어 100% traffic을 안전하게 받고 싶다면 초기 canary ReplicaSet 자체가 전체 replica만큼 떠 있어야 합니다.
전체 replicas = 20
minPodsPerReplicaSet = 20이렇게 하면 20대가 준비되기 전에 traffic이 넘어가는 문제는 없어집니다.
하지만 그 순간 Canary의 장점도 대부분 사라집니다.
5% traffic을 검증하기 위해서도 새 버전을 20대 전부 띄우게 됩니다.
따라서 minPodsPerReplicaSet은 최소한의 Canary 용량을 보장하는 옵션으로 써야지, traffic 100% 사고를 막기 위해 전체 replicas까지 올리는 방식으로 사용하기에는 비용이 큽니다.
그리고 우리 환경에서는 이 값 자체가 다른 문제를 만든 이력이 있습니다.
그 이야기를 다음 장에서 봅니다.
4. minPodsPerReplicaSet: 2는 왜 존재했나
2026-08-21 사고에서 롤백 대상 ReplicaSet이 처음 2대로 뜬 것은 우연이 아닙니다.
base chart의
minPodsPerReplicaSet: 2기본값 때문입니다.
이 값은 작은 Canary weight에서도 최소 두 개의 파드를 유지해 단일 Pod에 traffic이 몰리는 것을 막기 위한 의도로 들어갔습니다.
하지만 전체 replica 수가 작아지면서 다른 문제가 나타났습니다.
예를 들어 전체 desired replica가 1인데 minPodsPerReplicaSet: 2라면 Canary ReplicaSet은 최소 2대가 됩니다.
spec.replicas = 1
minPodsPerReplicaSet = 2
stable = 1
canary = 2Controller가 기대하는 전체 상태와 실제 ReplicaSet 규모가 어긋나면서 Rollout이 Healthy 조건에 도달하지 못할 수 있습니다.
반대로 replica를 0으로 줄이는 Consumer류 workload에서는 최소 Pod 설정이 scale-to-zero와 충돌할 수 있습니다.
즉 같은 값이 워크로드 규모에 따라 서로 반대 방향의 문제를 만듭니다.
타임라인
중요한 변화만 추리면 다음과 같습니다.
| 시점 | 변화 |
|---|---|
| 2025-05 | minPodsPerReplicaSet: 2 도입 |
| 2025-06 | rollbackWindow.revisions: 3 도입 |
| 2025-08 | 현재와 유사한 traffic routing 구조 적용 |
| 2025 하반기 | 소규모 워크로드에서 minPodsPerReplicaSet 예외가 서비스별로 증가 |
| 2026-07 | 비용 절감을 위해 비프로덕션 최소 replicas를 대폭 축소 |
| 2026-08 | replica 0/1 워크로드에서 minPods 충돌이 대규모로 드러남 |
| 2026-08-21 | 이번 rollbackWindow 사고 발생 |
여기서 중요한 것은 2라는 숫자 자체가 잘못됐다는 것이 아닙니다.
문제는 하나의 base 기본값이
replicas 0
replicas 1
replicas 20
replicas 100+처럼 규모가 크게 다른 워크로드에 동일하게 적용됐다는 점입니다.
결국 서비스별 예외가 계속 추가됐고, 어떤 워크로드가 어떤 값을 가져야 하는지를 CI가 검증하지 못한 상태가 오래 유지됐습니다.
이번 사고에서도 마찬가지입니다.
minPodsPerReplicaSet: 2가 사고의 원인은 아닙니다.
2대만 준비된 상태에서 100% traffic을 먼저 전달한 것이 원인입니다.
다만 이 값이 사고 당시 노출된 용량을 결정했습니다.
따라서 앞으로는
최소 Canary Pod 수
×
마지막 Canary traffic weight
×
해당 서비스의 최대 Replica 규모를 함께 봐야 합니다.
5. abort / rollback에서 어떤 수단을 써야 하는가
인시던트 중 사용할 수 있는 명령도 같은 의미를 갖지는 않습니다.
| 수단 | 목적 | 이번 상황에서 |
|---|---|---|
retry | 실패한 Rollout을 다시 진행 | Analysis부터 다시 시작할 수 있어 부적합 |
promote | 현재 pause를 진행 | abort 자체를 해제하는 수단은 아님 |
promote --full | step/Analysis를 건너뛰고 전체 승격 | 수동 탈출 경로로 사용 가능 |
GitOps rollback / undo | 이전 Pod template으로 되돌림 | rollbackWindow 경로와 연결 |
특히 retry는 장애 상황에서 주의해야 합니다.
2부에서 본 것처럼 Analysis가 서비스 전체 오류율을 보는 상태라면 다시 시작한 AnalysisRun이 같은 높은 오류율을 읽고 또 abort될 수 있습니다.
반면 promote --full은 별도의 weight 보호 경로를 가지고 있습니다.
따라서 이미 정상 버전으로 되돌아왔고 남은 문제가 Rollout 상태뿐인 경우 수동 탈출 수단으로 사용할 수 있습니다.
다만 이것은 근본 해결책이 아니라 운영 런북의 비상 경로입니다.
GitOps 환경에서는 선언 상태와 실제 상태도 함께 맞춰야 합니다.
따라서 인시던트 대응과 이후 Git 정합성 복구는 구분해서 운영해야 합니다.
6. 업스트림 상태
이 문제는 우리 환경에서만 발생한 것은 아닙니다.
Argo Rollouts에서는 과거에도 ReplicaSet의 실제 가용량보다 traffic weight가 먼저 증가하는 문제가 여러 경로에서 반복적으로 보고됐습니다.
특히 promote --full 경로에서는 유사한 문제가 이미 수정됐고, 이후 stable ReplicaSet의 용량을 확인하는 guard도 추가됐습니다.
하지만 rollbackWindow 경로에서는 여전히 한 가지 빈틈이 남았습니다.
기존 guard들은 대략 다음을 확인합니다.
| Guard | 확인하는 것 |
|---|---|
| Canary Available = 0 | Canary가 하나도 없으면 traffic을 보내지 않음 |
| Stable capacity guard | Stable이 남은 traffic을 감당할 수 있는지 확인 |
promote --full guard | Canary Available에 맞춰 traffic 증가를 제한 |
문제의 경우는
Canary Available = 2
Canary Desired = 20
Canary Weight = 100%
Stable Weight = 0%입니다.
Canary는 0대가 아니므로 첫 guard를 통과합니다.
Stable traffic은 0%이므로 stable capacity guard도 통과합니다.
그리고 rollbackWindow는 promote --full 전용 보호 경로를 사용하지 않습니다.
즉 Canary가 “0대보다 많다”는 것과 “100% traffic을 받을 만큼 충분하다”는 것 사이의 빈 구간이 남습니다.
이 문제를 직접 다루는 upstream 수정안도 같은 방향입니다.
핵심은 단순합니다.
Canary traffic weight를 실제 Available Replica 비율 이상으로 올리지 않는다.
이 방식이 Controller에 정식으로 반영되면 이번에 values에서 적용하는 workaround의 중요도는 크게 낮아집니다.
하지만 Controller 변경이 모든 클러스터에 실제 배포되기 전까지는 현재 방어선을 유지하는 편이 안전합니다.
또한 Controller 업그레이드만으로 해결됐다고 가정해서도 안 됩니다.
우리 환경에서는 여러 클러스터가 서로 다른 Controller 버전을 사용할 수 있기 때문에 실제 버전과 해당 버전의 rollback 동작을 확인한 뒤 변경해야 합니다.
7. 검증 — rollback drill
설정 변경은 정상 배포만 테스트해서는 충분하지 않습니다.
이번 문제는 정상 배포가 아니라 rollbackWindow 전용 경로에서만 발생했기 때문입니다.
따라서 회귀 테스트도 실제 rollback을 포함해야 합니다.
적용 경로가 두 개이므로 검증 역시 두 레인으로 나눕니다.
| 레인 | 대상 | 확인할 것 |
|---|---|---|
| A | 서비스 자체 steps override | rollback 시 weight가 100 대신 앞의 낮은 rung에 머무는지 |
| B | base chart 상속 서비스 | base 변경이 렌더 결과에 정상적으로 전파되는지 |
레인 A — 실제 rollback
예를 들어 BEFORE 설정을
steps:
- setWeight: 5
- pause:
duration: 10s
- setWeight: 100으로 두고 rollbackWindow를 재현합니다.
확인할 값은 다음입니다.
BEFORE rollback
→ 초기 ReplicaSet이 완전히 준비되기 전에 weight 100이 관측되는가그다음 마지막 100을 제거합니다.
steps:
- setWeight: 5
- pause:
duration: 10s같은 rollback을 다시 수행합니다.
AFTER rollback
→ 초기 ReplicaSet이 준비 중일 때 weight 5가 유지되는가
→ 전체 ReplicaSet 준비 후 100% promotion 되는가레인 B — base 전파
base chart 변경은 다음 순서로 확인합니다.
helm template
→ 변경 전/후 manifest 비교
→ base를 상속하는 서비스 1개 적용
→ 실제 Rollout 관찰
→ 적용 범위 확대차트 변경 자체와 실제 서비스 적용은 별개입니다.
서비스가 자체 steps를 선언했다면 base 변경이 전파되지 않으므로 반드시 최종 렌더 결과를 기준으로 확인해야 합니다.
또한 작은 stage 환경에서는
20 Pods 중 2 Pods에 100%같은 실제 사고 비율을 그대로 재현하지 못할 수 있습니다.
따라서 이 drill의 목표는 피해량 재현이 아니라 traffic weight 결정 경로의 회귀 확인입니다.
8. 정리
이번 문제는 세 단계로 정리할 수 있습니다.
1부에서는 정상 Canary가 안전한 이유를 봤습니다.
step을 순서대로 진행
→ 새 ReplicaSet 준비 확인
→ 준비가 안 됐으면 이전 weight 유지
→ AnalysisRun으로 이상 감지
→ promotion2부에서는 rollbackWindow가 이 전제를 어떻게 바꾸는지 봤습니다.
rollbackWindow
→ step index를 끝으로 이동
→ AnalysisRun skip
→ 새 ReplicaSet은 아직 일부만 Available
→ 이전 weight 역탐색
→ 마지막 setWeight: 100 선택
→ 준비되지 않은 ReplicaSet에 100% traffic이번 글의 처방은 그 역탐색 결과를 바꾸는 것입니다.
5 → pause → 100을
5 → pause로 바꾸면 정상 배포의 최종 결과는 그대로입니다.
하지만 rollbackWindow에서는
이전 weight = 100이 아니라
이전 weight = 5가 됩니다.
즉 마지막 setWeight: 100 제거의 의미는 최종 traffic을 100%로 만들지 않겠다는 것이 아닙니다.
100%를 적용할 시점을 step 안에서 강제로 앞당기지 않겠다는 것입니다.
실제 적용에서는 다음을 함께 가져갑니다.
- 마지막
setWeight: 100제거 - 서비스 override까지 별도 점검
- 마지막으로 남는 weight와 초기 Canary 용량의 관계 검증
- CI에서 위험한 step 패턴 검출
- rollback drill을 Controller 업그레이드 회귀 테스트에 포함
- 인시던트 런북에
retry,promote,promote --full의 차이 명시
그리고 upstream에서 Canary Available에 맞춰 traffic weight를 제한하는 수정이 실제 릴리스되고 모든 클러스터에 적용되면 이 workaround를 다시 평가할 수 있습니다.
9. 남는 위험
이 해결 방법은 현재 Controller가
ReplicaSet이 준비되지 않았을 때 이전
setWeight를 역탐색한다.
는 동작을 한다는 전제 위에 있습니다.
Controller 버전이 올라가면서 이 로직이나 rollbackWindow의 처리 순서가 바뀌면 같은 values가 다른 의미를 가질 수 있습니다.
따라서 Argo Rollouts 업그레이드에서는 일반 Canary 성공 여부만 확인해서는 부족합니다.
최소한 다음 시퀀스를 회귀 테스트로 유지해야 합니다.
정상 배포
→ 이전 revision으로 rollback
→ rollbackWindow 진입 확인
→ 초기 ReplicaSet 준비 중 traffic weight 확인
→ 전체 ReplicaSet 준비
→ promotion
→ abort / promote --full 탈출 경로 확인특히 여러 클러스터에서 서로 다른 Argo Rollouts 버전을 사용한다면 values가 같아도 동작이 같다고 가정하면 안 됩니다.
최종적으로 필요한 것은 특정 설정 한 줄보다 Rollout의 traffic weight와 ReplicaSet capacity가 항상 함께 검증되는 구조입니다.
이번 수정은 upstream에서 그 관계를 직접 보장하기 전까지 사용하는 방어선입니다.