Skip to content

Cluster Topology

Source of truth

hns-platform/inventory/hosts.yml, inventory/host_vars/*.yml, inventory/group_vars/all/main.yml, cloud-init.yml, cloud-init-db.yml, and roles/registry/, roles/k3s-server/. Update this page when those change.

The HNS Ticketing cluster runs on two Hetzner Cloud servers joined by a private network (10.0.0.0/24). Application workloads run in a single-node k3s cluster; PostgreSQL runs on bare metal outside Kubernetes.

              Public Internet
                    │  :80 / :443
                    ▼
┌─────────────────────────────────────┐        ┌──────────────────────────┐
│  app-01   private 10.0.0.2           │        │  db-01   private 10.0.0.3 │
│           public  178.105.212.28     │ 10.0.0.x│                          │
│                                      │────────▶│  PostgreSQL 16           │
│  k3s (server mode)  v1.35.5+k3s1     │ :5432   │  (bare metal)            │
│   ├─ Traefik (built-in ingress)      │        │   ├─ hns_ticketing       │
│   ├─ HNS service pods (see inventory)│        │   └─ keycloak            │
│   └─ hns-registry  10.0.0.2:5000     │        │                          │
└─────────────────────────────────────┘        └──────────────────────────┘
        NOT in k8s — managed by Ansible/cloud-init ▲

Hosts

Host Private IP Public IP Role
app-01 10.0.0.2 178.105.212.28 k3s server (control plane + workloads), internal registry
db-01 10.0.0.3 PostgreSQL 16, bare metal

k3s_agents is defined but empty — the inventory is ready to scale out to worker nodes (playbooks/agents.yml), but the cluster currently runs everything on app-01.

k3s

  • Distribution: k3s (single-binary certified Kubernetes), server mode on app-01.
  • Version: pinned cluster-wide as k3s_version: v1.35.5+k3s1 in main.yml. Bump it deliberately, then run the upgrade playbook node-by-node (cordon/drain/reinstall) to avoid kubelet ↔ kube-apiserver skew.
  • Ingress: Traefik, bundled with k3s. See TLS & ingress.
  • Service discovery: in-cluster traffic uses Kubernetes DNS — services reach each other by name over ClusterIP (e.g. http://hns-ticketing-backend:80, nats://nats:4222). See Service inventory. This traffic never traverses Traefik, so it is unaffected by ingress auth or TLS.

Internal Docker registry

All HNS service images are built on app-01 and pushed to a private registry; every k3s node pulls from it.

  • Container: hns-registry (registry:3), bound to 10.0.0.2:5000 (the app-01 private IP only — never exposed publicly).
  • Storage: /srv/registry on app-01; deletes enabled (REGISTRY_STORAGE_DELETE_ENABLED=true) so the registry-gc maintenance action can reclaim space.
  • Plain HTTP, trusted explicitly: the Docker daemon lists it under insecure-registries, and k3s containerd mirrors it via /etc/rancher/k3s/registries.yaml with insecure_skip_verify. This is safe because the endpoint is private-network-only.

Database server (db-01)

PostgreSQL 16 runs on bare metal, provisioned by cloud-init-db.yml:

  • Listens on the private IP 10.0.0.3:5432; pg_hba.conf allows the 10.0.0.0/24 network with md5 auth.
  • Two baseline databases (inventory/host_vars/db-01.yml): hns_ticketing (owner vault_db_user) and keycloak (owner vault_kc_db_user).
  • Preview compositions get their own backend database on this same server — see Compositions & environments.

Keycloak's database is split between two places

The realm/identity database keycloak lives on db-01 (above). But each composition also runs its own in-cluster Postgres for Keycloak (hns-idp-db) where applicable — KC data is isolated per namespace automatically, so it needs no per-composition db-01 knob. The bare-metal keycloak DB is the baseline instance.

Bootstrap

Servers are provisioned from cloud-init, then converged by Ansible (playbooks/infra.yml / site.yml):

  • cloud-init.yml (app-01): installs docker.io, git, Ansible + the kubernetes.core/community.docker collections, then k3s ... server and wires kubectl + kubeconfig.
  • cloud-init-db.yml (db-01): adds the PGDG apt repo, installs postgresql-16, switches listen_addresses to the private IP, and opens pg_hba to the private network.

Last Updated: June 2026