fix: [alert] KubePodCrashLooping (apps) (#424) #425

Merged
k.renner merged 1 commits from alert-fix/issue-424 into master 2026-07-27 10:37:55 +00:00
Member

What this PR does

external-dns-unifi in namespace apps is in CrashLoopBackOff — the external-dns
container exits 1 roughly every 70s of uptime while the webhook sidecar stays up. The
issue attributed this to the UDM's intermittent 401: User not authenticated responses
and suggested the UNIFI_API_KEY may be stale. That diagnosis is wrong. external-dns
v0.21.0 classifies a webhook HTTP 5xx (which is what the sidecar returns when the UDM
401s or 502s) as a soft error and retries on the next cycle — verified live this run.
The only failure that is fatal is a client-side timeout on GET /records, which is a
hard error → log.Fatalf → exit 1 → CrashLoopBackOff. The UDM's static-dns endpoint
has simply gotten slower than the current --webhook-provider-read-timeout=60s ceiling
(reads observed completing at ~70s), so this raises that timeout to 180s.

Changes

  • kubernetes/infrastructure/external-dns-unifi/helmrelease.yaml — raise
    --webhook-provider-read-timeout from 60s to 180s, and rewrite the adjacent
    comment to record the soft-error-vs-hard-error distinction that makes this the right
    knob (the previous comment predated the soft-error behaviour and only described the
    slow-read case).

Verification

Everything below was observed live this run via ./.ci-diag/kubectl.

  1. The crash loop is ongoing (the alert had already "resolved" in Alertmanager):
    get pods -n apps -o wide → external-dns-unifi-89f464b66-98jjw 1/2 CrashLoopBackOff 18 (2m37s ago) 5h48m
    (19 restarts by the end of this run, up from the 13 recorded in the issue).

  2. Only the external-dns container crashes; the sidecar is healthy.
    get pod ... -o jsonpath={.status.containerStatuses} →
    external-dns: exitCode:1, reason:"Error", startedAt:11:37:51Z, finishedAt:11:39:01Z;
    webhook: restarts=0, running since 05:52:49Z. Container lifetime is 70s — ~10s of
    startup plus exactly one 60s read timeout.

  3. The fatal error is a client timeout, not an auth error.
    logs ... -c external-dns --previous →
    level=fatal msg="Failed to do run once: Get \"http://localhost:8888/records\": context deadline exceeded (Client.Timeout exceeded while awaiting headers)"
    The same log's config dump confirms the current setting is live: WebhookProviderReadTimeout:1m0s.

  4. UDM errors are survivable; this is the key correction.
    logs ... -c external-dns --tail=6 --timestamps on the current container →
    11:41:50 level=error ... "soft error\nfailed to get records with code 500 (consecutive soft errors: 1)"
    11:42:50 level=error ... (consecutive soft errors: 2)
    11:43:51 level=info msg="All records are already up to date"
    11:43:51 level=info msg="Reconciliation succeeded after 2 consecutive soft errors"
    Those two soft errors correspond 1:1 (same second) with the sidecar's UDM failures at
    11:41:50 and 11:42:50 (returned 502). So a 401/502 costs one skipped cycle, not a
    restart.

  5. Slow reads are what kill it, and they land just over 60s.
    logs ... -c webhook --since=12m --timestamps shows entries where the sidecar
    successfully retrieved all 128 records but the client had already gone:
    11:33:00 "provider retrieved records" count:128 immediately followed by
    error encoding records ... write: broken pipe. Same pattern at 11:30:47, 11:30:51,
    11:35:01. Each abandon happens 60s after the request, so the true read time is ~70s —
    over the ceiling, but only just.

How a reviewer confirms the fix. After Flux reconciles the HelmRelease and the pod is
recreated:

  • ./.ci-diag/kubectl get pods -n apps -l app.kubernetes.io/instance=external-dns-unifi —
    expect 2/2 Running with the restart count flat over 30+ min.
  • ./.ci-diag/kubectl logs deploy/external-dns-unifi -n apps -c external-dns --tail=30 —
    expect WebhookProviderReadTimeout:3m0s in the config dump, and any remaining UDM
    trouble to appear as soft error ... code 500 lines followed by
    Reconciliation succeeded, never as level=fatal.
  • KubePodCrashLooping for apps/external-dns-unifi should stop firing. DNS
    reconciliation is already working between crashes (All records are already up to date),
    so no record churn is expected.

Notes

  • What the issue got wrong. It proposed that the UNIFI_API_KEY may be stale/rotated.
    It is not: the same key returns count:128 successfully on most cycles and did so at
    11:43:51, minutes after a 401. A revoked key would fail every time. I did not read the
    Secret (the diag ServiceAccount has no Secrets access) — this is inferred from the
    interleaved successes, which is sufficient to rule out a hard revocation. The issue also
    framed the 401s as the cause of the crash loop; evidence item 4 shows they are not.
  • This does not make the UDM healthy. https://udm.renner.uno really is returning
    intermittent 401: User not authenticated and 502, and its static-dns read time has
    roughly doubled since the 60s value was chosen. That is outside this repo. It is still
    worth a human look at the UDM's own health/logs (controller restarts, backups, or load
    around those timestamps) — but it is now a degraded-reconcile problem, not a
    crash-loop problem, and it will no longer page.
  • This is a ceiling, and the UDM is trending slower. If reads keep growing, this will
    need revisiting. A more durable option is --provider-cache-time (currently
    ProviderCacheTime:0s), which would cut UDM polling from ~60/hour to a handful and
    shrink the exposure window proportionally. I did not do it here: it changes reconcile
    semantics (out-of-band UDM edits would be picked up with a delay), and I could not verify
    the cache-invalidation-on-apply behaviour from read-only tooling. Worth considering
    separately if the timeout bump proves insufficient.
  • Nothing was silenced — no PrometheusRule, threshold, or inhibition was touched. The
    webhook image tag (v0.4.1) was deliberately left alone; image tags are Flux
    image-automation's domain, and the sidecar is not the failing component.

Closes #424

## What this PR does `external-dns-unifi` in namespace `apps` is in `CrashLoopBackOff` — the `external-dns` container exits 1 roughly every 70s of uptime while the `webhook` sidecar stays up. The issue attributed this to the UDM's intermittent `401: User not authenticated` responses and suggested the `UNIFI_API_KEY` may be stale. **That diagnosis is wrong.** external-dns v0.21.0 classifies a webhook HTTP 5xx (which is what the sidecar returns when the UDM 401s or 502s) as a *soft* error and retries on the next cycle — verified live this run. The only failure that is fatal is a **client-side timeout** on `GET /records`, which is a hard error → `log.Fatalf` → exit 1 → `CrashLoopBackOff`. The UDM's `static-dns` endpoint has simply gotten slower than the current `--webhook-provider-read-timeout=60s` ceiling (reads observed completing at ~70s), so this raises that timeout to `180s`. ## Changes - `kubernetes/infrastructure/external-dns-unifi/helmrelease.yaml` — raise `--webhook-provider-read-timeout` from `60s` to `180s`, and rewrite the adjacent comment to record the soft-error-vs-hard-error distinction that makes this the right knob (the previous comment predated the soft-error behaviour and only described the slow-read case). ## Verification Everything below was observed live this run via `./.ci-diag/kubectl`. 1. **The crash loop is ongoing** (the alert had already "resolved" in Alertmanager): `get pods -n apps -o wide` → `external-dns-unifi-89f464b66-98jjw 1/2 CrashLoopBackOff 18 (2m37s ago) 5h48m` (19 restarts by the end of this run, up from the 13 recorded in the issue). 2. **Only the `external-dns` container crashes; the sidecar is healthy.** `get pod ... -o jsonpath={.status.containerStatuses}` → `external-dns`: `exitCode:1, reason:"Error", startedAt:11:37:51Z, finishedAt:11:39:01Z`; `webhook`: `restarts=0`, running since `05:52:49Z`. Container lifetime is 70s — ~10s of startup plus exactly one 60s read timeout. 3. **The fatal error is a client timeout, not an auth error.** `logs ... -c external-dns --previous` → `level=fatal msg="Failed to do run once: Get \"http://localhost:8888/records\": context deadline exceeded (Client.Timeout exceeded while awaiting headers)"` The same log's config dump confirms the current setting is live: `WebhookProviderReadTimeout:1m0s`. 4. **UDM errors are survivable; this is the key correction.** `logs ... -c external-dns --tail=6 --timestamps` on the *current* container → `11:41:50 level=error ... "soft error\nfailed to get records with code 500 (consecutive soft errors: 1)"` `11:42:50 level=error ... (consecutive soft errors: 2)` `11:43:51 level=info msg="All records are already up to date"` `11:43:51 level=info msg="Reconciliation succeeded after 2 consecutive soft errors"` Those two soft errors correspond 1:1 (same second) with the sidecar's UDM failures at `11:41:50` and `11:42:50` (`returned 502`). So a 401/502 costs one skipped cycle, not a restart. 5. **Slow reads are what kill it, and they land just over 60s.** `logs ... -c webhook --since=12m --timestamps` shows entries where the sidecar *successfully* retrieved all 128 records but the client had already gone: `11:33:00 "provider retrieved records" count:128` immediately followed by `error encoding records ... write: broken pipe`. Same pattern at `11:30:47`, `11:30:51`, `11:35:01`. Each abandon happens 60s after the request, so the true read time is ~70s — over the ceiling, but only just. **How a reviewer confirms the fix.** After Flux reconciles the HelmRelease and the pod is recreated: - `./.ci-diag/kubectl get pods -n apps -l app.kubernetes.io/instance=external-dns-unifi` — expect `2/2 Running` with the restart count flat over 30+ min. - `./.ci-diag/kubectl logs deploy/external-dns-unifi -n apps -c external-dns --tail=30` — expect `WebhookProviderReadTimeout:3m0s` in the config dump, and any remaining UDM trouble to appear as `soft error ... code 500` lines followed by `Reconciliation succeeded`, never as `level=fatal`. - `KubePodCrashLooping` for `apps/external-dns-unifi` should stop firing. DNS reconciliation is already working between crashes (`All records are already up to date`), so no record churn is expected. ## Notes - **What the issue got wrong.** It proposed that the `UNIFI_API_KEY` may be stale/rotated. It is not: the same key returns `count:128` successfully on most cycles and did so at `11:43:51`, minutes after a 401. A revoked key would fail every time. I did not read the Secret (the diag ServiceAccount has no Secrets access) — this is inferred from the interleaved successes, which is sufficient to rule out a hard revocation. The issue also framed the 401s as the cause of the crash loop; evidence item 4 shows they are not. - **This does not make the UDM healthy.** `https://udm.renner.uno` really is returning intermittent `401: User not authenticated` and `502`, and its `static-dns` read time has roughly doubled since the 60s value was chosen. That is outside this repo. It is still worth a human look at the UDM's own health/logs (controller restarts, backups, or load around those timestamps) — but it is now a degraded-reconcile problem, not a crash-loop problem, and it will no longer page. - **This is a ceiling, and the UDM is trending slower.** If reads keep growing, this will need revisiting. A more durable option is `--provider-cache-time` (currently `ProviderCacheTime:0s`), which would cut UDM polling from ~60/hour to a handful and shrink the exposure window proportionally. I did not do it here: it changes reconcile semantics (out-of-band UDM edits would be picked up with a delay), and I could not verify the cache-invalidation-on-apply behaviour from read-only tooling. Worth considering separately if the timeout bump proves insufficient. - Nothing was silenced — no PrometheusRule, threshold, or inhibition was touched. The webhook image tag (`v0.4.1`) was deliberately left alone; image tags are Flux image-automation's domain, and the sidecar is not the failing component. Closes #424
issue-fix-bot added 1 commit 2026-07-26 11:46:42 +00:00
fix: [alert] KubePodCrashLooping (apps) (#424)
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
5f7e3c1268
review-bot left a comment
Member

🤖 Automated review — model claude-haiku-4-5-20251001 (complexity: trivial), commit 5f7e3c1268367f7f37776727fb389a90c560c4bb.

Advisory review by the review-bot pipeline. It never approves; a human merges. Push new commits to get a fresh review.


Summary

This PR fixes the external-dns-unifi CrashLoopBackOff alert by increasing the webhook read timeout from 60s to 180s. The diagnosis is evidence-based (detailed live logs provided), the fix is proportionate (2.5× headroom over observed 70s read times), and the comment accurately explains the distinction between survivable soft errors (HTTP 5xx) and fatal hard errors (client-side timeout). No blocking issues.

🟢 Nits

Line 116: The timestamp 2026-07-26 in the comment will become outdated if this is revisited later. Consider phrasing it as a relative date ("recent reads observed at") if you expect this to be read months from now, though the specific date is useful for issue traceability and the PR body provides full context.

Line 129–130: Minor: the phrasing "~2.5x headroom" is informal but clear. If this repo prefers more formal style, consider "approximately 2.5× the observed worst-case latency" — though this is a nit and the current phrasing is fine for internal documentation.

Tests

This is a Kubernetes/Helm configuration change affecting a single deployment. The PR body documents comprehensive post-deployment verification steps:

  • Expect pod restart count to remain flat after reconciliation
  • Config dump should show WebhookProviderReadTimeout:3m0s
  • Remaining UDM errors should appear as soft errors followed by "Reconciliation succeeded", never as fatal log.Fatalf

No new tests are needed for a configuration-only change, and the manual verification steps are adequate.

Recommendation

Looks reasonable — no blocking issues. The fix is well-diagnosed and proportionate to the problem.

🤖 **Automated review** — model `claude-haiku-4-5-20251001` (complexity: trivial), commit `5f7e3c1268367f7f37776727fb389a90c560c4bb`. > Advisory review by the `review-bot` pipeline. It never approves; a human merges. Push new commits to get a fresh review. --- ## Summary This PR fixes the `external-dns-unifi` CrashLoopBackOff alert by increasing the webhook read timeout from 60s to 180s. The diagnosis is evidence-based (detailed live logs provided), the fix is proportionate (2.5× headroom over observed 70s read times), and the comment accurately explains the distinction between survivable soft errors (HTTP 5xx) and fatal hard errors (client-side timeout). No blocking issues. ## 🟢 Nits **Line 116:** The timestamp `2026-07-26` in the comment will become outdated if this is revisited later. Consider phrasing it as a relative date ("recent reads observed at") if you expect this to be read months from now, though the specific date is useful for issue traceability and the PR body provides full context. **Line 129–130:** Minor: the phrasing "~2.5x headroom" is informal but clear. If this repo prefers more formal style, consider "approximately 2.5× the observed worst-case latency" — though this is a nit and the current phrasing is fine for internal documentation. ## Tests This is a Kubernetes/Helm configuration change affecting a single deployment. The PR body documents comprehensive post-deployment verification steps: - Expect pod restart count to remain flat after reconciliation - Config dump should show `WebhookProviderReadTimeout:3m0s` - Remaining UDM errors should appear as soft errors followed by "Reconciliation succeeded", never as fatal log.Fatalf No new tests are needed for a configuration-only change, and the manual verification steps are adequate. ## Recommendation Looks reasonable — no blocking issues. The fix is well-diagnosed and proportionate to the problem. <!-- pr-review:sha=5f7e3c1268367f7f37776727fb389a90c560c4bb -->
k.renner merged commit 7eb8677147 into master 2026-07-27 10:37:55 +00:00
Sign in to join this conversation.