fix(kubernetes): make MinIO report its health and fail loudly on bucket setup - #7272
fix(kubernetes): make MinIO report its health and fail loudly on bucket setup#7272corneliusroemer-agent wants to merge 5 commits into
Conversation
…et setup The bucket is created by a postStart hook that backgrounds itself with `&`, so nothing can observe whether it worked, and MinIO declares no probes, so the pod is Ready the moment the container starts. A failed bucket creation therefore leaves a Ready MinIO with no bucket and no trace anywhere. Run the hook in the foreground with `set -e` and replace the fixed `sleep 10` with a bounded retry, so a failure kills and restarts the container instead of vanishing. Add liveness and readiness probes on MinIO's health endpoint. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WiCjBAPT6cG6qHr6r5dqzA
Readiness is what fixes the bug: it is the field wait_for_pods_to_be_ready.py reads, and its absence is why a MinIO with no bucket counted as ready. Liveness is not worth its risk here. Whether kubelet begins probing before the postStart hook returns is not something I could verify offline, and the two probes fail very differently if it does: a readiness probe that cannot reach MinIO only delays Ready, while a liveness probe kills the container after 30s, on exactly the loaded nodes this change exists to survive. Restarting a single-replica ephemeral standin also recovers nothing that the postStart retry does not already handle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WiCjBAPT6cG6qHr6r5dqzA
|
Claude finished @corneliusroemer's task in 3m 48s —— View job Review:
|
There was a problem hiding this comment.
🟡 Changes recommended
The current hook is not idempotent on bucket creation (can restart-loop when the bucket already exists) and the readiness probe points at MinIO’s liveness endpoint rather than its readiness endpoint.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR improves the reliability and observability of the dev/CI MinIO deployment by ensuring the bucket setup is performed deterministically during container startup and by adding a Kubernetes readiness probe so the Pod doesn’t appear Ready prematurely.
Changes:
- Added a MinIO
readinessProbeso Kubernetes can gate readiness on MinIO’s health endpoint. - Reworked the
postStarthook to run in the foreground withset -e, polling until MinIO responds (with a timeout) before creating/configuring the bucket.
File summaries
| File | Description |
|---|---|
| kubernetes/loculus/templates/minio-deployment.yaml | Adds readiness probing and makes bucket creation/setup a blocking, fail-fast postStart step to prevent “Ready but unusable” MinIO pods |
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| readinessProbe: | ||
| httpGet: | ||
| path: /minio/health/live | ||
| port: 9000 | ||
| periodSeconds: 10 | ||
| timeoutSeconds: 5 | ||
| failureThreshold: 3 |
There was a problem hiding this comment.
The readiness probe now gates loculus-minio-service endpoints (minio-service.yaml:10-15 selects these same labels), so a probe flap doesn't just change what wait_for_pods_to_be_ready.py sees — it pulls MinIO out of the Service mid-test.
That matters because MinIO requests only cpu: 10m (values.yaml:3035-3040). On the same contended runners this PR is fixing for, a CPU-starved MinIO can miss a 5 s probe timeout; three misses (30 s) drop the endpoint and the backend gets connection refused instead of a slow response. That's a failure mode the old probe-less config didn't have.
Worth being more forgiving, since nothing here needs fast NotReady detection on a single dev replica:
| readinessProbe: | |
| httpGet: | |
| path: /minio/health/live | |
| port: 9000 | |
| periodSeconds: 10 | |
| timeoutSeconds: 5 | |
| failureThreshold: 3 | |
| readinessProbe: | |
| httpGet: | |
| path: /minio/health/live | |
| port: 9000 | |
| periodSeconds: 10 | |
| timeoutSeconds: 5 | |
| failureThreshold: 6 |
There was a problem hiding this comment.
Taken, failureThreshold is now 6. Nothing here needs fast NotReady detection on a single dev replica, so the slower detection costs nothing and the flap risk you describe is real.
…etup Pin the image to the release `latest` currently points at (same digest, sha256:14cea49). That is worth more than reproducibility here: Kubernetes defaults imagePullPolicy to Always for a `latest` tag and IfNotPresent for any other, so pinning alone takes Docker Hub out of the container-restart path, which this PR made the failure path. Put the whole bootstrap inside the retry loop. Previously only the connectivity check retried, so a transient `mc mb` or `mc anonymous` failure exited under `set -e` and cost a full container restart. Both are idempotent, so retrying them in place is free and the loop gets simpler rather than more complex. Spell out `--ignore-existing` instead of its short form `-p`; a reviewer read `-p` as `--parents` and thought bucket creation was not idempotent. Use `/minio/health/ready` rather than the liveness endpoint. In this deployment the two are identical (readiness only additionally checks KMS and etcd, neither configured), but the named-for-the-job endpoint is clearer. Raise the readiness failureThreshold to 6. Nothing needs fast NotReady detection on a single dev replica, and MinIO requests only 10m CPU, so on a contended runner a missed probe should not pull it from the Service. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WiCjBAPT6cG6qHr6r5dqzA
Why
In run 33881830926 28 integration tests failed in one chromium job because the MinIO bucket did not exist. The pod reported
1/1 Runningwith 0 restarts, andwait_for_pods_to_be_ready.pypassed.Two things combine to hide it. The bucket is created by a
postStarthook that backgrounds itself with( … ) &, so nothing can fail if it goes wrong and kubelet discards its output; and its entire readiness strategy issleep 10, so if MinIO is not serving ten seconds after the container starts,mc alias setfails andmc mbnever runs. That run was on a heavily loaded runner — postgres took 33.9 s to pull onto the same node. MinIO also declares no probes at all, so it is Ready the instant the process starts, which is why a bucketless MinIO sailed through the wait step.What this changes
The hook now runs in the foreground under
set -e, polling until MinIO answers instead of sleeping a fixed ten seconds, and gives up after 120 s. A failure now kills the container and restarts it, which is both self-healing and visible as a restart count. MinIO also gets a readiness probe on its health endpoint, so the pod stops claiming to be Ready before it is.Things worth knowing
postStartblocking is deliberate and safe here: kubelet does not report the container as Running until the hook returns, but the MinIO process itself is already running and serving, so the hook's own poll is what waits. There is no deadlock, and the probes only start once the hook has proved the API answers.This was a separate
minio/mcsidecar container until #3978 replaced it with the backgrounded hook. I kept the hook rather than going back to a sidecar or moving to a post-installJob, because a Job brings Helm hook semantics and Argo CD sync-phase behaviour for what is a dev- and CI-only path (s3.enabledandrunDevelopmentS3).I used
/minio/health/live, which is what MinIO's own chart does.I deliberately did not add a liveness or startup probe. Readiness is the field
wait_for_pods_to_be_ready.pyactually reads. DuringpostStartthe status still says the container is starting, and liveness, readiness and startup probes are all inert.That has a nice consequence for this PR: because the hook now runs in the foreground, the container cannot be reported Running — and therefore cannot be Ready — until the bucket exists. It also explains the original bug precisely. With
( … ) &the hook returned in milliseconds, so the container went Running immediately, and with no readiness probe a running container is Ready by definition. The pod was Ready whilesleep 10; mc mbwas still running detached, or had failed.The image pin is part of the same fix
I originally left
image: minio/minio:latestfor a follow-up, and that was the wrong call once the hook fails fast: bucket problems now become container restarts, and with alatesttag Kubernetes defaultsimagePullPolicytoAlways, so every restart re-pulls from Docker Hub — on the loaded runner this PR is about.Pinning is the whole fix, with no new field: Kubernetes defaults the pull policy to
IfNotPresentfor any tag that is notlatest. And it is behaviour-neutral today —latestandRELEASE.2025-09-07T16-13-09Zare the same digest,sha256:14cea49..., which is also the digest CI has been running. MinIO's community image is not moving anyway, so a pin costs nothing in updates.🚀 Preview: https://fix-minio-startup-probe-a.loculus.org