|
| 1 | +# /readyz wiring — k8s manifest diff (NOT auto-applied) |
| 2 | + |
| 3 | +## Context |
| 4 | + |
| 5 | +Today's `/healthz` is a shallow liveness probe — DB ping + commit_id only. The |
| 6 | +Brevo silent-rejection bug from 2026-05-20 (and analogous upstream blips) is |
| 7 | +invisible to it. Three repo PRs land `/readyz` with component-by-component |
| 8 | +checks (platform_db, customer_db, provisioner_grpc, brevo, razorpay, redis, |
| 9 | +do_spaces, river) per service. |
| 10 | + |
| 11 | +This file is the k8s manifest diff. **Do not auto-apply.** Apply manually |
| 12 | +after the new images deploy + the existing `/healthz` is verified untouched. |
| 13 | + |
| 14 | +## Why manual |
| 15 | + |
| 16 | +`/healthz` stays wired to `livenessProbe`. The risk is that a misconfigured |
| 17 | +`/readyz` returns 503 every probe → kubelet pulls every pod → outage. Verify |
| 18 | +on one pod first (`kubectl port-forward` + `curl /readyz`) before flipping |
| 19 | +the readinessProbe. |
| 20 | + |
| 21 | +## Diff 1 — `infra/k8s/app.yaml` (api) |
| 22 | + |
| 23 | +```diff |
| 24 | + readinessProbe: |
| 25 | + httpGet: |
| 26 | +- path: /healthz |
| 27 | ++ path: /readyz |
| 28 | + port: 8080 |
| 29 | +- initialDelaySeconds: 5 |
| 30 | +- periodSeconds: 10 |
| 31 | +- failureThreshold: 3 |
| 32 | ++ initialDelaySeconds: 10 |
| 33 | ++ periodSeconds: 15 |
| 34 | ++ timeoutSeconds: 5 |
| 35 | ++ failureThreshold: 3 |
| 36 | + livenessProbe: |
| 37 | + httpGet: |
| 38 | + path: /livez # process-only check; see startupProbe note |
| 39 | + port: 8080 |
| 40 | + initialDelaySeconds: 15 |
| 41 | + periodSeconds: 30 |
| 42 | + failureThreshold: 6 |
| 43 | +``` |
| 44 | + |
| 45 | +**What changes:** readinessProbe now points at `/readyz` (deep check), |
| 46 | +`livenessProbe` stays at `/livez` (shallow). `periodSeconds` bumps from 10s |
| 47 | +to 15s so the per-check cache (10s TTL) saves an upstream call per period. |
| 48 | +`timeoutSeconds: 5` is added explicitly so a slow Brevo probe can't stall |
| 49 | +the probe (the runner's internal `OverallTimeout: 3s` is well within this). |
| 50 | + |
| 51 | +**Why livenessProbe stays at `/livez`:** a Brevo outage must NOT restart the |
| 52 | +api pod. `/livez` is the pure process-up signal — wired correctly today. |
| 53 | + |
| 54 | +## Diff 2 — `infra/k8s/worker/deployment.yaml` |
| 55 | + |
| 56 | +```diff |
| 57 | + readinessProbe: |
| 58 | + httpGet: |
| 59 | +- path: /healthz |
| 60 | ++ path: /readyz |
| 61 | + port: 8091 |
| 62 | +- initialDelaySeconds: 10 |
| 63 | +- periodSeconds: 10 |
| 64 | +- failureThreshold: 3 |
| 65 | ++ initialDelaySeconds: 10 |
| 66 | ++ periodSeconds: 15 |
| 67 | ++ timeoutSeconds: 5 |
| 68 | ++ failureThreshold: 3 |
| 69 | + livenessProbe: |
| 70 | + httpGet: |
| 71 | + path: /healthz # worker serves only /healthz (no /livez) |
| 72 | + port: 8091 |
| 73 | + initialDelaySeconds: 15 |
| 74 | + periodSeconds: 30 |
| 75 | + failureThreshold: 6 |
| 76 | +``` |
| 77 | + |
| 78 | +**Note:** the worker has no Service endpoint to be pulled from, so a 503 |
| 79 | +on `/readyz` is purely an observability signal today (readiness fires the |
| 80 | +NR alert and updates the Prometheus gauge; nothing routes around the |
| 81 | +pod). If a future PR adds a worker Service for in-cluster lookups, the |
| 82 | +readiness gate is already wired. |
| 83 | + |
| 84 | +## Diff 3 — `infra/k8s/provisioner/deployment.yaml` |
| 85 | + |
| 86 | +```diff |
| 87 | + readinessProbe: |
| 88 | + httpGet: |
| 89 | +- path: /healthz |
| 90 | ++ path: /readyz |
| 91 | + port: 8092 |
| 92 | +- initialDelaySeconds: 5 |
| 93 | +- periodSeconds: 10 |
| 94 | +- failureThreshold: 3 |
| 95 | ++ initialDelaySeconds: 10 |
| 96 | ++ periodSeconds: 15 |
| 97 | ++ timeoutSeconds: 5 |
| 98 | ++ failureThreshold: 3 |
| 99 | + livenessProbe: |
| 100 | + httpGet: |
| 101 | + path: /healthz # provisioner serves only /healthz (no /livez) |
| 102 | + port: 8092 |
| 103 | + initialDelaySeconds: 15 |
| 104 | + periodSeconds: 30 |
| 105 | + failureThreshold: 6 |
| 106 | +``` |
| 107 | + |
| 108 | +**Provisioner extra:** the provisioner's gRPC server now registers the |
| 109 | +standard `grpc.health.v1.Health` service so the api's `/readyz` check |
| 110 | +`provisioner_grpc` works (it calls `Check(service="")`). No manifest |
| 111 | +change is required for that — it's a code change inside the provisioner |
| 112 | +binary. |
| 113 | + |
| 114 | +## Rollout order (per CLAUDE.md rule 23 — verify-live each step) |
| 115 | + |
| 116 | +1. **api** (largest blast radius). Build + push + rollout. Verify on |
| 117 | + one pod via `kubectl port-forward -n instant svc/instant-api 8080:8080` |
| 118 | + then `curl http://localhost:8080/readyz | jq .` Expect 200 + overall=ok |
| 119 | + (or degraded if Brevo/Razorpay configuration is in fact broken). |
| 120 | + Then apply Diff 1 and watch `kubectl get pods -n instant -w` for any |
| 121 | + Ready=False flapping. |
| 122 | + |
| 123 | +2. **worker.** Same pattern with port-forward to `svc/instant-worker` |
| 124 | + (which doesn't exist as a Service today — `kubectl port-forward |
| 125 | + pod/<worker-pod> 8091:8091` instead). Apply Diff 2. |
| 126 | + |
| 127 | +3. **provisioner.** `kubectl port-forward -n instant-infra |
| 128 | + svc/instant-provisioner 8092:8092` then `curl |
| 129 | + http://localhost:8092/readyz | jq .` Apply Diff 3. |
| 130 | + |
| 131 | +## Verification commands |
| 132 | + |
| 133 | +```bash |
| 134 | +# 1) api — expect ok + 200, or degraded + 200 if Brevo etc. are configured wrong |
| 135 | +curl https://api.instanode.dev/readyz | jq . |
| 136 | + |
| 137 | +# 2) worker — in-cluster only |
| 138 | +kubectl port-forward -n instant-infra pod/<worker-pod> 8091 & |
| 139 | +curl http://localhost:8091/readyz | jq . |
| 140 | + |
| 141 | +# 3) provisioner — in-cluster only |
| 142 | +kubectl port-forward -n instant-infra svc/instant-provisioner 8092 & |
| 143 | +curl http://localhost:8092/readyz | jq . |
| 144 | + |
| 145 | +# 4) verify /healthz untouched on api (must still be the shallow shape) |
| 146 | +curl https://api.instanode.dev/healthz | jq . |
| 147 | +# expected: ok, service, commit_id, build_time, version, migration_* |
| 148 | +``` |
| 149 | + |
| 150 | +## Rollback |
| 151 | + |
| 152 | +```bash |
| 153 | +# Revert one service: |
| 154 | +kubectl set image -n instant deploy/instant-api app=ghcr.io/instanode-dev/api:<prev-sha> |
| 155 | + |
| 156 | +# Or revert manifest: |
| 157 | +git revert <commit> |
| 158 | +kubectl apply -f infra/k8s/app.yaml |
| 159 | +``` |
| 160 | + |
| 161 | +A pod that flips Ready=False on `/readyz` but Ready=True on the OLD |
| 162 | +`/healthz` is benign — kubelet just stops sending it traffic. Revert |
| 163 | +the readinessProbe path back to `/healthz` to restore the pre-change |
| 164 | +behavior without re-deploying the binary. |
| 165 | + |
| 166 | +## Why readiness, not liveness |
| 167 | + |
| 168 | +`/readyz` checks upstream HTTP APIs (Brevo, Razorpay, DO Spaces). If any of |
| 169 | +those is flapping, a livenessProbe on `/readyz` would SIGKILL every api pod |
| 170 | +and a Brevo outage becomes an api outage. The readinessProbe just pulls the |
| 171 | +pod from the Service endpoint list — no restart, just no new traffic. |
| 172 | + |
| 173 | +The criticality matrix per service (which checks return 503 vs which |
| 174 | +return 200+degraded) is hard-coded in handlers/readyz.go for each repo — |
| 175 | +NOT env-tunable, because a misconfigured matrix is worse than no /readyz. |
| 176 | + |
| 177 | +## Field names + wire shape |
| 178 | + |
| 179 | +```json |
| 180 | +{ |
| 181 | + "overall": "ok" | "degraded" | "failed", |
| 182 | + "service": "instant-api", |
| 183 | + "commit_id": "<sha>", |
| 184 | + "checks": [ |
| 185 | + { |
| 186 | + "name": "platform_db", |
| 187 | + "status": "ok" | "degraded" | "failed", |
| 188 | + "latency_ms": 3, |
| 189 | + "last_check_at": "2026-05-20T08:01:23Z", |
| 190 | + "last_error": "" |
| 191 | + }, |
| 192 | + ... |
| 193 | + ] |
| 194 | +} |
| 195 | +``` |
| 196 | + |
| 197 | +Sorted by check name alphabetically so jq filters are stable across |
| 198 | +probes. |
0 commit comments