Single PostgreSQL deployment in K8S

PostgreSQL by default doesnot build for kubernetes, and a database with statefulset workload in k8s can be brutal to manage. In my lab k8s cluster, here we create namespace, secret, configuremap, PVC and statefulset to run a single PostgreSQL

# create namespace "postgresql"
[root@freeipa-server ~]# kubectl create ns postgresql
namespace/postgresql created

# create secret to store database creds
[root@freeipa-server ~]# kubectl -n postgresql create secret generic postgresql --from-literal POSTGRES_USER="postgresadmin" --from-literal POSTGRES_PASSWORD='admin123' --from-literal POSTGRES_DB="postgresdb" --from-literal REPLICATION_USER="replicationuser" --from-literal REPLICATION_PASSWORD='replicationPassword'
secret/postgresql created

# create configmap, pvc, statefulset with init container to run postgresql
[root@freeipa-server ~]# vim stateful.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres
data: 
  pg_hba.conf: |+
    # TYPE  DATABASE        USER            ADDRESS                 METHOD
    host     replication     replicationuser         0.0.0.0/0        md5
    # "local" is for Unix domain socket connections only
    local   all             all                                     trust
    # IPv4 local connections:
    host    all             all             127.0.0.1/32            trust
    # IPv6 local connections:
    host    all             all             ::1/128                 trust
    # Allow replication connections from localhost, by a user with the
    # replication privilege.
    local   replication     all                                     trust
    host    replication     all             127.0.0.1/32            trust
    host    replication     all             ::1/128                 trust

    host all all all scram-sha-256
  postgresql.conf: |+
    data_directory = '/data/pgdata'
    hba_file = '/config/pg_hba.conf'
    ident_file = '/config/pg_ident.conf'

    port = 5432
    listen_addresses = '*'
    max_connections = 100
    shared_buffers = 128MB
    dynamic_shared_memory_type = posix
    max_wal_size = 1GB
    min_wal_size = 80MB
    log_timezone = 'Etc/UTC'
    datestyle = 'iso, mdy'
    timezone = 'Etc/UTC'

    #locale settings
    lc_messages = 'en_US.utf8'			# locale for system error message
    lc_monetary = 'en_US.utf8'			# locale for monetary formatting
    lc_numeric = 'en_US.utf8'			# locale for number formatting
    lc_time = 'en_US.utf8'				# locale for time formatting

    default_text_search_config = 'pg_catalog.english'

    #replication
    wal_level = replica
    archive_mode = on
    archive_command = 'test ! -f /data/archive/%f && cp %p /data/archive/%f'
    max_wal_senders = 3
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  selector:
    matchLabels:
      app: postgres
  serviceName: "postgres"
  replicas: 1
  template:
    metadata:
      labels:
        app: postgres
    spec:
      terminationGracePeriodSeconds: 30
      initContainers:
      - name: init
        image: postgres:15.0
        command: [ "bash", "-c" ]
        args:
        - |
          #create archive directory
          mkdir -p /data/archive && chown -R 999:999 /data/archive
        volumeMounts:
        - name: data
          mountPath: /data
          readOnly: false
      containers:
      - name: postgres
        image: postgres:15.0
        args: ["-c", "config_file=/config/postgresql.conf"]
        ports:
        - containerPort: 5432
          name: database
        env:
        - name: PGDATA
          value: "/data/pgdata"
        - name: POSTGRES_USER
          valueFrom:
            secretKeyRef:
              name: postgresql
              key: POSTGRES_USER
              optional: false
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgresql
              key: POSTGRES_PASSWORD
              optional: false
        - name: POSTGRES_DB
          valueFrom:
            secretKeyRef:
              name: postgresql
              key: POSTGRES_DB
              optional: false
        volumeMounts:
        - name: config
          mountPath: /config
          readOnly: false
        - name: data
          mountPath: /data
          readOnly: false
      volumes:
      - name: config
        configMap:
          name: postgres
          defaultMode: 0755
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "standard"
      resources:
        requests:
          storage: 100Mi
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
  labels:
    app: postgres
spec:
  ports:
  - port: 5432
    targetPort: 5432
    name: postgres
  clusterIP: None
  selector:
    app: postgres

[root@freeipa-server ~]# kubectl create -f stateful.yaml -n postgresql 
configmap/postgres created
statefulset.apps/postgres created
service/postgres created

# validate for pvc, pods
[root@freeipa-server ~]# kubectl get pvc -n postgresql 
NAME              STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
data-postgres-0   Bound    pvc-dd89fc0a-915f-40eb-b61f-917234074a61   100Mi      RWO            longhorn       19m

[root@freeipa-server ~]# kubectl get all -n postgresql 
NAME             READY   STATUS    RESTARTS   AGE
pod/postgres-0   1/1     Running   0          6m29s

NAME               TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
service/postgres   ClusterIP   None         <none>        5432/TCP   6m29s

NAME                        READY   AGE
statefulset.apps/postgres   1/1     6m29s

# check container logs for database connection status
[root@freeipa-server ~]# kubectl logs -n postgresql postgres-0 
Defaulted container "postgres" out of: postgres, init (init)
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with locale "en_US.utf8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".

Data page checksums are disabled.

fixing permissions on existing directory /data/pgdata ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default time zone ... Etc/UTC
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
initdb: warning: enabling "trust" authentication for local connections
initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb.
syncing data to disk ... ok


Success. You can now start the database server using:

    pg_ctl -D /data/pgdata -l logfile start

waiting for server to start....2024-05-12 00:52:56.718 UTC [49] LOG:  starting PostgreSQL 15.0 (Debian 15.0-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit
2024-05-12 00:52:56.719 UTC [49] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2024-05-12 00:52:56.730 UTC [49] LOG:  could not open usermap file "/config/pg_ident.conf": No such file or directory
2024-05-12 00:52:56.733 UTC [52] LOG:  database system was shut down at 2024-05-12 00:52:55 UTC
2024-05-12 00:52:56.744 UTC [49] LOG:  database system is ready to accept connections
 done
server started
CREATE DATABASE


/usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/*

2024-05-12 00:52:56.957 UTC [49] LOG:  received fast shutdown request
waiting for server to shut down....2024-05-12 00:52:56.961 UTC [49] LOG:  aborting any active transactions
2024-05-12 00:52:56.962 UTC [49] LOG:  background worker "logical replication launcher" (PID 56) exited with exit code 1
2024-05-12 00:52:56.963 UTC [50] LOG:  shutting down
2024-05-12 00:52:57.042 UTC [50] LOG:  checkpoint starting: shutdown immediate
..2024-05-12 00:52:59.314 UTC [50] LOG:  checkpoint complete: wrote 918 buffers (5.6%); 0 WAL file(s) added, 0 removed, 1 recycled; write=0.434 s, sync=0.014 s, total=2.279 s; sync files=250, longest=0.007 s, average=0.001 s; distance=11271 kB, estimate=11271 kB
2024-05-12 00:52:59.318 UTC [49] LOG:  database system is shut down
 done
server stopped

PostgreSQL init process complete; ready for start up.

2024-05-12 00:52:59.385 UTC [1] LOG:  starting PostgreSQL 15.0 (Debian 15.0-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit
2024-05-12 00:52:59.385 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2024-05-12 00:52:59.385 UTC [1] LOG:  listening on IPv6 address "::", port 5432
2024-05-12 00:52:59.389 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2024-05-12 00:52:59.398 UTC [1] LOG:  could not open usermap file "/config/pg_ident.conf": No such file or directory
2024-05-12 00:52:59.404 UTC [67] LOG:  database system was shut down at 2024-05-12 00:52:59 UTC
2024-05-12 00:52:59.415 UTC [1] LOG:  database system is ready to accept connections

Conclusion

Now we are able to deploy a PostgreSQL in local k8s cluster, with defined environment variables in kubernetes secret and configmap, together with init container to create data archive volume in presistent storage class, the next blog I will discover how to run PostgreSQL HA with presistent volume on kubernetes with both Helm and operater, then validate scale up and down, backup using cronjob and etc.