# ============================================================================= # AI Software Factory — PostgreSQL read replica (hot standby) — TASK-0135. # # Streams WAL from the primary (postgres/statefulset.yaml) via # `pg_basebackup` (first boot, initContainer) + continuous logical→physical # replication (standby.signal). The API's DATABASE_REPLICA_URL points at the # pooler's `factory;reader` entry (pgbouncer.yaml) → this Service. # # Staleness contract (perf-report §6): the replica lags. `db.read()` serves # tree/listing/search from it; permission + If-Match version checks + writes # stay on the primary. A read-only hot standby rejects writes at the DB. # # The replica is a SEPARATE StatefulSet (its own PVC). It is NOT part of the # PITR set — a replica loss is re-created by re-running the base backup, and # PITR restores target a fresh instance (runbook §8.1), never the replica. # ============================================================================= apiVersion: v1 kind: ConfigMap metadata: name: factory-pg-replica-config labels: app.kubernetes.io/name: postgres app.kubernetes.io/component: replica app.kubernetes.io/part-of: ai-software-factory data: # Empty file — its presence makes postgres start as a standby. Mounted at # $PGDATA/standby.signal after the base backup. standby.signal: "" # Streaming-replication connection to the primary. The standby runs as the # `factory` role (a SUPERUSER-ish app role) so it can stream WAL from the # primary's `factory` role. `primary_slot_name` is empty — with a single # replica a physical replication slot is optional; set it if you add more # standbys (wal-g PITR also uses slots, but those are restore-time only). standby.conf: | primary_conninfo = 'host=factory-db.factory.svc port=5432 user=factory password=%(REPLICA_PASSWORD)s application_name=factory-replica' primary_slot_name = '' hot_standby = on hot_standby_feedback = on --- apiVersion: apps/v1 kind: StatefulSet metadata: name: postgres-replica labels: app.kubernetes.io/name: postgres app.kubernetes.io/component: replica app.kubernetes.io/part-of: ai-software-factory spec: serviceName: factory-db-replica replicas: 1 selector: matchLabels: app.kubernetes.io/name: postgres app.kubernetes.io/component: replica template: metadata: labels: app.kubernetes.io/name: postgres app.kubernetes.io/component: replica spec: # initContainer: first boot only. Base-backup the primary's data into # the PVC, then drop standby.signal + standby.conf in place. If the # standby.signal is already present (a restore), skip the base backup # (the volume already holds a valid standby data dir). initContainers: - name: pg-basebackup image: postgres:16-alpine command: - sh - -c - | set -e DATA="${PGDATA:-/var/lib/postgresql/data}" if [ -f "$DATA/standby.signal" ]; then echo "[replica-init] standby.signal present — reusing existing data dir" exit 0 fi echo "[replica-init] base backup from primary (first boot)" mkdir -p "$DATA" su postgres -c "PGPASSWORD=\"\$REPLICA_PASSWORD\" pg_basebackup -h postgres.factory.svc -p 5432 -U factory -D $DATA -X stream -c fast" # standby.signal + standby.conf (from the mounted ConfigMap) are # placed by the entrypoint below (it owns $PGDATA). env: - name: PGDATA value: /var/lib/postgresql/data - name: REPLICA_PASSWORD valueFrom: { secretKeyRef: { name: factory-db, key: POSTGRES_PASSWORD } } volumeMounts: - name: pgdata mountPath: /var/lib/postgresql/data containers: - name: postgres image: postgres:16-alpine env: - name: PGDATA value: /var/lib/postgresql/data - name: POSTGRES_USER value: factory - name: POSTGRES_DB value: factory - name: REPLICA_PASSWORD valueFrom: { secretKeyRef: { name: factory-db, key: POSTGRES_PASSWORD } } # The official entrypoint sees a NON-EMPTY $PGDATA with # standby.signal → it starts the standby (no re-init). The # standby.conf is placed by the entrypoint's first-run branch # below; to be safe we also copy it in the initContainer. command: - sh - -c - | set -e DATA="${PGDATA:-/var/lib/postgresql/data}" # Place standby.signal + standby.conf if not already present. [ -f "$DATA/standby.signal" ] || cp /etc/pg-replica/standby.signal "$DATA/standby.signal" [ -f "$DATA/standby.conf" ] || sed "s/%(REPLICA_PASSWORD)s/$REPLICA_PASSWORD/" /etc/pg-replica/standby.conf > "$DATA/standby.conf" # Run postgres as the postgres user (the official image's user). su postgres -c "pg_ctl -D $DATA -w -t 60 start" exec tail -f /dev/null ports: - name: postgres containerPort: 5432 envFrom: - secretRef: { name: factory-db } readinessProbe: exec: command: - sh - -c - | # A standby is ready when it can ACCEPT a connection and is # in recovery (pg_is_in_recovery = 't'). [ "$$(su postgres -c "psql -tAc \"select pg_is_in_recovery()\" 2>/dev/null || true)") = "t" ] initialDelaySeconds: 5 periodSeconds: 5 resources: requests: { cpu: 250m, memory: 512Mi } limits: { cpu: "1", memory: 2Gi } volumeMounts: - name: pgdata mountPath: /var/lib/postgresql/data - name: replica-conf mountPath: /etc/pg-replica readOnly: true volumes: - name: replica-conf configMap: { name: factory-pg-replica-config } volumeClaimTemplates: - metadata: name: pgdata spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 20Gi --- apiVersion: v1 kind: Service metadata: name: factory-db-replica labels: app.kubernetes.io/name: postgres app.kubernetes.io/component: replica app.kubernetes.io/part-of: ai-software-factory spec: selector: app.kubernetes.io/name: postgres app.kubernetes.io/component: replica ports: - name: postgres port: 5432 targetPort: 5432