Athena — valideerleeftijd/deployment.md

ValideerLeeftijd — Deployment

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: repo docs/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.


1. Environments

| 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).


2. Pipeline (GitHub Actions → GHCR → Helm over SSH)

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.

What the CD job does (deploy-backend.yml, mirrored by frontend)

  1. Resolve config — derive ENV, NS, image tag sha-<short-sha>, and per-env args:
    • production → namespace valideerleeftijd, base values.yaml, --atomic (auto-rollback on failure).
    • staging → namespace 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).
  2. SSH setup to the VPS using VPS_SSH_KEY.
  3. Ensure namespace + GHCR pull secret (ghcr-pull-secret from GHCR_TOKEN).
  4. Apply the env-specific SealedSecrethelm/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.
  5. Copy the Helm chart to /tmp on the VPS.
  6. Node capacity pre-check (guardrail)./.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.
  7. Helm upgrade on the VPS:
    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
    

Required GitHub secrets

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).


3. Helm chart

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:

  • Backend service ClusterIP 8095; frontend service ClusterIP 80 → targetPort 8080 (nginx-unprivileged).
  • Ingress uses cert-manager letsencrypt-prod, ssl-redirect, proxy-body-size: 1m, routed /api → backend, / → frontend.
  • Probes — backend /actuator/health/liveness + /readiness; frontend GET / on 8080.
  • Redis is the Bitnami subchart (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.
  • Security contexts — non-root, dropped capabilities, read-only root FS for both pods.

4. Secrets (1Password → Sealed Secrets → k3s)

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.


5. Manual / local deploy

# 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

6. Smoke check after deploy

# 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.


7. Rollback

  • Production uses --atomic, so a failed helm upgrade rolls back automatically.
  • Manual rollback: helm history valideerleeftijd -n <ns> then helm rollback valideerleeftijd <REVISION> -n <ns>.
  • Re-deploying a known-good sha-<short-sha> via workflow_dispatch is the cleanest way to pin back to a previous image.

Reacties

Nog geen reacties