# Managed by Ansible (roles/k3s_postgres) — do not edit on the node. # # Plain manifests, not a HelmChart — same pattern as # roles/k3s_metallb/templates/metallb-config.yaml.j2: the Cluster CR below # references a CRD (postgresql.cnpg.io) that only exists once the # cloudnative-pg HelmChart (cnpg-operator.yaml, same manifests directory) has # actually installed the operator, so it's applied here rather than gated # behind a "wait for CRDs" step — k3s's deploy controller retries a manifest # referencing not-yet-existing CRDs until they show up, instead of failing # once and giving up. templates/postgres-superuser-secret.yaml.j2 relies on # the Namespace created here the same way, for the same reason. apiVersion: v1 kind: Namespace metadata: name: {{ k3s_postgres_namespace }} --- apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: name: shared-postgres namespace: {{ k3s_postgres_namespace }} spec: # 1 primary + 1 replica (src/shared/postgres/ansible/kubernetes/vars.yml). # CNPG's own default pod anti-affinity (preferred, topology key hostname) # spreads them across distinct nodes; the nodeAffinity below narrows which # nodes those can be. instances: {{ pg_config.POSTGRES_INSTANCES }} imageName: {{ k3s_postgres_image }} # Password-based superuser login, from the Secret Ansible renders # alongside this file. Off by default in CNPG — without this, the operator # ignores the secret's content and sets the postgres user's password to # NULL, disabling remote login as it. # # The name must stay clear of `-superuser` (i.e. # `shared-postgres-superuser`), which is what CNPG calls the secret it # generates for itself when this stanza is absent. Claiming that name for # our own object stops password changes reaching the database — see # postgres-superuser-secret.yaml.j2 for the full symptom. enableSuperuserAccess: true superuserSecret: name: shared-postgres-superuser-vault # A LoadBalancer Service for the primary, on top of the ClusterIP -rw/-ro/-r # Services CNPG creates for every Cluster. Declared here as a CNPG *managed # service* rather than as a Service manifest of our own: selectorType: rw # means the operator maintains the selector, so the LB follows a failover to # the other instance the same way the built-in -rw Service does. A # hand-written Service would need its selector re-pointed by hand after # every promotion. # # This is what makes the cluster reachable from the Ansible controller at a # fixed host:port, which is the thing per-app database provisioning needs — # the community.postgresql tasks compose_stack/lxc_app use can't talk to a # ClusterIP, and `kubectl port-forward` isn't a stable address. It does put # the superuser on the LAN: acceptable here for the same reason the Unraid # instance's published port is, and the LAN is the boundary either way. managed: services: additional: - selectorType: rw serviceTemplate: metadata: name: shared-postgres-lb {% if k3s_postgres_loadbalancer_ip %} # Pinned rather than left to MetalLB's next-free pick, because # this address ends up in config elsewhere (inventory, an app's # vars.yml) and shouldn't move when the Service is recreated. annotations: metallb.universe.tf/loadBalancerIPs: "{{ k3s_postgres_loadbalancer_ip }}" {% endif %} spec: type: LoadBalancer # No ports: — the operator fills in Postgres's own (5432) from # the same template it builds the -rw Service from; naming them # here would only risk disagreeing with it. # POSTGRES_PORT (common/vars.yml) is 5432, the same port CNPG always # listens on inside the pod/Service — nothing to override here. storage: size: {{ pg_config.POSTGRES_STORAGE_SIZE }} storageClass: {{ pg_config.POSTGRES_STORAGE_CLASS }} resources: {{ k3s_postgres_resources | to_nice_yaml(indent=2) | indent(4, first=true) }} # Workers only — k3s-ctrl-01 stays free of app pods, same boundary the # cluster already keeps for monitoring/MetalLB. affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: node-role.kubernetes.io/control-plane operator: DoesNotExist # No bootstrap: stanza — CNPG's default initdb bootstrap creates a # `postgres` superuser (above) plus a default `app` database owned by an # auto-generated `app` role/secret. That default app database is unused # today: no app is deployed onto k3s yet, and real per-app database # provisioning here is a deferred design problem (see CLAUDE.md → "Key # decisions") — the community.postgresql approach compose_stack/lxc_app # use needs a controller-reachable host:port, which this cluster's # in-cluster -rw Service isn't without further work.