Frontend Labs
⋯

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), and orbit-db-r (any instance).
  • Secret orbit-db-app with username, password, uri, host, port and 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):

  1. kubectl delete pod -l app.kubernetes.io/name=orders. Watch replacements appear. Did any request fail during it? (Probably yes; Part III fixes that.)
  2. Delete the CNPG primary pod. Watch failover with kubectl get pods -L role -w. Do orders pods go not-ready, then ready again? Do they restart? (They shouldn't: liveness doesn't check the DB.)
  3. Scale worker to 0, create 50 orders, scale back to 3. The backlog drains. This is the behavior KEDA automates in §27.
  4. kubectl drain <node> --ignore-daemonsets --delete-emptydir-data, then kubectl 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.


Generated from real-life-k8s-guide.md · 8 Oct 2026 16:36