Cloud tutorial with source code showing how to create a Kubernetes persistent volume for long term storage.

Using a Persistent NFS Volume for a Postgres Kubernetes Deployment (Using Vagrant, Ansible and VirtualBox)

In this tutorial, I present a way to launch a Postgres database instance in our Kubernetes Vagrant cluster deployed using Vagrant, Ansible, and VirtualBox. The Postgres instance will have all its data stored in a remote NFS Server to preserve database data across Kubernetes cluster destroy or Postgres pod unintended disruptions.

External NSF Server providing storage for a PostgreSQL server inside a Kubernetes Cluster

Persistent Volumes in Kubernetes

Kubernetes containers are mostly used for stateless applications, where each instance is disposable, does not store data that needs to be persisted across restarts inside the container or needed for client sessions as its storage is ephemeral.

On the contrary, the stateful applications need to store data and have it available between restarts and sessions. Databases like Postgres or MySQL are typical stateful applications.

Kubernetes provides support for many types of volumes depending on the Cloud provider. For our local Kubernetes Cluster, the most appropriate and easy to configures is an NFS volume.

NFS Server Installation

If you don’t have an existing NFS Server, it is easy to create a local NFS server for our Kubernetes Cluster.

Install the Ubuntu needed packages and create a local directory /mnt/vagrant-kubernetes:

sudo apt install nfs-kernel-server
sudo mkdir -p /mnt/vagrant-kubernetes
sudo mkdir -p /mnt/vagrant-kubernetes/data
sudo chown nobody:nogroup /mnt/vagrant-kubernetes
sudo chmod 777 /mnt/vagrant-kubernetes

Edit the /etc/exports file to add the exported local directory and limit the share to the CIDR 192.168.50.0/24 of our Kubernetes Vagrant Cluster VirtualBox machines.

/mnt/vagrant-kubernetes 192.168.50.0/24(rw,sync,no_subtree_check,insecure,no_root_squash)

Start the NFS server

# sudo exportfs -a
# sudo systemctl restart nfs-kernel-server    
# sudo exportfs -v
/mnt/vagrant-kubernetes
	192.168.50.0/24(rw,wdelay,insecure,no_root_squash,no_subtree_check,sec=sys,rw,insecure,no_root_squash,no_all_squash)

Kubernetes Deployment File

The Postgres database deployment is composed of the following Kubernetes Objects:

  • ConfigMap: stores common configuration data for the Postgres database server
  • PersistentVolume: creates an NFS client that connects to the server and makes the NFS share available for volume claims.
  • PersistentVolumeClaim: defines the characteristics of the needed volume, that Kubernetes will try to solve using an available PersistentVolume with the requested configuration.
  • Deployment: starts an instance of a Postgres docker container using the supplied ConfigMap to redefine the value of Postgres configuration environment variables and mounts the PersistentVolumeClaim inside the container to replace PostgreSQL Data directory.
  • Service (NodePort):¬† publishes the Postgres server port outside the Kubernetes Cluster.

Postgres Kubernetes ConfigMap

The config map is a key-value store that is available to all Kubernetes nodes. The data will be used to set some environment variables of the Postgres container:

  • POSTGRES_DB: db (the database to be created at startup if it doesn’t exists)
  • POSTGRES_USER: user (the admin user that will be created)
  • POSTGRES_PASSWORD: pass (the password for the admin user that will be created)
  • PGDATA: /var/lib/postgresql/data/k8s (the location for the data to be used upon initialization, we will be using the subdirectory k8s for this instance)
apiVersion: v1
kind: ConfigMap
metadata:
  name: psql-itwl-dev-01-cm
data:
  POSTGRES_DB: db
  POSTGRES_USER: user
  POSTGRES_PASSWORD: pass
  PGDATA: /var/lib/postgresql/data/k8s

Postgres Kubernetes PersistentVolume

Defines an NFS PersistentVolume located at:

  • Server: 192.168.50.1 (The IP assigned by VirtualBox to our host)
  • Path: /mnt/vagrant-kubernetes/data (a subdirectory “data” inside the shared folder)

The volume has labels:

  • app: psql
  • ver: itwl-dev-01-pv

It is able to store up to 1 Gigabyte.

Supports many clients reading and writing at the same time.

After usage, the volume is not destroyed (Retain policy).

apiVersion: v1
kind: PersistentVolume
metadata:
  name: psql-itwl-dev-01-pv
  labels: #Labels 
    app: psql
    ver: itwl-dev-01-pv 
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  nfs:
    server: 192.168.50.1
    path: "/mnt/vagrant-kubernetes/data"

 

Postgres Kubernetes PersistentVolumeClaim

The PersistentVolumeClaim is the object that is assigned to the deployment. The PersistentVolumeClaim defines how the volume needs to be, and Kubernetes tries to find a corresponding PersistentVolume that satisfies all the requirements.

The PersistentVolumeClaim asks for a volume with the following labels:

  • app: psql
  • ver: itwl-dev-01-pv

An access mode of ReadWriteMany and at least 1 Gigabyte of storage.

 

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: psql-itwl-dev-01-pvc
spec:
  selector:
    matchLabels:  #Select a volume with this labels
      app: psql
      ver: itwl-dev-01-pv
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi

Postgres Kubernetes Deployment

A regular application deployment descriptor with the following characteristics:

A single replica will be deployed. Istio is not needed, annotation sidecar.istio.io/inject false prevents the macro to inject the proxy.

The container uses Postgres latest image from the public Docker registry (https://hub.docker.com/) and sets:

  • Export container port 5432
  • Set environment variables for the container using the Kubernetes config map psql-itwl-dev-01-cm
  • Replace the directory /var/lib/postgresql/data inside the Postgres container with a volume named pgdatavol
  • Define the pgdatavol volume as an instance of the persistentVolumeClaim psql-itwl-dev-01-pvc defined before
apiVersion: apps/v1
kind: Deployment
metadata:
  name: psql-itwl-dev-01
  labels: 
    app: psql 
    ver: itwl-dev-01
spec:
  replicas: 1
  selector:
    matchLabels:  #Deploy in a POD that has labels app: color and color: blue
      app: psql
      ver: itwl-dev-01
  template: #For the creation of the pod      
    metadata:
      labels:
        app: psql
        ver: itwl-dev-01
      annotations:
        sidecar.istio.io/inject: "false"        
    spec:
      containers:
        - name: postgres
          image: postgres:latest
          imagePullPolicy: "IfNotPresent"
          ports:
            - containerPort: 5432 
          envFrom:
            - configMapRef:
                name: psql-itwl-dev-01-cm          
          volumeMounts:
            - mountPath: /var/lib/postgresql/data
              name: pgdatavol
      volumes:
        - name: pgdatavol
          persistentVolumeClaim:
            claimName: psql-itwl-dev-01-pvc

Postgres Kubernetes NodePort Service

Since our local Kubernetes Cluster doesn’t have a Cloud provided Load Balancer, we are using the NodePort functionality to access published ports in containers.

The NodePort will publish Postgres in port 30100 of every Kubernetes Master and Nodes:

  • 192.168.50.11:30100
  • 192.168.50.12:30100
  • 192.168.50.13:30100
apiVersion: v1
kind: Service
metadata:
  name: postgres-service-np
spec:
  type: NodePort
  selector:
    app: psql
  ports:
    - name: psql
      port: 5432        # Cluster IP http://10.109.199.234:port (docker exposed port)
      nodePort: 30100   # (EXTERNAL-IP VirtualBox IPs) 192.168.50.11:nodePort 192.168.50.12:nodePort 192.168.50.13:nodePort
      protocol: TCP      

Deploy Postgres in Kubernetes

Create the Kubernetes resources

$ kubectl apply -f postgresql.yaml 
configmap/psql-itwl-dev-01-cm created
persistentvolume/psql-itwl-dev-01-pv created
persistentvolumeclaim/psql-itwl-dev-01-pvc created
deployment.apps/psql-itwl-dev-01 created
service/postgres-service-np created

Check Kubernetes

Check that all resources have been created:

$ kubectl get all
NAME                                    READY   STATUS    RESTARTS   AGE
pod/psql-itwl-dev-01-594c7468c7-p9k9l   1/1     Running   0          6s

NAME                          TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
service/kubernetes            ClusterIP   10.96.0.1       <none>        443/TCP          4d1h
service/postgres-service-np   NodePort    10.105.135.29   <none>        5432:30100/TCP   6s

NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/psql-itwl-dev-01   1/1     1            1           6s

NAME                                          DESIRED   CURRENT   READY   AGE
replicaset.apps/psql-itwl-dev-01-594c7468c7   1         1         1       6s

See the Kubernetes Postgres pod log file

$ kubectl logs 
$ kubectl logs pod/psql-itwl-dev-01-594c7468c7-p9k9l -f
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 /var/lib/postgresql/data/k8s ... ok
creating subdirectories ... ok
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting dynamic shared memory implementation ... posix
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok

WARNING: enabling "trust" authentication for local connections
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 /var/lib/postgresql/data/k8s -l logfile start

waiting for server to start....2019-06-10 17:59:41.009 UTC [42] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2019-06-10 17:59:41.104 UTC [43] LOG:  database system was shut down at 2019-06-10 17:59:40 UTC
2019-06-10 17:59:41.141 UTC [42] LOG:  database system is ready to accept connections
 done
server started
CREATE DATABASE


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

2019-06-10 17:59:48.590 UTC [42] LOG:  received fast shutdown request
waiting for server to shut down...2019-06-10 17:59:48.597 UTC [42] LOG:  aborting any active transactions
.2019-06-10 17:59:48.603 UTC [42] LOG:  background worker "logical replication launcher" (PID 49) exited with exit code 1
2019-06-10 17:59:48.603 UTC [44] LOG:  shutting down
2019-06-10 17:59:48.715 UTC [42] LOG:  database system is shut down
 done
server stopped

PostgreSQL init process complete; ready for start up.

2019-06-10 17:59:48.854 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2019-06-10 17:59:48.854 UTC [1] LOG:  listening on IPv6 address "::", port 5432
2019-06-10 17:59:48.865 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2019-06-10 17:59:48.954 UTC [60] LOG:  database system was shut down at 2019-06-10 17:59:48 UTC
2019-06-10 17:59:48.997 UTC [1] LOG:  database system is ready to accept connections

Test the Kubernetes Postgres database

Use the Postgres client psql (apt-get install -y postgresql-client)

Password is pass

# psql db -h 192.168.50.11 -p 30100 -U user
Password for user user: 
psql (10.8 (Ubuntu 10.8-0ubuntu0.18.10.1), server 11.3 (Debian 11.3-1.pgdg90+1))
WARNING: psql major version 10, server major version 11.
         Some psql features might not work.
Type "help" for help.

db=#

Create a table and add data

db=# CREATE TABLE COLOR(
db(#    ID SERIAL PRIMARY KEY   NOT NULL,
db(#    NAME           TEXT     NOT NULL UNIQUE,
db(#    RED            SMALLINT NOT NULL,
db(#    GREEN          SMALLINT NOT NULL,
db(#    BLUE           SMALLINT NOT NULL
db(# );
CREATE TABLE
db=# 
db=# INSERT INTO COLOR (NAME,RED,GREEN,BLUE) VALUES('GREEN',0,128,0);
INSERT 0 1
db=# INSERT INTO COLOR (NAME,RED,GREEN,BLUE) VALUES('RED',255,0,0);
INSERT 0 1
db=# INSERT INTO COLOR (NAME,RED,GREEN,BLUE) VALUES('BLUE',0,0,255);
INSERT 0 1
db=# INSERT INTO COLOR (NAME,RED,GREEN,BLUE) VALUES('WHITE',255,255,255);
INSERT 0 1
db=# INSERT INTO COLOR (NAME,RED,GREEN,BLUE) VALUES('YELLOW',255,255,0);
INSERT 0 1
db=# INSERT INTO COLOR (NAME,RED,GREEN,BLUE) VALUES('LIME',0,255,0);
INSERT 0 1
db=# INSERT INTO COLOR (NAME,RED,GREEN,BLUE) VALUES('BLACK',255,255,255);
INSERT 0 1
db=# INSERT INTO COLOR (NAME,RED,GREEN,BLUE) VALUES('GRAY',128,128,128);
INSERT 0 1
db=# 
db=# SELECT * FROM COLOR;
 id |  name  | red | green | blue 
----+--------+-----+-------+------
  1 | GREEN  |   0 |   128 |    0
  2 | RED    | 255 |     0 |    0
  3 | BLUE   |   0 |     0 |  255
  4 | WHITE  | 255 |   255 |  255
  5 | YELLOW | 255 |   255 |    0
  6 | LIME   |   0 |   255 |    0
  7 | BLACK  | 255 |   255 |  255
  8 | GRAY   | 128 |   128 |  128
(8 rows)

db=# 

Delete and Recreate the Postgres Instance

You can now destroy the Postgres instance and create it again, the data is preserved.

$ kubectl delete -f postgresql.yaml 
configmap "psql-itwl-dev-01-cm" deleted
persistentvolume "psql-itwl-dev-01-pv" deleted
persistentvolumeclaim "psql-itwl-dev-01-pvc" deleted
deployment.apps "psql-itwl-dev-01" deleted
service "postgres-service-np" deleted

$ kubectl apply -f postgresql.yaml 
configmap/psql-itwl-dev-01-cm created
persistentvolume/psql-itwl-dev-01-pv created
persistentvolumeclaim/psql-itwl-dev-01-pvc created
deployment.apps/psql-itwl-dev-01 created
service/postgres-service-np created

$ kubectl get all
NAME                                    READY   STATUS              RESTARTS   AGE
pod/psql-itwl-dev-01-594c7468c7-7hrzt   0/1     ContainerCreating   0          4s

NAME                          TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
service/kubernetes            ClusterIP   10.96.0.1      <none>        443/TCP          4d1h
service/postgres-service-np   NodePort    10.100.67.43   <none>        5432:30100/TCP   3s

NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/psql-itwl-dev-01   0/1     1            0           4s

NAME                                          DESIRED   CURRENT   READY   AGE
replicaset.apps/psql-itwl-dev-01-594c7468c7   1         1         0       4s

$ kubectl logs pod/psql-itwl-dev-01-594c7468c7-7hrzt -f
2019-06-10 18:05:51.456 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2019-06-10 18:05:51.457 UTC [1] LOG:  listening on IPv6 address "::", port 5432
2019-06-10 18:05:51.466 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2019-06-10 18:05:51.547 UTC [22] LOG:  database system was interrupted; last known up at 2019-06-10 18:04:54 UTC
2019-06-10 18:05:53.043 UTC [22] LOG:  database system was not properly shut down; automatic recovery in progress
2019-06-10 18:05:53.055 UTC [22] LOG:  redo starts at 0/1676B10
2019-06-10 18:05:53.055 UTC [22] LOG:  invalid record length at 0/1676BB8: wanted 24, got 0
2019-06-10 18:05:53.055 UTC [22] LOG:  redo done at 0/1676B48
2019-06-10 18:05:53.087 UTC [1] LOG:  database system is ready to accept connections

Use the psql client to check that the database tables have been preserved:

# psql db -h 192.168.50.11 -p 30100 -U user
Password for user user: 
psql (10.8 (Ubuntu 10.8-0ubuntu0.18.10.1), server 11.3 (Debian 11.3-1.pgdg90+1))
WARNING: psql major version 10, server major version 11.
         Some psql features might not work.
Type "help" for help.

db=# select * from COLOR;
 id |  name  | red | green | blue 
----+--------+-----+-------+------
  1 | GREEN  |   0 |   128 |    0
  2 | RED    | 255 |     0 |    0
  3 | BLUE   |   0 |     0 |  255
  4 | WHITE  | 255 |   255 |  255
  5 | YELLOW | 255 |   255 |    0
  6 | LIME   |   0 |   255 |    0
  7 | BLACK  | 255 |   255 |  255
  8 | GRAY   | 128 |   128 |  128
(8 rows)

db=#

How useful was this post?

Click on a star to rate it!

Average rating / 5. Vote count:

We are sorry that this post was not useful for you!

Let us improve this post!


Leave a Reply

avatar

This site uses Akismet to reduce spam. Learn how your comment data is processed.