feat(k8s): Helm chart for the whole stack on a single-node cluster (refs #25)

One chart whose values.yaml is a near-literal transcription of
infra/docker-compose.yml, rendered by three generic templates (Deployment, Job,
Service) over a `workloads` map — so the two stacks can be diffed by eye instead
of by archaeology, and adding a service is a values edit.

Platform-forced deviations, each commented where it appears:
- `args`, never `command`: compose replaces the image CMD, Kubernetes replaces the
  ENTRYPOINT. The chart fails to render on `command`, because the symptom (postgres
  refusing to run as root, Keycloak exec-ing `start-dev`) is nothing like the cause.
- The four Django services apply their own setup_configuration in the web pod
  rather than in a separate init Job: both scripts migrate, and without compose's
  depends_on they race the same database.
- OpenZaak and Objecten are addressed by service FQDN, because Django rejects a
  single-label host in a URL — the reason compose passes container IPs around.
- NodePorts, no ingress; databases are emptyDir until persistence.storageClass is
  set, so the stack comes up on a cluster with no CSI driver.

The upstream config inputs stay in the repo and become ConfigMaps via
infra/helm/seed-configmaps.sh — the Kubernetes sibling of infra/seed-config.sh —
so the compose stack and the chart cannot fork. infra/helm/registry.yaml runs an
in-cluster registry because Talos cannot side-load an image and a laptop-side one
needs a root-level firewall change.
This commit is contained in:
not
2026-09-04 17:51:21 +02:00
parent 916d671d49
commit 7a5840149c
12 changed files with 1124 additions and 1 deletions
+66 -1
View File
@@ -43,7 +43,7 @@ export DOCKER_HOST := unix://$(PODMAN_SOCK)
endif
endif
.PHONY: ci lint build unit mutation frontend integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-observability verify-tracing verify-metrics verify-objecttypen verify-objecten verify-registerrecord verify-objecten-notifications verify-notifications smoke up down local verify-local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down k8s-lint help
.PHONY: ci lint build unit mutation frontend integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-observability verify-tracing verify-metrics verify-objecttypen verify-objecten verify-registerrecord verify-objecten-notifications verify-notifications smoke up down local verify-local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down k8s-lint k8s-registry k8s-images k8s-seed k8s-up k8s-reseed k8s-portals k8s-down k8s-purge help
## ci: run the full pipeline — lint, build, unit, mutation, frontend, verify (mirrors Gitea Actions)
## `verify` is the live-stack stage (full stack up once → ACL + notification checks).
@@ -350,6 +350,71 @@ k8s-lint:
helm lint $(K8S_CHART)
helm template big $(K8S_CHART) -n $(K8S_NS) --set images.registry=registry.invalid:5000 >/dev/null
## k8s-registry: deploy the in-cluster image registry (NodePort 30500)
k8s-registry:
kubectl apply -f infra/helm/registry.yaml
kubectl -n registry rollout status deploy/registry --timeout=180s
## k8s-images: build this repo's images (via compose) and push them to $(K8S_REGISTRY)
# `docker save | crane push` rather than `docker push`: the registry speaks plain
# HTTP, which the Docker daemon refuses without a root-level insecure-registries
# entry, while crane just takes --insecure. Install: see docs/runbooks/kubernetes-talos.md.
k8s-images:
@command -v crane >/dev/null || { echo "crane not found — see docs/runbooks/kubernetes-talos.md §0" >&2; exit 2; }
@test -n "$(K8S_REGISTRY)" || { echo "set K8S_REGISTRY=<registry host:port>" >&2; exit 2; }
docker compose -f $(COMPOSE) build $(K8S_IMAGES)
@tar=$$(mktemp -t rr-img-XXXX.tar); \
for i in $(K8S_IMAGES); do \
docker save register-referentie/$$i:dev -o $$tar; \
crane push --insecure $$tar $(K8S_REGISTRY)/register-referentie/$$i:dev; \
done; rm -f $$tar
## k8s-seed: create the ConfigMaps the chart mounts (upstream config + bootstrap scripts)
k8s-seed:
bash infra/helm/seed-configmaps.sh $(K8S_NS)
## k8s-up: seed the config and install/upgrade the release
k8s-up: k8s-seed
@test -n "$(TALOS_HOST)" || { echo "set TALOS_HOST=<node ip>" >&2; exit 2; }
@test -n "$(K8S_REGISTRY)" || { echo "set K8S_REGISTRY=<registry the node can pull from>" >&2; exit 2; }
helm upgrade --install big $(K8S_CHART) -n $(K8S_NS) --create-namespace \
--set host=$(TALOS_HOST) --set images.registry=$(K8S_REGISTRY) $(K8S_SET)
kubectl -n $(K8S_NS) get pods
## k8s-reseed: re-run the bootstrap jobs (after a database was wiped, or after
## changing a Job in the chart — Job pod templates are immutable, so a plain
## `helm upgrade` is rejected)
k8s-reseed:
kubectl -n $(K8S_NS) delete job -l app.kubernetes.io/component=init --ignore-not-found
$(MAKE) k8s-up
# The projection's schema is created on service start (Projection.ReadModel migrates in a
# hosted service), so a wiped database also needs these two restarted — otherwise they keep
# writing to a schema-less DB and fail with `relation "processed_notifications" does not exist`.
kubectl -n $(K8S_NS) rollout restart deploy/event-subscriber deploy/projection-api
kubectl -n $(K8S_NS) rollout status deploy/event-subscriber deploy/projection-api --timeout=180s
## k8s-portals: forward the browser-facing services to localhost (Ctrl-C stops them all)
# The portals' OIDC flow needs a *secure context* for crypto.subtle (PKCE), and browsers
# only grant that to https or localhost — a NodePort on the VM's IP is neither. Forwarding
# to localhost on the same port numbers keeps Keycloak's pinned issuer valid. Deploy with
# TALOS_HOST=localhost for this to line up.
k8s-portals:
@echo "self-service http://localhost:30140 · openbaar :30141 · behandel :30142 · beheer :30143 · keycloak :30180"
@trap 'kill 0' INT TERM; \
for f in self-service:30140:80 openbaar:30141:80 behandel:30142:80 beheer:30143:80 keycloak:30180:8080; do \
svc=$${f%%:*}; rest=$${f#*:}; lport=$${rest%%:*}; rport=$${rest#*:}; \
kubectl -n $(K8S_NS) port-forward --address 127.0.0.1 svc/$$svc $$lport:$$rport >/dev/null & \
done; wait
## k8s-down: uninstall the release (database PVCs are kept)
k8s-down:
helm uninstall big -n $(K8S_NS)
## k8s-purge: uninstall AND drop the namespace, including the database volumes
k8s-purge:
-helm uninstall big -n $(K8S_NS)
kubectl delete namespace $(K8S_NS) --ignore-not-found
## help: list available targets
help:
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## //'