Deployment Model¶
Source of truth
hns-platform/playbooks/*.yml and roles/hns-service/tasks/{main,deploy-one,deploy-job}.yml. Update this page when those change.
A deploy is an Ansible run on app-01. There is no Helm and no per-service role — one generic role, hns-service, reads each service's .platform.yml and renders Kubernetes manifests from Jinja2 templates.
How a deploy is triggered¶
hns-platform itself is deployed via a Nebion manual recipe: Nebion clones the repo onto app-01 and runs Ansible directly on the box (no container) with ansible_connection: local. The entry point is playbooks/services.yml. Per-service redeploys and maintenance tasks are exposed as Nebion actions (clickable from the Drupal UI).
The pipeline¶
playbooks/services.yml runs two safety guards, creates the namespace, loops the service inventory through the hns-service role, then applies the shared TLS cert and prunes unused images.
playbooks/services.yml
├─ guard: K8S_NAMESPACE must be set (refuse otherwise)
├─ guard: preview namespace ⇒ non-baseline DB_NAME
├─ ensure Namespace {{ K8S_NAMESPACE }}
├─ for each service in components.yml → role hns-service:
│ 1. git clone/pull repo → srv/<name> @ version
│ 2. image_tag = <git-sha7>-<namespace>
│ 3. load that repo's .platform.yml
│ 4. for each deployment → deploy-one.yml
│ 5. for each job → deploy-job.yml
├─ role portal-tls (one shared multi-SAN cert)
└─ prune unused images (k3s crictl rmi --prune)
Per-deployment steps (deploy-one.yml)¶
- List registry tags — query the internal registry for the target image.
- Build (if needed) + push — build the image when a
build:section is declared and (the git ref changed or the tag is absent from the registry). See build-if-missing below. Tag =registry/<deployment>:<sha7>-<namespace>. - Apply manifests — ConfigMap → PVC → Deployment → Service → (headless Service if
discovery) → (basic-auth Secret + Middleware ifingress.basic_auth) → Ingress → (internal-deny Middleware + Ingress ifingress.internal_paths) → HPA (ifscaling). - Wait for rollout —
kubectl rollout statuswith a 300s timeout. On failure: auto-rollback (kubectl rollout undo) and fail the play. - Post-deploy — run each
post_deploycommand viakubectl exec deploy/<name>(see below).
Jobs (deploy-job.yml)¶
One-shot jobs[] are idempotent: a Job that already reports succeeded is skipped. Otherwise the existing Job is deleted (its spec.template is immutable, so re-apply needs a delete first) and re-created.
Two reliability behaviours worth knowing¶
Build-if-missing
The build step fires when the git ref changed or the image tag is absent from the registry — not on git changed alone. This self-heals the dangling-tag class of failure (a prior run advanced git but died before pushing, leaving a Deployment referencing a tag the registry doesn't have → ImagePullBackOff). It also surfaces a genuinely broken build at the build step with the real error, instead of as an opaque rollout timeout 300s later.
Post-deploy runs in a live pod
Post-deploy commands (migrations, cache clear) run via kubectl exec deploy/<name> -- sh -c '<cmd> 2>&1'. kubectl picks an active (preferentially Ready) pod of the deployment, so there is no manual pod-name lookup that could grab an old, still-terminating pod. The 2>&1 + explicit exit-code check surface the real stdout/stderr instead of masking a non-zero remote exit.
Image tags carry the namespace¶
The image tag is <git-sha7>-<namespace> (e.g. a1b2c3d-hns-ver2), not a bare SHA. The namespace suffix isolates images per composition: a service that bakes composition-specific values at build time (e.g. the mobile-web SPA baking VITE_* from {{ domain }}) would otherwise collide on a SHA-only tag across namespaces on the single node. Code-only images still share all layers (the registry dedupes by digest), so the suffix is near-free. See Compositions & environments.
Playbooks¶
| Playbook | Purpose |
|---|---|
site.yml |
Full provision: infra + database + services |
infra.yml |
k3s + cert-manager + ClusterIssuers |
database.yml |
Initial PostgreSQL setup on db-01 |
services.yml |
Main deploy entry point — all services into a namespace |
deploy-one.yml |
Deploy a single service (per-service Nebion action) |
init-composition-db.yml |
Provision a preview composition's database on db-01 |
teardown-composition-db.yml |
Drop a composition's database (guards the baseline names) |
seed-composition.yml |
Converge a namespace to seed users + reference content |
teardown.yml |
Delete a namespace and its workloads |
agents.yml |
Join a new k3s agent node |
registry-gc.yml |
Garbage-collect unused images from the internal registry |
Safety guards¶
Both enforced in services.yml (and mirrored in .nebion.yml and the Makefile):
K8S_NAMESPACEis mandatory. The play refuses to run if it is unset — silently defaulting tohnswould overwrite the baseline.- Preview compositions must use a non-baseline
DB_NAME. If the namespace is nothnsandDB_NAMEis empty orhns_ticketing, the play refuses — this prevents a preview stack from connecting to and mixing baseline data.
See Compositions & environments for the full workflow.
Last Updated: June 2026