By Mahmoud Consultancy (built by GloryLabs) Last verified against the repo + workflows: 2026-06-01
Companion docs: README.md · architecture.md · repo
CLAUDE.md· cluster access: repodocs/CLUSTER_ACCESS.md
This is the deploy guide. For how the system is built see architecture.md; for the full operator runbook of the shared node see docs/ops/cluster.md.
| Branch | Environment | Namespace | Ingress host |
|---|---|---|---|
| develop | Staging | valideerleeftijd-staging | valideerleeftijd.staging.glorylabs.nl |
| main | Production | valideerleeftijd | api.valideerleeftijd.nl |
Both run on the same shared k3s cluster on the TransIP VPS (136.144.174.219), isolated by namespace. Everything must work in staging first (real usage, quasi-production); prod cutover is owner-gated — the pipeline auto-deploys develop→staging, but a main merge / go-prod is an owner decision, per workspace standing policy (2026-05-29).
feature/* ──PR──► develop ──► CI (test+build+push GHCR) ──► CD ──► staging namespace
develop ──PR──► main ──► CI (test+build+push GHCR) ──► CD ──► production namespace (owner-gated)
Workflows in .github/workflows/: ci-backend.yml, ci-frontend.yml, deploy-backend.yml, deploy-frontend.yml, plus pr-validation.yml, security-gitleaks.yml, zap-scan.yml, claude.yml.
CD triggers on the workflow_run completion of the matching CI workflow for main/develop (and can be run manually via workflow_dispatch with an explicit image_tag + environment). head_branch == main → production, else staging.
deploy-backend.yml, mirrored by frontend)ENV, NS, image tag sha-<short-sha>, and per-env args:valideerleeftijd, base values.yaml, --atomic (auto-rollback on failure).valideerleeftijd-staging, -f values-staging.yaml, no --atomic (cold image pulls on the single node can exceed Helm's wait window and would wrongly roll back a healthy deploy; kubectl rollout status is the real gate).VPS_SSH_KEY.ghcr-pull-secret from GHCR_TOKEN).helm/valideerleeftijd/sealed-secrets/backend-secrets-${ENV}.yaml. Job fails if the file is missing. Never sed-rename a sealed manifest's namespace — SealedSecrets are namespace-scoped and the cluster can't decrypt a re-targeted manifest./tmp on the VPS../.github/actions/node-capacity-check renders projected requests and aborts if they'd exceed 90% of allocatable CPU/memory on the shared node (mode: enforce; flip to warn for report-only). See docs/ops/node-capacity-guardrail-proposal.md.helm dependency update
helm upgrade --install valideerleeftijd . \
<values_arg> \
--namespace <ns> --create-namespace \
--reset-then-reuse-values \
--set image.backend.repository=ghcr.io/mahmoudholding/valideerleeftijd/backend \
--set image.backend.tag=sha-<short-sha> \
--set image.backend.pullPolicy=Always \
--set imagePullSecrets[0].name=ghcr-pull-secret \
--set secrets.create=false \
<atomic_arg> --timeout 10m
kubectl rollout status deployment/valideerleeftijd-backend -n <ns> --timeout=8m
VPS_HOST, VPS_USER, VPS_SSH_KEY (ed25519), GHCR_TOKEN (PAT, read:packages), and OP_SERVICE_ACCOUNT_TOKEN (for sealing, used locally). Images: ghcr.io/mahmoudholding/valideerleeftijd/{backend,frontend}, tags sha-<short-sha> (never latest in manifests).
Chart: helm/valideerleeftijd/. Base values.yaml = production; values-staging.yaml is the staging overlay (deep-merged).
| Aspect | Production (values.yaml) | Staging (values-staging.yaml) |
|---|---|---|
| Replicas (backend/frontend) | 2 / 2 | 1 / 1 |
| Ingress host | api.valideerleeftijd.nl | valideerleeftijd.staging.glorylabs.nl |
| TLS secret | valideerleeftijd-tls | valideerleeftijd-staging-tls |
| podAntiAffinity | enabled (HA spread) | n/a (single replica) |
| Resource requests | base | trimmed to ~actual+headroom (Lever 1) |
| JVM heap cap | MaxRAMPercentage=75 | 70 (keeps actual RAM under the 90% ceiling) |
Other chart facts:
8095; frontend service ClusterIP 80 → targetPort 8080 (nginx-unprivileged).cert-manager letsencrypt-prod, ssl-redirect, proxy-body-size: 1m, routed /api → backend, / → frontend./actuator/health/liveness + /readiness; frontend GET / on 8080.redis.enabled: true, standalone, auth disabled in-cluster, persistence 1Gi on the default k3s local-path StorageClass — not standard, which doesn't exist on this cluster). Backend reaches it at valideerleeftijd-redis-master:6379.1Password vault Valideerleeftijd (mahmoudholdingbv.1password.com) holds per-env items:
| Item | Fields |
|---|---|
| valideerleeftijd-secrets-{production,staging} | api-keys, redis-password, idin-client-id, idin-client-secret, psd2-client-id, psd2-client-secret |
| rabobank-sandbox-credentials-{production,staging} | Rabobank client id + secret (one app serves both PSD2 and iDIN) |
The cluster Secret consumed by the backend is valideerleeftijd-backend-secrets, materialised by the SealedSecrets controller. To (re-)seal and commit:
OP_SERVICE_ACCOUNT_TOKEN=<token> ./scripts/seal-secrets.sh staging
OP_SERVICE_ACCOUNT_TOKEN=<token> ./scripts/seal-secrets.sh production
git add helm/valideerleeftijd/sealed-secrets/backend-secrets-{staging,production}.yaml
git commit -m "chore: rotate sealed secrets (both envs)"
git push
The CD Apply SealedSecret for this env step applies the env-specific file directly. Pre-2026-05-19 docs wrongly referenced vault mahmoud-consultancy / item valideerleeftijd-secrets (singular) — that never existed; secrets lived in Employee until the consolidation.
# Local k8s (Docker Desktop / kind) via Helm
make deploy-local
# Manual production/staging deploy (normally CD does this):
# - open the SSH tunnel and target the right context, then run the same
# `helm upgrade --install` shown in §2 on the VPS, or use
# workflow_dispatch on deploy-backend.yml with an explicit image_tag.
To inspect a live deployment from your laptop, follow repo docs/CLUSTER_ACCESS.md: the k3s API (6443) is firewalled, so you tunnel over SSH to 127.0.0.1:16443 and use the transip-k3s kubectl context.
transip-k3s-tunnel
kubectl --context=transip-k3s -n valideerleeftijd-staging get pods
kubectl --context=transip-k3s -n valideerleeftijd-staging logs deploy/valideerleeftijd-backend -f
# Health
curl https://valideerleeftijd.staging.glorylabs.nl/api/actuator/health # staging
curl https://api.valideerleeftijd.nl/api/actuator/health # production
# Initiate a PSD2 session (needs a valid X-API-Key for the env)
curl -X POST https://valideerleeftijd.staging.glorylabs.nl/api/v1/verify/initiate \
-H "X-API-Key: <vl_test_or_live_key>" -H "Content-Type: application/json" \
-d '{"minimumAge": 18, "redirectUri": "https://example.com/callback"}'
Expect HTTP 200 with a sessionId and a bankRedirect. A 401 means the API key isn't in the env's api-keys secret; a 5xx that doesn't leak a stack trace is the GlobalExceptionHandler doing its job — check pod logs.
--atomic, so a failed helm upgrade rolls back automatically.helm history valideerleeftijd -n <ns> then helm rollback valideerleeftijd <REVISION> -n <ns>.sha-<short-sha> via workflow_dispatch is the cleanest way to pin back to a previous image.
Reacties