Kubernetes: Deploy Postgres to a Cluster
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.