Repository navigation
infra: add the API manifests (2 pods, probes, zero-downtime rollout) - #15
Conversation
AbdikhaniAbdirashid
left a comment
There was a problem hiding this comment.
Approving. The manifests are careful, the comments explain the reasoning rather
than restating the YAML, and everything I could check holds up. Five things below,
none blocking.
What I verified against the cluster and the code
- Namespace
relayexists,letsencrypt-prodisReady, and thehelloIngress
really does holdapi.myrelay.tech— so the hand-over you describe for 1.8 is a
real step, not a hypothetical. preStop.sleepneeds Kubernetes 1.29+; we're on v1.36.5, fine./health/readydoes not touch the database yet, andconfig.tshas defaults
forPORT,GIT_SHAandLOG_LEVEL. So pods can go ready with no env block and
no Postgres, which is what this PR needs today.server.tslistens on0.0.0.0:8080, matchingcontainerPort: 8080, handles
SIGTERM viaapp.close(), and the Dockerfile's exec-form CMD makes Bun PID 1, so
it actually receives the signal.- The image pulls anonymously. I tried
ghcr.io/devinder-dev/relay-api:<main sha>with no credentials and got a manifest back, so the package is already
public and noimagePullSecretis needed. That was the biggest deploy risk, and
it's already handled. runAsUser: 1000is right. I pulled the 3.4 kB layer from
oven/bun:1.4.4-slimthat carries/etc/passwdand read it:
bun:x:1000:1000::/home/bun:/bin/sh. MatchesrunAsUserandrunAsGroup.
Five things
-
The namespace isn't in git.
relaywas created by hand. These manifests use
it but don't create it, sokubectl apply -f k8s/app/against a rebuilt cluster
fails on the first file. Does the runbook in #17 cover it, or should a
namespace.yamllive here? It's the one gap between "the cluster works" and
"the repo can rebuild the cluster". -
Readiness and the database, in Phase 2. Harmless today. But once
/health/readychecks Postgres,periodSeconds: 5 × failureThreshold: 3means
both pods leave the Service about 15 s into any database blip, and Traefik
answers 503 for everything. Readiness tied to a shared dependency turns a slow
database into a total outage. Worth deciding before Phase 2 rather than after:
either keep the DB check out of readiness, or raisefailureThresholda lot. -
Two replicas on one node. I checked, the cluster is a single node, so both
pods always land together. That's not a flaw — we have one VPS — but it means
the two replicas buy us process-level resilience and zero-downtime rollouts, not
high availability. I'd rather we both say that precisely than get caught
claiming HA. -
No
startupProbe.initialDelaySeconds: 15is plenty for Bun today. When
the database connection lands in Phase 2 and startup gets slower, liveness can
start killing a pod that is merely still booting. AstartupProbeis the clean
fix and costs three lines. -
terminationGracePeriodSecondsis implicit. The default 30 s minus the 5 s
preStopleaves 25 s for in-flight requests, and the webhook timeout in
CONTEXT.mdis 10 s, so the numbers work. But those three values are coupled and
nothing says so. One comment, or set it explicitly, so a future change to one
doesn't silently break the other.
One thing I liked
Keeping metrics on a separate port 9090 rather than the http port. The Ingress is
path: / Prefix, so metrics on the http port would have been exposed to the
internet, against our own rule in CONTEXT.md. Easy to get wrong, and you didn't.
Questions
- For 1.8: can deleting the
helloIngress go in the runbook, or does it have to
stay a manual step at deploy time? A manual step we only do once is the kind of
thing we'll have forgotten by the demo. readOnlyRootFilesystem: truewith noemptyDirfor/tmp— does Bun ever
write there under load? You tested the health endpoints; I'm wondering about the
k6 run in Phase 8.
|
Thanks, checking
Your questions
Merging now. Thanks for the careful review. |
Step 1.8: every merge to main is deployed, then checked from outside. Rebased onto main now that #12, #14 and #15 are merged. **Merge only after the switch-over below.** ## What changes - **`api.yml` → `pipeline.yml`.** It now deploys too, and the dashboard's jobs will join it later (`needs:` only works inside one workflow file). - **No path filter on `main`.** Every merge is a release: a deploy needs an image tagged with exactly the merged commit, even when only `k8s/app/` or the docs changed. - **New job `deploy-script-test`** on every PR and merge. It runs `infra/server/test-relay-deploy.sh` (relay-deploy's gates and rollback, with fake git and kubectl) and `shellcheck` on the server scripts. The deploy needs it, so a broken deploy script never reaches the server. - **New job `deploy`**, after `api-image` and `deploy-script-test`, only on a merge to main: - **`environment: production`:** the only way to get `DEPLOY_SSH_KEY`, and only on main. - **`permissions: {}`:** no GitHub access at all. - **`timeout-minutes: 30`:** the worst case is the server's lock (15 min, #19) plus a rollout and a rollback. - **Step 1:** writes the key and our server's `known_hosts`, then `ssh … deploy@<vps> "$GITHUB_SHA"` (`StrictHostKeyChecking=yes`, `BatchMode=yes`). The exit code of `relay-deploy` becomes the step's. - **Step 2:** asks `https://api.myrelay.tech/version` until it answers this commit, for up to 36 × 5 s, because the first deploy also waits for a new certificate. - **`timeout-minutes: 10`** on `api-test` and `api-image` (from the #12 review). - **`k8s/hello/` is deleted**, together with its row in `k8s/README.md`. The API's Ingress takes over `api.myrelay.tech`. ## Tested - `actionlint` 1.7.7 (with shellcheck on the `run:` scripts): no findings. - The new job's two steps, run locally: `26 passed, 0 failed`, and `shellcheck` is clean. - The version check, run on its own with `bash -eo pipefail` against a local container: - the right SHA → passes; - an old SHA → keeps asking, then fails; - nothing answering → "no answer" on every try, then fails. - The rebase was rehearsed in a throwaway clone first: the 3 commits replayed onto main without conflicts. ## Already done before merging - #12, #14 and #15 merged. - `deployer.yaml` applied: the deployer can read events, and still can't read Secrets or delete. - `relay-deploy` installed, byte-identical to main. - The image is pullable without credentials (ADR 0004, #18), and the server has already pulled `relay-api:a8274c1`. ## Switch-over (right before merging) 1. Delete the hello app from `default`: the Ingress, Service and Deployment, plus its leftover TLS Secret `api-myrelay-tech-tls`. `api.myrelay.tech` is down for about 1–2 minutes, once. 2. Merge → the pipeline tests, builds, deploys → `/version` answers the commit. ## After the first deploy - **A real rollback:** deploy #15's merge commit `a78934c` by hand. It has no image, so the rollout fails and `relay-deploy` redeploys the last good commit, while a curl loop shows only 200s. - **Zero downtime:** a load loop during a normal deploy (merging #19) should show no failed requests. - Then make `api-test` and `deploy-script-test` required checks.
Step 1.7: the API's Kubernetes manifests in
k8s/app/. This PR doesn't deploy anything. The pipeline's deploy job comes in 1.8, once #12 (the image) and #14 (rollback) are merged.What's in it
deployment.yaml:maxSurge: 1, maxUnavailable: 0, so there are never fewer than 2 ready pods (VG2).relay-api:__GIT_SHA__, filled in byrelay-deploy.preStopsleep of 5 s.service.yaml: port 80 → the pods' port namedhttp.ingress.yaml:api.myrelay.tech, with aletsencrypt-prodcertificate in namespacerelay.The probes
/health/readyevery 5 s. After 3 failures (about 15 s), the pod is taken out of the Service but keeps running./health/liveevery 20 s, starting after 15 s. After 3 failures (about 60 s), the container is restarted.Liveness is the more patient one because a restart is more drastic than losing traffic. It also never checks the database: a database outage must not restart every pod.
Why
runAsUser: 1000The image says
USER bun, which is a name. Kubernetes can only verifyrunAsNonRootagainst a number. With just the name, the pod would fail withCreateContainerConfigError.Tested
deployeraccount itself (sudo -u deploy kubectl … --dry-run=server): all three files were accepted. A dry run is authorized like a real request, so this proves the pipeline may apply exactly these files.path:fromdeployment.yamland curls the running container:200 /health/ready,200 /health/live. The dry run alone can't catch a wrong path: it also passed with/readyand/health, which both answer 404.At the first deploy (1.8)
The hello app in
defaultalso claimsapi.myrelay.tech, so we delete it by hand at that moment, because two Ingresses for one host would compete. cert-manager then issues a new certificate for namespacerelay.Added after an independent review of Phases 0–1
automountServiceAccountToken: false: the API never talks to Kubernetes, so its pods get no token at all.readOnlyRootFilesystem: true: tested with the real image (--read-only --cap-drop ALL --user 1000:1000)./health/ready,/health/liveand/versionanswer 200, anddocker stopexits 0 in 0.15 s.