Flux HelmRelease Stalled — 대기 타임아웃 한 번에 멈춘 릴리스
Keycloak PostgreSQL 의 CPU limit 을 올리는 values 변경을 머지했지만, StatefulSet 에는 예전 값이 그대로 남아 있었습니다.
FluxCD 로 배포하는 클러스터라 values 는 ConfigMap 을 거쳐 HelmRelease 가 helm upgrade 로 반영합니다. 그 경로에서 HelmRelease 하나만 멈춰 있었고, 에러도 알림도 없었습니다.
GitRepository 새 커밋을 당겨 옴 OK
Kustomization values ConfigMap 적용 OK
ConfigMap 새 limit 값이 들어가 있음 OK
HelmRelease Stalled — 재시도 안 함 ✗
StatefulSet 예전 값 그대로 ✗
Stalled 에 찍힌 시각과 실제로 막힌 시각
HelmRelease 조건은 이랬습니다.
[Stalled] RetriesExceeded: Failed to upgrade after 1 attempt(s) (2026-06-18)
[Released] UpgradeFailed: Helm upgrade failed for release keycloak/keycloak
with chart keycloak@24.4.14: context deadline exceeded (2026-03-25)
Stalled 는 6월로 찍혀 있지만 막힌 건 그때가 아닙니다. 6월 18일은 클러스터 노드를 전면 교체한
날이고 helm-controller 가 재기동하면서 조건을 다시 평가해 새 시각으로 기록한 것이었습니다. 실제로
막힌 건 3월 업그레이드가 context deadline exceeded 로 실패한 때부터라, 발견한 9월까지 170일이
지나 있었습니다.
helm 릴리스 이력도 같은 시점을 가리킵니다.
sh.helm.release.v1.keycloak.v12 deployed 2026-01-21
sh.helm.release.v1.keycloak.v13 failed 2026-03-25
매니페스트는 이미 적용돼 있었음
3월에 실패한 v13 을 릴리스 시크릿에서 풀어 v12 와 비교하면, 바꾸려던 건 Keycloak StatefulSet 의
requests.cpu 를 1 에서 500m 으로 내리는 것 하나뿐이었고, 라이브 StatefulSet 과 파드는 이미
500m 이었습니다.
helm 은 매니페스트를 먼저 적용하고 그다음에 리소스가 Ready 가 될 때까지 기다립니다. v13 은 적용을
마친 뒤 기다리는 동안 기본 대기 시간 5분을 넘겨 failed 로 찍힌 것이었습니다. 3월에 왜 5분을
넘겼는지는 당시 이벤트가 남아 있지 않아 확인하지 못했습니다.
HelmRelease spec 에는 timeout 도 upgrade.remediation 도 없었습니다. 대기 5분에 재시도 0회라,
타임아웃 한 번이면 거기서 끝나는 설정입니다.
spec:
chart:
spec: { chart: keycloak, version: 24.4.14, sourceRef: { kind: HelmRepository, name: bitnami } }
interval: 30m
valuesFrom:
- kind: ConfigMap
name: keycloak-values
스스로 안 풀린 이유 — 두 겹
재시도가 0회라도 interval: 30m 이 있으니 30분마다 다시 시도할 것처럼 보이지만 막고 있던 게 두
겹이었습니다.
첫째는 리컨사일 자체가 돌지 않은 것입니다. helm-controller 는 RetriesExceeded 를 terminal error 로
돌려주고 그러면 spec.interval 에 따른 재큐가 걸리지 않습니다. 이틀 남짓한 helm-controller 로그에서
keycloak 줄은 하나도 없었습니다. 30분 주기라면 110번쯤 찍혀 있어야 하는 자리입니다.
둘째는 리컨사일이 돌더라도 업그레이드를 다시 시도할 조건이 없었던 것입니다. 실패 카운터가 지워져야 다시 시도하는데, 그 조건이 하나도 맞지 않았습니다. helm-controller(v1.4.3)는 아래 넷 중 하나가 바뀌어야 카운터를 리셋합니다.
① spec 변경 lastAttemptedGeneration != generation
② 차트 버전 변경 lastAttemptedRevision != chart version
③ values 변경 config digest 불일치
④ 명시적 리셋 reconcile.fluxcd.io/resetAt 어노테이션
values 를 valuesFrom 의 ConfigMap 으로 넘기는 구성은 Git 에서 values 를 바꿔도 HelmRelease spec
이 그대로여서 ①이 생기지 않습니다. 차트 버전은 고정해 둬서 ②도 없고 ④는 사람이 걸어야 합니다.
남는 건 ③뿐인데 3월 이후 keycloak values 를 바꾼 커밋이 한 건도 없었으니, 170일 동안 풀릴 계기가
없었던 겁니다.
실제로 푼 것 — values 변경과 requestedAt
이번엔 values 를 막 바꾼 참이라 ③이 성립해 있었고 남은 건 리컨사일을 한 번 돌리는 것이었습니다.
kubectl -n keycloak annotate helmrelease keycloak \
reconcile.fluxcd.io/requestedAt="$(date -u +%Y-%m-%dT%H:%M:%SZ)" --overwrite
helm-controller 로그와 릴리스 이력을 시간순으로 맞춰 보면 이렇습니다.
04:20:16Z ConfigMap 갱신 → 리셋 조건 ③ 성립
04:32:08Z requestedAt 어노테이션 → 리컨사일 발생
04:32:11Z "release conditions have changed since last failure"
04:32:26Z upgrade 성공 (v14 deployed)
ConfigMap 이 바뀐 뒤 12분 동안 helm-controller 로그에 keycloak 이 한 줄도 없는 게 첫째 겹의
증거로, values 가 바뀌어도 리컨사일은 돌지 않았던 겁니다. 반대로 values 변경 없이 어노테이션만
걸었다면 리컨사일은 돌아도 리셋 조건이 안 맞아 그대로 RetriesExceeded 로 돌아갔을 겁니다. 둘 다
있어야 했고 이번엔 우연히 둘 다 있었습니다.
requestedAt 은 리컨사일을 당기기만 할 뿐 generation 을 올리지 않아서 그것만으로는 카운터가
지워지지 않습니다. 바꿀 values 없이 재시도만 시키려면 resetAt 을 같이 걸고 두 값을 반드시 같게
맞춥니다. helm-controller 는 requestedAt 과 값이 같을 때만 resetAt 을 처리해서 $(date) 를
따로 두 번 부르면 1초 차이로 리셋이 안 걸리고, 그 타임스탬프는 처리된 것으로 기록돼 같은 값을 다시
걸어도 무시됩니다.
TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
kubectl -n keycloak annotate helmrelease keycloak --overwrite \
reconcile.fluxcd.io/requestedAt="$TS" \
reconcile.fluxcd.io/resetAt="$TS"
다시 돌리기 전에 대조한 것
반년 묵은 릴리스를 다시 돌리면 그사이 쌓인 변경이 한꺼번에 나갈 수 있어서 어노테이션을 걸기 전에
네 가지를 맞춰 봤습니다. 실패한 v13 의 values 와 현재 ConfigMap 의 차이는 이번에 고친 4줄뿐이었고
v13 매니페스트가 관리하는 오브젝트 16개를 라이브와 비교하니 cpu: 2 와 cpu: "2" 같은 표기 차이
하나만 나왔습니다. bitnami 차트에서 자주 사고가 나는 비밀번호 재생성은 비밀번호를 values 에 고정해
둔 구성이라 해당이 없었고, StatefulSet 의 volumeClaimTemplates 같은 불변 필드도 그대로였습니다.
업그레이드는 20초 만에 끝났고 그사이 PostgreSQL 파드가 재생성됐지만 Keycloak 파드는 readiness 를 잃지 않고 엔드포인트에 그대로 남아 있었습니다.
같은 상태를 찾는 법과 막는 설정
하나가 이렇게 멈춰 있으면 다른 것도 그럴 수 있어서 클러스터마다 훑었고, 운영 클러스터에서 같은 상태로 158일, 423일째 멈춰 있던 HelmRelease 가 네 개 더 나왔습니다.
kubectl get helmrelease -A -o json | jq -r '.items[]
| select(.status.conditions[]? | select(.type=="Stalled" and .status=="True"))
| "\(.metadata.namespace)/\(.metadata.name)"'
Stalled 없이 Ready=False 만 있는 건 재큐가 살아 있어 계속 재시도하는 중이라 이 경우와 다릅니다.
HelmRelease 에 대기 시간과 재시도를 주면 타임아웃 한 번에 굳어 버리는 일은 막을 수 있습니다.
spec:
timeout: 10m
upgrade:
remediation:
retries: 2
values 를 머지했는데 반영이 안 되면 HelmRelease 조건부터 열어 봅니다. Stalled 에
RetriesExceeded 가 있으면 기다려도 풀리지 않는 상태이고 언제부터 막혔는지는 Stalled 가 아니라
Released 조건의 시각과 helm 릴리스 이력에서 확인합니다.
댓글 0