Kubernetes: Deploy Postgres to a Cluster

Jun 04, 2023
Kubernetes

In this article, we will deploy Postgres and pgAdmin to a Kubernetes cluster. The article contains the contents of YAML files which can be applied to a cluster using kubectl e.g.

kubectl apply -f the-file.yaml

The YAML files work for minikube (a single node cluster for local testing). Some parts, in particular the persistence and exposing services outside of the cluster, will need to be tweaked if deploying to a cluster running in the cloud.

Note: I’d usually recommend a managed Postgres instance however on a tight budget running Postgres in your cluster can be useful.

Kubernetes Re-Cap

Below is a brief re-cap of some Kubernetes terms we’ll use in this article.

  • Persistent Volume: a piece of storage in the cluster.
  • Persistent Volume Claim: a claim on a persistent volume.
  • Deployment: a description of the desired state of a collection of pods.
  • Service: a way to expose pods to other pods in the cluster.
  • Config Map: non-confidential configuration data e.g. URLs, settings, etc.
  • Secret: confidential configuration data e.g. passwords, tokens, etc.

For a more in-depth explanation see the Kubernetes concepts documentation.

Namespace Setup

To keep the clusters resources organised we’ll keep all resources in the db namespace.

apiVersion: v1
kind: Namespace
metadata:
  name: db

Postgres Setup

Persistent Volume and Persistent Volume Claim

The YAML below creates a 5GB persistent volume using disk space on the node itself by mounting the “/data” directory.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: db-pv
  namespace: db
spec:
  storageClassName: manual
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/data"

Below is the claim for the use of the persistent volume created above. This will be different if deploying to a managed cluster in the cloud, for example, if you are running your cluster using DigitalOcean’s Kubernetes service changing the storageClassName to “do-block-storage” will automatically provision the requested storage.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: db-pvc
  namespace: db
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi

Postgres Config Map, Secret and Deployment

Postgres requires the database, user, and password to be supplied as environment variables. Below we create a config map and secret to store the values of these pieces of configuration.

apiVersion: v1
kind: ConfigMap
metadata:
  name: db-pg-cm
  namespace: db
data:
  POSTGRES_DB: minibuildsdb
  POSTGRES_USER: postgres
kubectl create secret generic db-pg-secret -n db \ 
    --from-literal=password='replace_with_real_password'

The Postgres deployment specifies the image to run and brings everything we’ve previously created together. The spec sets the number of replicas and labels used to identify which pods are managed by the deployment. The replica count must be set to 1 as the same data directory can’t be shared by multiple instances of Postgres.

The config and secret values are provided as environment variables in the envFrom and env sections. The persistent volume claim is mounts as “/var/lib/postgresql/data” which is the location Postgres writes to.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: db-pg-deployment
  namespace: db
spec:
  replicas: 1
  selector:
    matchLabels:
      app: db-pg-deployment
  template:
    metadata:
      labels:
        app: db-pg-deployment
    spec:
      containers:
      - name: postgres
        image: postgres:15
        imagePullPolicy: "IfNotPresent"
        envFrom:
          - configMapRef:
              name: db-pg-cm
        env:
          - name: POSTGRES_PASSWORD
            valueFrom:
              secretKeyRef:
                name: db-pg-secret
                key: password
        ports:
          - containerPort: 5432
        volumeMounts:
          - name: db-volume
            mountPath: /var/lib/postgresql/data
            subPath: postgres
        resources:
            limits:
              memory: "256Mi"
              cpu: "1"
            requests:
              memory: "32Mi"
              cpu: "0m"
      volumes:
        - name: db-volume
          persistentVolumeClaim:
            claimName: db-pvc

Postgres Service

Finally, we create a service that exposes the deployment to other pods running in the cluster. The service name is used as the hostname, so the connection string would look like postgresql://db-pg-svc:5432/....

apiVersion: v1
kind: Service
metadata:
  name: db-pg-svc
  namespace: db
spec:
  ports:
    - port: 5432
      protocol: TCP
  selector:
    app: db-pg-deployment

pgAdmin Setup

pgAdmin Config Map, Secret and Deployment

The config map and secret for pgAdmin are very similar to the Postgres equivalents, we set the default pgAdmin username and password.

apiVersion: v1
kind: ConfigMap
metadata:
  name: db-pgadmin-cm
  namespace: db
data:
  PGADMIN_DEFAULT_EMAIL: admin@admin.com
kubectl create secret generic db-pgadmin-secret -n db \
    --from-literal=password='replace_with_real_password'

Like the Postgres deployment we set up environment variables from the config map and secret.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: db-pgadmin-deployment
  namespace: db
spec:
  replicas: 1
  selector:
    matchLabels:
      app: db-pgadmin-deployment
  template:
    metadata:
      labels:
        app: db-pgadmin-deployment
    spec:
      containers:
      - name: pgadmin
        image: dpage/pgadmin4
        imagePullPolicy: "IfNotPresent"
        envFrom:
          - configMapRef:
              name: db-pgadmin-cm
        env:
          - name: PGADMIN_DEFAULT_PASSWORD
            valueFrom:
              secretKeyRef:
                name: db-pgadmin-secret
                key: password
        ports:
          - containerPort: 5050
        resources:
            limits:
              memory: "256Mi"
              cpu: "1"
            requests:
              memory: "32Mi"
              cpu: "0m"

pgAdmin Service

Unlike the Postgres service, we add a targetPort to access pgAdmin on port 80 instead of 5050.

apiVersion: v1
kind: Service
metadata:
  name: db-pgadmin-svc
  namespace: db
spec:
  ports:
    - port: 5050
      targetPort: 80
      protocol: TCP
  selector:
    app: db-pgadmin-deployment

Exposing pgAdmin outside of the Cluster

With pgAdmin deployed, we can expose the pgAdmin service to the outside world. With minikube we can simply expose the service as below.

minikube service -n db --url db-pgadmin-svc
# outputs a url exposing pgAdmin e.g. http://localhost:65142

However, to do this in a cluster running in the cloud you’d create an Ingress to expose the service.