20. Part II capstone: Orbit from raw manifests
20.1 Postgres with CloudNativePG
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm upgrade --install cnpg cnpg/cloudnative-pg -n cnpg-system --create-namespace --wait
kubectl create namespace orbit
# deploy/base/postgres/cluster.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata: { name: orbit-db, namespace: orbit }
spec:
instances: 3 # 1 primary + 2 streaming replicas, automatic failover
imageName: ghcr.io/cloudnative-pg/postgresql:17
storage: { size: 2Gi }
bootstrap:
initdb: { database: orbit, owner: orbit }
postgresql:
parameters:
max_connections: "200"
affinity:
topologyKey: topology.kubernetes.io/zone # spread instances across zones
resources:
requests: { cpu: 250m, memory: 512Mi }
limits: { memory: 512Mi }
monitoring:
enablePodMonitor: true # Prometheus scrapes it once kube-prometheus-stack exists (§30)
CloudNativePG creates:
- Services
orbit-db-rw(primary),orbit-db-ro(replicas), andorbit-db-r(any instance). - Secret
orbit-db-appwithusername,password,uri,host,portand more. - Automated failover: delete the primary pod and watch a replica get promoted in seconds.
kubectl apply -f deploy/base/postgres/cluster.yaml
kubectl get cluster -n orbit -w # wait for "Cluster in healthy state"
kubectl get pods -n orbit -L role # which one is primary
20.2 Migrations as a Job
apiVersion: batch/v1
kind: Job
metadata: { name: migrate-0001, namespace: orbit }
spec:
backoffLimit: 5
ttlSecondsAfterFinished: 3600
template:
spec:
restartPolicy: OnFailure
containers:
- name: goose
image: ghcr.io/acme/orbit-migrations:sha-abc123 # FROM goose + COPY migrations/
args: ["-dir", "/migrations", "postgres", "$(DATABASE_URL)", "up"]
env:
- name: DATABASE_URL
valueFrom: { secretKeyRef: { name: orbit-db-app, key: uri } }
§38 covers how migrations are ordered relative to deploys in GitOps (sync waves / pre-sync hooks) and the expand/contract pattern.
20.3 The application manifests
For each service: a Deployment and a Service. Here is orders. This is
the minimal version; Part III turns it into the production "golden
manifest".
apiVersion: apps/v1
kind: Deployment
metadata: { name: orders, namespace: orbit, labels: { app.kubernetes.io/name: orders } }
spec:
replicas: 2
selector: { matchLabels: { app.kubernetes.io/name: orders } }
template:
metadata: { labels: { app.kubernetes.io/name: orders, app.kubernetes.io/part-of: orbit } }
spec:
containers:
- name: orders
image: orbit-orders:dev
imagePullPolicy: IfNotPresent # kind-loaded image
ports: [{ name: http, containerPort: 8080 }]
env:
- name: DATABASE_URL
valueFrom: { secretKeyRef: { name: orbit-db-app, key: uri } }
- { name: REDIS_ADDR, value: "redis:6379" }
readinessProbe: { httpGet: { path: /readyz, port: http } }
livenessProbe: { httpGet: { path: /livez, port: http } }
---
apiVersion: v1
kind: Service
metadata: { name: orders, namespace: orbit }
spec:
selector: { app.kubernetes.io/name: orders }
ports: [{ name: http, port: 80, targetPort: http }]
kubectl apply -n orbit -f deploy/raw/
kubectl get pods -n orbit
kubectl port-forward -n orbit svc/orders 8082:80 &
curl -s -XPOST localhost:8082/orders -H 'Idempotency-Key: k8s-1' -H 'Content-Type: application/json' \
-d '{"product_id":1,"quantity":1}'
Exercises (do them; they build intuition for Part V):
kubectl delete pod -l app.kubernetes.io/name=orders. Watch replacements appear. Did any request fail during it? (Probably yes; Part III fixes that.)- Delete the CNPG primary pod. Watch failover with
kubectl get pods -L role -w. Doorderspods go not-ready, then ready again? Do they restart? (They shouldn't: liveness doesn't check the DB.) - Scale
workerto 0, create 50 orders, scale back to 3. The backlog drains. This is the behavior KEDA automates in §27. kubectl drain <node> --ignore-daemonsets --delete-emptydir-data, thenkubectl uncordon. What happened to pods on it?
Part II checkpoint: you can read any manifest, explain which controller acts on it, and trace a request from Service → EndpointSlice → pod.