# PostgreSQL on Kubernetes. local kube = import "kube.libsonnet"; { local postgres = self, local cfg = postgres.cfg, cfg:: { namespace: error "namespace must be set", appName: error "app name must be set", storageClassName: "waw-hdd-paranoid-2", prefix: "", # if set, should be 'foo-' image: "postgres:10.4", database: error "database must be set", username: error "username must be set", # not literal, instead ref for env (like { secretKeyRef: ... }) password: error "password must be set", storageSize: "30Gi", # This option can be used to customize initial database creation. For # available options see: https://www.postgresql.org/docs/9.5/app-initdb.html # Changing this option in already existing deployments will not affect # existing database. initdbArgs: null, # Extra postgres configuration options passed on startup. Accepts only # string values. # Example: { max_connections: "300" } opts: {}, # Postgres cluster upgrade automation. In order to update running # postgres version: # * set image to target postgres version # * pgupgrade.from to previous/current major version # * pgupgrade.to to target major version number (optional, this should # be figured out from image version) # * switch pgupgrade.enable to true. # # While we do have some countermeasures to prevent stupid typos, you # should still probably make a database backup, eg. using: # kubectl exec deploy/postgres -- pg_dumpall > dump.sql # # After succesful upgrade /var/lib/postgresql/data/pgdata-old directory # needs to be removed by hand. In case a rollback is needed pgdata needs # to be swapped with the pgdata-old directory and the postgres image # needs to be adjusted accordingly. pgupgrade: { enable: false, from: "10", # Extract target version from image name, supported: # postgres:1.2-suffix, postgres:1-suffix, postgres:1.2, postgres:1 to: std.native('regexSubst')("^[^:]+:([^.]+).*$", cfg.image, "${1}"), }, }, makeName(suffix):: cfg.prefix + suffix, metadata:: { namespace: cfg.namespace, labels: { "app.kubernetes.io/name": cfg.appName, "app.kubernetes.io/managed-by": "kubecfg", "app.kubernetes.io/component": "postgres", }, }, volumeClaim: kube.PersistentVolumeClaim(postgres.makeName("postgres")) { metadata+: postgres.metadata, spec+: { storageClassName: cfg.storageClassName, accessModes: [ "ReadWriteOnce" ], resources: { requests: { storage: cfg.storageSize, }, }, }, }, deployment: kube.Deployment(postgres.makeName("postgres")) { metadata+: postgres.metadata, spec+: { replicas: 1, template+: { spec+: { volumes_: { data: kube.PersistentVolumeClaimVolume(postgres.volumeClaim), }, containers_: { postgres: kube.Container(postgres.makeName("postgres")) { image: cfg.image, ports_: { client: { containerPort: 5432 }, }, env_: { POSTGRES_DB: cfg.database, POSTGRES_USER: cfg.username, POSTGRES_PASSWORD: cfg.password, PGDATA: "/var/lib/postgresql/data/pgdata", } + if cfg.initdbArgs != null then { POSTGRES_INITDB_ARGS: cfg.initdbArgs, } else {}, args: std.flatMap( function(k) ["-c", "%s=%s" % [k, cfg.opts[k]]], std.objectFields(cfg.opts), ), volumeMounts_: { data: { mountPath: "/var/lib/postgresql/data" }, }, }, }, initContainers_: if cfg.pgupgrade.enable then { pgupgrade: kube.Container(postgres.makeName("pgupgrade")) { image: "tianon/postgres-upgrade:%s-to-%s" % [cfg.pgupgrade.from, cfg.pgupgrade.to], command: [ "bash", "-c", ||| set -e -x -o pipefail CURRENT_VERSION="$(cat "$PGDATA/PG_VERSION")" if [[ "$CURRENT_VERSION" == "$VERSION_TO" ]]; then echo "Already running target version ($VERSION_TO)" exit 0 fi if [[ "$CURRENT_VERSION" != "$VERSION_FROM" ]]; then echo "Running unexpected source version, wanted $VERSION_FROM, got $CURRENT_VERSION" exit 1 fi rm -rf $PGDATANEXT || true if [ ! -s "$PGDATANEXT/PG_VERSION" ]; then echo "Initializing new database..." PGDATA="$PGDATANEXT" eval "initdb $POSTGRES_INITDB_ARGS" fi chmod 700 $PGDATA $PGDATANEXT echo "Running upgrade..." pg_upgrade --link --old-datadir $PGDATA --new-datadir $PGDATANEXT || (sleep 3600 ; exit 1) echo "Copying pg_hba.conf" cp $PGDATA/pg_hba.conf $PGDATANEXT/pg_hba.conf echo "Done, swapping..." mv $PGDATA $PGDATAOLD mv $PGDATANEXT $PGDATA ||| ], env_: postgres.deployment.spec.template.spec.containers_.postgres.env_ + { VERSION_TO: cfg.pgupgrade.to, VERSION_FROM: cfg.pgupgrade.from, # pg_upgrade target directory, swapped with # PGDATA after cluster data upgrade is finished PGDATANEXT: "/var/lib/postgresql/data/pgdata-next", # Directory used to stash previous pgdata # version PGDATAOLD: "/var/lib/postgresql/data/pgdata-old", }, volumeMounts_: { data: { mountPath: "/var/lib/postgresql/data" }, }, }, } else {}, securityContext: { runAsUser: 999, }, }, }, }, }, svc: kube.Service(postgres.makeName("postgres")) { metadata+: postgres.metadata, target_pod:: postgres.deployment.spec.template, spec+: { ports: [ { name: "client", port: 5432, targetPort: 5432, protocol: "TCP" }, ], type: "ClusterIP", }, }, bouncer: { deployment: kube.Deployment(postgres.makeName("bouncer")) { metadata+: postgres.metadata { labels+: { "app.kubernetes.io/component": "bouncer", } }, spec+: { replicas: 1, template+: { spec+: { containers_: { bouncer: kube.Container(postgres.makeName("bouncer")) { image: "edoburu/pgbouncer:1.11.0", ports_: { client: { containerPort: 5432 }, }, env: [ { name: "POSTGRES_PASSWORD", valueFrom: cfg.password }, { name: "DATABASE_URL", value: "postgres://%s:$(POSTGRES_PASSWORD)@%s/%s" % [cfg.username, postgres.svc.host, cfg.database] }, ], }, }, }, }, }, }, svc: kube.Service(postgres.makeName("bouncer")) { metadata+: postgres.metadata { labels+: { "app.kubernetes.io/component": "bouncer", } }, target_pod:: postgres.bouncer.deployment.spec.template, spec+: { ports: [ { name: "client", port: 5432, targetPort: 5432, protocol: "TCP" }, ], type: "ClusterIP", }, }, }, }