Skip to content

Zalando single-instance cluster stuck UpdateFailed after a resource change

Problem

After changing spec.resources (e.g. lowering a memory request) on a single-replica (numberOfInstances: 1) Zalando postgresql CR, the operator sets status.PostgresClusterStatus: UpdateFailed and ArgoCD reports the application Degraded.

The database is actually fine: the postgres pod is Running, Patroni reports it Leader/running with 0 restarts, the new resource values are applied to the StatefulSet and pod, and clients connect normally. Only the operator's status is stuck. A periodic resync does not reliably clear it.

This is distinct from the stale-DCS deadlock, which happens after a PVC recreate; here the data dir and DCS are untouched.

Root Cause

To apply a pod-spec change such as resources, the operator must recreate the pod. With a single instance there is no replica to fail over to:

cannot perform switch over before re-creating the pod: no replicas
recreating old master pod "<ns>/<cluster>-0"

The operator deletes the sole master and waits for the old pod object to disappear. The StatefulSet controller immediately recreates a pod with the same name, so the operator's deletion-wait cannot confirm the old pod is gone and eventually times out (~10 minutes):

could not sync statefulsets: could not recreate pods: could not recreate old master pod
"<ns>/<cluster>-0": pod deletion wait timeout

The recreate itself succeeded — the new pod carries the updated resources — but the operator's bookkeeping ended in error, leaving UpdateFailed latched on the CR.

How to Diagnose

# Operator logs show the no-replica recreate + deletion timeout
kubectl --context=grigri -n postgres-operator logs deploy/postgres-operator --tail=2000 \
  | grep -i '<cluster>' | grep -iE 'no replicas|recreat|deletion wait|updated'

# CR status is latched
kubectl --context=grigri -n <ns> get postgresql <cluster> \
  -o jsonpath='{.status.PostgresClusterStatus}{"\n"}'   # -> UpdateFailed

# But the pod is healthy AND the change is applied
kubectl --context=grigri -n <ns> exec <cluster>-0 -c postgres -- patronictl list
kubectl --context=grigri -n <ns> get pod <cluster>-0 \
  -o jsonpath='{.spec.containers[0].resources}{"\n"}'

If the pod is Leader/running, restarts are not climbing, and the StatefulSet resources already match the CR, the failure is cosmetic bookkeeping — not a real outage.

Fix / Workaround

Restart the postgres-operator to clear the latched state. It resyncs, sees the StatefulSet already matches the desired spec, does nothing destructive, and sets the status back to Running:

kubectl --context=grigri -n postgres-operator rollout restart deploy/postgres-operator
kubectl --context=grigri -n <ns> get postgresql <cluster> \
  -o jsonpath='{.status.PostgresClusterStatus}{"\n"}'   # -> Running

This restarts the control-plane operator only; it does not touch the database pods or data. It does briefly pause reconciliation for all Zalando clusters, so prefer it over deleting/recreating the postgres pod.

Prevention

Resource/spec changes on a single-instance cluster inherently require recreating the sole master (a brief blip) and can trip this deletion-wait timeout. Where practical, batch resource changes, or expect a transient UpdateFailed that needs an operator restart to clear. Multi-instance clusters can switch over to a replica and avoid the same-name deletion-wait confusion.