{"id":840,"date":"2021-08-25T10:18:30","date_gmt":"2021-08-25T10:18:30","guid":{"rendered":"https:\/\/blog.ngocha.biz\/?p=840"},"modified":"2021-08-25T10:18:30","modified_gmt":"2021-08-25T10:18:30","slug":"deploy-postgresql-statefulset","status":"publish","type":"post","link":"https:\/\/blog.ngocha.biz\/?p=840","title":{"rendered":"How to Deploy PostgreSQL Statefulset Cluster on Kubernetes"},"content":{"rendered":"<p>This article aims to explain each of the components required to deploy PostgreSQL statefulset cluster on Kubernetes. We will also discuss the highly available PostgreSQL cluster setup using pgpool and repmgr.<\/p>\n<p>Towards the end of the article, we have added the guide to perform basic operations inside the PostgreSQL cluster using a client pod.<\/p>\n<p>As a beginner, creating components one by one while understanding the steps involved is a great way to learn about <a href=\"https:\/\/devopscube.com\/kubernetes-tutorials-beginners\/\" rel=\"noreferrer noopener\">Kubernetes<\/a> and Postgres.<\/p>\n<p>Going step by step ensures that you can focus on understanding the \u2018why\u2019 while learning the \u2018how\u2019.<\/p>\n<p>Here is what you are going to learn in this comprehensive PostgreSQL deployment guide.<\/p>\n<h2 id=\"postgresql-cluster-kubernetes-manifests\"><strong>PostgreSQL Cluster Kubernetes Manifests<\/strong><\/h2>\n<p>All the Kubernetes YAML manifests used in this guide are hosted on Github. Clone the repository for reference and implementation.<\/p>\n<pre><code>git clone https:\/\/github.com\/scriptcamp\/kubernetes-postgresql.git<\/code><\/pre>\n<p>We have explained all the manifests required for PostgreSQL on Kubernetes. If you have trouble copying the manifests from the article, please clone and refer to the manifests directly.<\/p>\n<p>We have categorized the manifest into three folders named <strong>client<\/strong>, <strong>pgpool<\/strong> and  <strong>postgresql<\/strong> as shown below.<\/p>\n<pre><code>\u251c\u2500\u2500 client\n\u2502&nbsp;&nbsp; \u251c\u2500\u2500 client-pod.yaml\n\u2502&nbsp;&nbsp; \u2514\u2500\u2500 psql-client.yaml\n\u251c\u2500\u2500 pgpool\n\u2502&nbsp;&nbsp; \u251c\u2500\u2500 nginx.yaml\n\u2502&nbsp;&nbsp; \u251c\u2500\u2500 pgpool-deployment.yaml\n\u2502&nbsp;&nbsp; \u251c\u2500\u2500 pgpool-secret.yaml\n\u2502&nbsp;&nbsp; \u251c\u2500\u2500 pgpool-svc-nodeport.yaml\n\u2502&nbsp;&nbsp; \u2514\u2500\u2500 pgpool-svc.yaml\n\u2514\u2500\u2500 postgresql\n    \u251c\u2500\u2500 postgres-configmap.yaml\n    \u251c\u2500\u2500 postgres-headless-svc.yaml\n    \u251c\u2500\u2500 postgres-secrets.yaml\n    \u2514\u2500\u2500 postgres-statefulset.yaml<\/code><\/pre>\n<p>If you wish to deploy the components in one go, cd into each directory and execute the following. Start with <strong>postgresql<\/strong> directory.<\/p>\n<pre><code>kubectl apply -f .<\/code><\/pre>\n<h2 id=\"bitnami-postgresql-docker-image\"><strong>Bitnami PostgreSQL Docker Image<\/strong><\/h2>\n<p>This tutorial has used Bitnami docker images, this has been done intentionally. There are certain advantages you can get as a beginner by using a Bitnami image.<\/p>\n<ol>\n<li>Bitnami images are shipped with necessary components pre-installed. This lets us maintain our focus can understanding and becoming familiar with Kubernetes side of things in depth.<\/li>\n<li>Bitnami images are well tested and validated before being released. This helps us save time and overcome any problems we may face with newer versions or patches.<\/li>\n<li>Bitnami images are very well documented, you&#8217;ll find a satisfactory explaination of each and every environment variable being used by the bitnami image. So a natural fit for beginners.<\/li>\n<\/ol>\n<p>Above all, as a beginner &#8211; we should focus on understanding kubernetes components and avoid getting off our goal due to installing dozens of packages, finding documentation, etc.<\/p>\n<p>Bitnami does just that for us.<\/p>\n<h2 id=\"high-level-architecture\">High Level Architecture<\/h2>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/image-18-37.png\" class=\"kg-image\" alt=\"PostgreSQL Statefulset in Kubernetes architecture\" loading=\"lazy\" width=\"1259\" height=\"638\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/03\/image-18-37.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/03\/image-18-37.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/image-18-37.png 1259w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<h2 id=\"create-a-namespace\">Create a Namespace<\/h2>\n<p>To deploy the PostgreSQL cluster, we will create a dedicated namespace named database.<\/p>\n<pre><code>kubectl create namespace database<\/code><\/pre>\n<p>The manifest files do not have the namespace added to them. So we will add the namespace while deploying each component. If you don&#8217;t specify the namespace, it gets deployed in the default namespace.<\/p>\n<h2 id=\"creating-postgres-server-configmaps\"><strong>Creating Postgres Server ConfigMaps<\/strong><\/h2>\n<p>A ConfigMaps in Kubernetes lets us mount files on containers without the need to make changes to the Dockerfile or rebuilding the container image.<\/p>\n<p>This feature is extremely helpful in cases where configurations have to be modified or created through files.<\/p>\n<p>Postgres requires a script (<code>pre-stop.sh<\/code>) to be run whenever it is about to be stopped. We will mount this script into the pod using config maps.<\/p>\n<p>Save the following manifest as <code>postgres-configmap.yaml<\/code><\/p>\n<pre><code>apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: postgres-configmap\ndata:\n  pre-stop.sh: |-\n    #!\/bin\/bash\n    set -o errexit\n    set -o pipefail\n    set -o nounset\n\n    # Debug section\n    exec 3&gt;&amp;1\n    exec 4&gt;&amp;2\n\n    # Load Libraries\n    . \/opt\/bitnami\/scripts\/liblog.sh\n    . \/opt\/bitnami\/scripts\/libpostgresql.sh\n    . \/opt\/bitnami\/scripts\/librepmgr.sh\n\n    # Auxiliary functions\n    is_new_primary_ready() {\n        return_value=1\n        currenty_primary_node=\"$(repmgr_get_primary_node)\"\n        currenty_primary_host=\"$(echo $currenty_primary_node | awk '{print $1}')\"\n\n        info \"$currenty_primary_host != $REPMGR_NODE_NETWORK_NAME\"\n        if [[ $(echo $currenty_primary_node | wc -w) -eq 2 ]] &amp;&amp; [[ \"$currenty_primary_host\" != \"$REPMGR_NODE_NETWORK_NAME\" ]]; then\n            info \"New primary detected, leaving the cluster...\"\n            return_value=0\n        else\n            info \"Waiting for a new primary to be available...\"\n        fi\n        return $return_value\n    }\n\n    export MODULE=\"pre-stop-hook\"\n\n    if [[ \"${BITNAMI_DEBUG}\" == \"true\" ]]; then\n        info \"Bash debug is on\"\n    else\n        info \"Bash debug is off\"\n        exec 1&gt;\/dev\/null\n        exec 2&gt;\/dev\/null\n    fi\n\n    # Load PostgreSQL &amp; repmgr environment variables\n    . \/opt\/bitnami\/scripts\/postgresql-env.sh\n\n    postgresql_enable_nss_wrapper\n\n    # Prepare env vars for managing roles\n    primary_node=\"$(repmgr_get_primary_node)\"\n    primary_host=\"$(echo $primary_node | awk '{print $1}')\"\n\n    # Stop postgresql for graceful exit.\n    postgresql_stop\n\n    if [[ \"$primary_host\" == \"$REPMGR_NODE_NETWORK_NAME\" ]]; then\n        info \"Primary node need to wait for a new primary node before leaving the cluster\"\n        retry_while is_new_primary_ready 10 5\n    else\n        info \"Standby node doesn't need to wait, leaving the cluster.\"\n    fi<\/code><\/pre>\n<p>Create the configmap<\/p>\n<pre><code>kubectl apply -f postgres-configmap.yaml -n database<\/code><\/pre>\n<p>It&#8217;s important to understand what the script is trying to do. Here is the overview:<\/p>\n<ol>\n<li>The script first checks what type of component is getting stopped i.e. master or follower.<\/li>\n<li>In case the master is getting stopped &#8211; the script delays the stoppage of the pod until a previous follower gets promoted to master.<\/li>\n<li>This is done to ensure high availabiliy i.e. atleast one master with write capabilities should exist.<\/li>\n<\/ol>\n<p>Confusing? Don&#8217;t worry. Just follow on. After reading the high availability section, the above points would become clear and make much more sense. <\/p>\n<p>Be sure to go through the script and revisit the points.<\/p>\n<h2 id=\"deploy-postgresql-services\"><strong>Deploy PostgreSQL Services<\/strong><\/h2>\n<p>Services in Kubernetes are the objects that pods use to communicate with each other. Services of type <code>ClusterIP<\/code> are usually used for inter-pod communication.<\/p>\n<p>There are two types of ClusterIP services<\/p>\n<ol>\n<li>Headless Services<\/li>\n<li>Services<\/li>\n<\/ol>\n<p>Normal Kubernetes services act as load balancers and <strong>follow round-robin logic<\/strong> to distribute loads. Headless services don\u2019t act like load balancers. Also, normal services are assigned IPs by Kubernetes whereas Headless services are not.<\/p>\n<p>In the case of Postgres servers, we require a headless service because it is a requirement for the PostgresSQL statefulset.<\/p>\n<p>Save the following manifest as <code>postgres-headless-svc.yaml<\/code>.<\/p>\n<pre><code>apiVersion: v1\nkind: Service\nmetadata:\n  name: postgres-headless-svc\nspec:\n  type: ClusterIP\n  clusterIP: None\n  ports:\n  - name: postgresql\n    port: 5432\n    targetPort: postgresql\n    protocol: TCP \n  selector:\n    app: postgres<\/code><\/pre>\n<p>Create the service.<\/p>\n<pre><code>kubectl apply -f postgres-headless-svc.yaml -n database<\/code><\/pre>\n<h2 id=\"create-postgressql-server-secrets\"><strong>Create PostgresSQL Server Secrets<\/strong><\/h2>\n<p>Secrets in Kubernetes are the objects used for supplying sensitive information to containers. They are like ConfigMaps with the difference that data is store in a base 64 encoded format.<\/p>\n<p>For the security of our PostgreSQL cluster, it is wise to restrict access to the database with a password. We will use Secrets to mount our desired passwords to the containers.<\/p>\n<blockquote><p><strong>Note:<\/strong> In production use cases, a secrets management solution like hashicorp vault will be used to store and retrive secrets. You can refer to <a href=\"https:\/\/devopscube.com\/vault-in-kubernetes\/\" rel=\"noreferrer noopener\">hashicorp vault setup guide<\/a> on Kubernetes to understand more.<\/p><\/blockquote>\n<p>Save the following manifest as <code>postgres-secrets.yaml<\/code>. Please change the password with a secure password of your own.<\/p>\n<pre><code>apiVersion: v1\nkind: Secret\nmetadata:\n  name: postgres-secrets\ndata:\n  postgresql-password: \"V2JyVHBOM2c3cQ==\"\n  repmgr-password: \"c1ZwYm90R2d4Nw==\"<\/code><\/pre>\n<p>Create the Secret.<\/p>\n<pre><code>kubectl apply -f postgres-secrets.yaml -n database<\/code><\/pre>\n<p>When the cluster is initialized, it creates certain users like <code>postgres<\/code> &amp; <code>repmgr<\/code> in our case. The above passwords are for these users.<\/p>\n<h2 id=\"postgresql-statefulset\"><strong>PostgreSQL StatefulSet<\/strong><\/h2>\n<p>While deploying the PostgreSQL on Kubernetes, what object type should be used and why? <a href=\"https:\/\/devopscube.com\/kubernetes-deployment-tutorial\/\" rel=\"noreferrer noopener\"><strong>Deployments<\/strong><\/a><strong> or StatefulSets?<\/strong> <\/p>\n<p>The answer is StatefulSet. Let&#8217;s discuss!<\/p>\n<p>StatefulSet is the Kubernetes object used to manage stateful applications. It\u2019s preferred over deployments for this use case as it provides guarantees about the ordering and uniqueness of these Pods i.e. the management of volumes is better with stateful sets.<\/p>\n<p>This section is critical to get a deeper understanding of the deployment logic. So read with full concentration!<\/p>\n<p>As a beginner, it is important to understand why we want to deploy a Statefulset and not Deployments. After all, our focus is on understanding the \u2018why\u2019 along with learning the \u2018how\u2019.<\/p>\n<h2 id=\"why-do-we-need-postgresql-statefulset\"><strong>Why do We Need PostgreSQL<\/strong> <strong>Statefulset?<\/strong><\/h2>\n<p>Postgres is going to be a stateful application i.e. it stores data (like tables, users) inside a volume. If the data is stored in pod ephemeral storage, then the data will get erased once the pod restarts.<\/p>\n<p>Also, Postgres may have to be scaled to more than one pod in caseload increases.<\/p>\n<p>All these operations have to be done in such a way that data consistency is maintained across all the pods like <code>postgres-<\/code>0, <code>postgres-1<\/code>, <code>postgres-2<\/code>.<\/p>\n<p>How can we achieve this in Kubernetes? Think and then read ahead!<\/p>\n<p>Postgres implements continuous replication of data across all its pods. So when data is written on <code>postgres-0<\/code> it gets replicated into <code>postgres-1<\/code>. <code>postgres-2<\/code> replicates data from <code>postgres-3<\/code>. And so on\u2026<\/p>\n<p>Postgres is able to do this continuous replication using an open-source tool called <a href=\"https:\/\/repmgr.org\/?ref=devopscube.com\" rel=\"noreferrer noopener\">RepMgr<\/a> which comes built-in with Postgres docker image.<\/p>\n<p>The thing to understand here is that <code>postgres-1<\/code> needs to know where to look for <code>postgres-0<\/code>. Otherwise, How will the replication happen?<\/p>\n<ol>\n<li>How will it know from where to fetch data for the replication process?<\/li>\n<li>How will postgres-1 know where to look for postgres-0?<\/li>\n<li>How will postgres-2 know where to look for postgres-1?<\/li>\n<\/ol>\n<p>Let\u2019s try to answer these questions now.<\/p>\n<p>In case of deployments &amp; stateful sets, pods are always assigned a unique name that can be used to look for the pods.<\/p>\n<p>In the <strong>case of deployments,<\/strong> pods are always assigned a unique name but this unique name <strong>changes after the pod are deleted &amp; recreated<\/strong>. So it\u2019s not useful to identify any pod.<\/p>\n<pre><code>Case of deployments: name of pod initially: postgres-bcr25qd41c-skxpe  <\/code><\/pre>\n<p>In the <strong>case of the stateful set <\/strong>\u2013 each pod is assigned a unique name and this <strong>unique name stays with it even if the pod is deleted <\/strong>&amp; recreated.<\/p>\n<pre><code>Case of statefulsets: name of pod initially: postgres-0 <\/code><\/pre>\n<p>That\u2019s why we want to use a stateful set here i.e. so that we can reach any pod without any discrepancies.<\/p>\n<p>Also, this unique ordering ensures that each pod is allocated the same underlying volume irrespective of pod restarts.<\/p>\n<p>These concepts of Statefulset &amp; deployments are not unique to Postgres deployments, if you explore \u2013 you\u2019ll find that many popular Kubernetes tools use stateful sets.<\/p>\n<p>Some examples are Hashicorp&#8217;s Vault, Elasticsearch, and many more. All of them use Stateful Set and not deployments due to the same logic.<\/p>\n<h2 id=\"deploy-postgresql-statefulset\"><strong>Deploy PostgreSQL StatefulSet<\/strong><\/h2>\n<p>First, let\u2019s create the Statefulset. I have added an explanation for the Postgres Statefulset as well towards the end of this section.<\/p>\n<p>Save the following manifest as <code>postgres-statefulset.yaml<\/code><\/p>\n<pre><code>apiVersion: apps\/v1\nkind: StatefulSet\nmetadata:\n  name: postgres-sts\nspec:\n  serviceName: postgres-headless-svc\n  replicas: 3\n  selector:\n    matchLabels:\n      app: postgres\n  template:\n    metadata:\n      labels:\n        app: postgres\n    spec:\n      securityContext:\n        fsGroup: 1001\n      containers:\n        - name: postgresql\n          lifecycle:\n            preStop:\n              exec:\n                command:\n                  - \/pre-stop.sh\n          image: docker.io\/bitnami\/postgresql-repmgr:11.12.0-debian-10-r44\n          imagePullPolicy: \"IfNotPresent\"\n          securityContext:\n            runAsUser: 1001\n          # Auxiliary vars to populate environment variables\n          env:\n            - name: BITNAMI_DEBUG\n              value: \"false\"\n            # PostgreSQL configuration\n            - name: POSTGRESQL_VOLUME_DIR\n              value: \"\/bitnami\/postgresql\"\n            - name: PGDATA\n              value: \"\/bitnami\/postgresql\/data\"\n            - name: POSTGRES_USER\n              value: \"postgres\"\n            - name: POSTGRES_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: postgres-secrets\n                  key: postgresql-password\n            - name: POSTGRES_DB\n              value: \"postgres\"\n            - name: POSTGRESQL_LOG_HOSTNAME\n              value: \"true\"\n            - name: POSTGRESQL_LOG_CONNECTIONS\n              value: \"false\"\n            - name: POSTGRESQL_LOG_DISCONNECTIONS\n              value: \"false\"\n            - name: POSTGRESQL_PGAUDIT_LOG_CATALOG\n              value: \"off\"\n            - name: POSTGRESQL_CLIENT_MIN_MESSAGES\n              value: \"error\"\n            - name: POSTGRESQL_SHARED_PRELOAD_LIBRARIES\n              value: \"pgaudit, repmgr\"\n            - name: POSTGRESQL_ENABLE_TLS\n              value: \"no\"\n            # Repmgr configuration\n            - name: MY_POD_NAME\n              valueFrom:\n                fieldRef:\n                  fieldPath: metadata.name\n            - name: REPMGR_UPGRADE_EXTENSION\n              value: \"no\"\n            - name: REPMGR_PGHBA_TRUST_ALL\n              value: \"no\"\n            - name: REPMGR_MOUNTED_CONF_DIR\n              value: \"\/bitnami\/repmgr\/conf\"\n            - name: REPMGR_NAMESPACE\n              valueFrom:\n                fieldRef:\n                  fieldPath: metadata.namespace\n            - name: REPMGR_PARTNER_NODES\n              value: postgres-sts-0.postgres-headless-svc.$(REPMGR_NAMESPACE).svc.cluster.local,postgres-sts-1.postgres-headless-svc.$(REPMGR_NAMESPACE).svc.cluster.local,postgres-sts-2.postgres-headless-svc.$(REPMGR_NAMESPACE).svc.cluster.local\n            - name: REPMGR_PRIMARY_HOST\n              value: \"postgres-sts-0.postgres-headless-svc.$(REPMGR_NAMESPACE).svc.cluster.local\"\n            - name: REPMGR_NODE_NAME\n              value: \"$(MY_POD_NAME)\"\n            - name: REPMGR_NODE_NETWORK_NAME\n              value: \"$(MY_POD_NAME).postgres-headless-svc.$(REPMGR_NAMESPACE).svc.cluster.local\"\n            - name: REPMGR_LOG_LEVEL\n              value: \"NOTICE\"\n            - name: REPMGR_CONNECT_TIMEOUT\n              value: \"5\"\n            - name: REPMGR_RECONNECT_ATTEMPTS\n              value: \"3\"\n            - name: REPMGR_RECONNECT_INTERVAL\n              value: \"5\"\n            - name: REPMGR_USERNAME\n              value: \"repmgr\"\n            - name: REPMGR_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: postgres-secrets\n                  key: repmgr-password\n            - name: REPMGR_DATABASE\n              value: \"repmgr\"\n          ports:\n            - name: postgresql\n              containerPort: 5432\n              protocol: TCP\n          livenessProbe:\n            exec:\n              command:\n                - bash\n                - -ec\n                - 'PGPASSWORD=$POSTGRES_PASSWORD psql -w -U \"postgres\" -d \"postgres\"  -h 127.0.0.1 -c \"SELECT 1\"'\n            initialDelaySeconds: 30\n            periodSeconds: 10\n            timeoutSeconds: 5\n            successThreshold: 1\n            failureThreshold: 6\n          readinessProbe:\n            exec:\n              command:\n                - bash\n                - -ec\n                - 'PGPASSWORD=$POSTGRES_PASSWORD psql -w -U \"postgres\" -d \"postgres\"  -h 127.0.0.1 -c \"SELECT 1\"'\n            initialDelaySeconds: 5\n            periodSeconds: 10\n            timeoutSeconds: 5\n            successThreshold: 1\n            failureThreshold: 6\n          volumeMounts:\n            - name: data\n              mountPath: \/bitnami\/postgresql\n            - name: hooks-scripts\n              mountPath: \/pre-stop.sh\n              subPath: pre-stop.sh\n      volumes:\n        - name: hooks-scripts\n          configMap:\n            name: postgres-configmap\n            defaultMode: 0755\n  volumeClaimTemplates:\n    - metadata:\n        name: data\n      spec:\n        accessModes:\n          - \"ReadWriteOnce\"\n        resources:\n          requests:\n            storage: \"1Gi\"<\/code><\/pre>\n<p>Create the Statefulset.<\/p>\n<pre><code>kubectl apply -f postgres-statefulset.yaml -n database<\/code><\/pre>\n<p>The Statefulset YAML of the PostgreSQL server has components such as configmap mounts, security context, probes, etc. Let understand the key configurations.<\/p>\n<p><strong>Metadata as env vars:<\/strong> In Kubernetes, information like the name of pods, the namespace of the pods can be utilized as env var for the pod. <\/p>\n<p>This is useful in cases where the env vars need to use pods metadata or some Kubernetes defined fields for the pods.<\/p>\n<pre><code>- name: MY_POD_NAME\n  valueFrom:\n    fieldRef:\n      fieldPath: metadata.name \n- name: REPMGR_PASSWORD\n  valueFrom:\n    secretKeyRef:\n      name: postgres-secrets <\/code><\/pre>\n<p><strong>Env vars injected through secrets:<\/strong> Sometimes the containers need to know the sensitive data in order to make use of it. <\/p>\n<p>For e.g. in order to assign a password to the Postgres database, the required password must be supplied securely to the Postgres container.<\/p>\n<pre><code> - name: POSTGRES_PASSWORD\n   valueFrom:\n     secretKeyRef:\n       name: postgres-secrets\n       key: postgresql-password\n - name: REPMGR_PASSWORD\n   valueFrom:\n     secretKeyRef:\n       name: postgres-secrets\n       key: repmgr-password <\/code><\/pre>\n<p><strong>Probes:<\/strong> Probes ensure that the vault does not get stuck in a loop due to any bug and can be restarted automatically in case an unexpected error comes up.<\/p>\n<pre><code>  livenessProbe:\n    exec:\n      command:\n        - bash\n        - -ec\n        - 'PGPASSWORD=$POSTGRES_PASSWORD psql -w -U \"postgres\" -d \"postgres\"  -h 127.0.0.1 -c \"SELECT 1\"'\n    initialDelaySeconds: 30\n    periodSeconds: 10\n    timeoutSeconds: 5\n    successThreshold: 1\n    failureThreshold: 6\n  readinessProbe:\n    exec:\n      command:\n        - bash\n        - -ec\n        - 'PGPASSWORD=$POSTGRES_PASSWORD psql -w -U \"postgres\" -d \"postgres\"  -h 127.0.0.1 -c \"SELECT 1\"'\n    initialDelaySeconds: 5\n    periodSeconds: 10\n    timeoutSeconds: 5\n    successThreshold: 1\n    failureThreshold: 6<\/code><\/pre>\n<h3 id=\"basic-principle-of-the-above-probes\"><strong>Basic principle of the above probes<\/strong><\/h3>\n<p>Here the command used is just going to the &#8220;<code>postgres<\/code>&#8221; database using the &#8220;<code>postgres<\/code>&#8221; user and running the &#8220;SELECT 1&#8221; query. <\/p>\n<p>If the Postgres process is running correctly, then the query will have a successful exit code. Otherwise not. <\/p>\n<p>That&#8217;s how we can be sure that the probes can tell us if the process is running or the container needs to be restarted!<\/p>\n<p><strong>VolumeClaimTemplates: <\/strong>A template by which a stateful set can create volumes for replicas.<\/p>\n<pre><code>  volumeClaimTemplates:\n    - metadata:\n        name: data\n      spec:\n        accessModes:\n          - \"ReadWriteOnce\"\n        resources:\n          requests:\n            storage: \"1Gi\"<\/code><\/pre>\n<p>Let&#8217;s go through the important env vars for the Postgres process now.<\/p>\n<ol>\n<li><strong>POSTGRESQL_VOLUME_DIR<\/strong>: The directory where the Postgres process shall write its configuration files and data. This is the directory that we should mount with a PVC.<\/li>\n<li><strong>PGDATA<\/strong>: The directory inside the main Postgres directory where the data directory should be created.<\/li>\n<li><strong>POSTGRES_USER<\/strong>: The user that should be created automatically when the Postgres process starts.<\/li>\n<li><strong>POSTGRES_PASSWORD<\/strong>: The password for the user created by default.<\/li>\n<li><strong>POSTGRES_DB<\/strong>: The database which should be created when the Postgres process starts.<\/li>\n<\/ol>\n<p>Let&#8217;s discuss the <strong>RepMgr<\/strong> now.<\/p>\n<h2 id=\"repmgr-inside-postgressql-servers\"><strong>RepMgr Inside PostgresSQL Servers<\/strong><\/h2>\n<p>RepMgr is an open-source tool shipped with Postgres which serves two purposes: Replication &amp; Failover.<\/p>\n<ol>\n<li><strong>Replication<\/strong>: It replicates data from the primary server to all the replicas. This helps in reducing the load on the servers by distributing read &amp; write queries.<\/li>\n<li><strong>Failover<\/strong>: It can handle failovers in the cluster i.e. it can promote a read-only server to a primary server when required.<\/li>\n<\/ol>\n<p>You can read about it more here: <a href=\"https:\/\/repmgr.org\/?ref=devopscube.com\" rel=\"noreferrer noopener\">https:\/\/repmgr.org\/<\/a><\/p>\n<p>Let&#8217;s go through the important env vars for <strong>repmgr<\/strong> setup.<\/p>\n<ol>\n<li><strong>REPMGR_PARTNER_NODES<\/strong>: This expects a comma-separated list of all Postgres server addresses in the cluster. Including the primary server&#8217;s address.<\/li>\n<li><strong>REPMGR_PRIMARY_HOST<\/strong>: This expects the Postgres primary server&#8217;s address.<\/li>\n<li><strong>REPMGR_USERNAME<\/strong>: User to be created for the repmgr operations.<\/li>\n<li><strong>REPMGR_PASSWORD<\/strong>: Password to be created for the repmgr operations.<\/li>\n<li><strong>REPMGR_DATABASE<\/strong>: Database to be created for the repmgr operations.<\/li>\n<\/ol>\n<h2 id=\"tip-for-using-headless-svc-effectively\"><strong>Tip for using headless svc effectively<\/strong><\/h2>\n<p>As you know, headless svc does not work as a load balancer and is used to address a group of pods together. There is another use case for headless services.<\/p>\n<p>We can use it to get the address of individual pods. Let&#8217;s take an e.g. to understand this. We have three pods running as part of the Postgres Statefulset.<\/p>\n<p><!--kg-card-begin: html--><\/p>\n<table class=\"has-fixed-layout\">\n<tbody>\n<tr>\n<td><strong>Pod name<\/strong><\/td>\n<td><strong>Pod Address<\/strong><\/td>\n<\/tr>\n<tr>\n<td>postgres-sts-0<\/td>\n<td>172.17.0.3<\/td>\n<\/tr>\n<tr>\n<td>postgres-sts-1<\/td>\n<td>172.17.0.8<\/td>\n<\/tr>\n<tr>\n<td>postgres-sts-2<\/td>\n<td>172.17.0.10<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><!--kg-card-end: html--><\/p>\n<p>Pods and their addresses.<\/p>\n<p>A headless svc  &#8220;<code>postgres-headless-svc<\/code>&#8221; is pointed to these pods.<\/p>\n<p>If you do a <code>nslookup<\/code> from a pod running inside the same namespace of your cluster, you&#8217;ll be able to get the address of the above pods through the headless svc.<\/p>\n<pre><code>nslookup postgres-sts-0.postgres-headless-svc.database.svc.cluster.local\n\nServer:\t\t10.96.0.10\nAddress:\t10.96.0.10#53\n\nName:\tpostgres-sts-0.postgres-headless-svc.default.svc.cluster.local\nAddress: 172.17.0.3\n<\/code><\/pre>\n<p>The above concept is used very commonly in Kubernetes, in fact, the env var REPMGR_PARTNER_NODES is using this.<\/p>\n<h2 id=\"pg-pool-for-postgres\"><strong>Pg-Pool for Postgres<\/strong><\/h2>\n<p>Pg pool is a middleware component that sits in front of the Postgres servers and acts as a gatekeeper to the cluster. <\/p>\n<p>It mainly serves two purposes: Load balancing &amp; Limiting the requests.<\/p>\n<ol>\n<li><strong>Load Balancing:<\/strong> Pg pool takes connection requests and queries. It analyzes the query to decide where the query should be sent.<\/li>\n<li>Read-only queries can be handled by read-replicas. Write operations can only be handled by the primary server. In this way, it loads balances the cluster.<\/li>\n<li><strong>Limits the requests:<\/strong> Like any other system, Postgres has a limit on no. of concurrent connections it can handle gracefully.<\/li>\n<li>Pg-pool limits the no. of connections it takes up and queues up the remaining. Thus, gracefully handling the overload.<\/li>\n<\/ol>\n<h2 id=\"creating-the-pg-pool-secrets\"><strong>Creating the pg-pool secrets<\/strong><\/h2>\n<p>Save the following manifest as <code>pgpool-secret.yaml<\/code><\/p>\n<pre><code>apiVersion: v1\nkind: Secret\nmetadata:\n  name: pgpool-secrets\ndata:\n  admin-password: \"SFRzaVZxYjdSZQ==\"<\/code><\/pre>\n<p>Create the secret.<\/p>\n<pre><code>kubectl create -f pgpool-secret.yaml -n database<\/code><\/pre>\n<h2 id=\"creating-the-pg-pool-service\"><strong>Creating the pg-pool service<\/strong><\/h2>\n<p>Save the following manifest as <code>pgpool-svc.yaml<\/code><\/p>\n<pre><code>apiVersion: v1\nkind: Service\nmetadata:\n  name: pgpool-svc\nspec:\n  type: ClusterIP\n  sessionAffinity: None\n  ports:\n    - name: postgresql\n      port: 5432\n      targetPort: postgresql\n      protocol: TCP\n      nodePort: null\n  selector:\n    app: pgpool<\/code><\/pre>\n<p>Create the service.<\/p>\n<pre><code>kubectl apply -f pgpool-svc.yaml -n database<\/code><\/pre>\n<p>If you wish to access the cluster from outside the <a href=\"https:\/\/devopscube.com\/setup-kubernetes-cluster-kubeadm\/\" rel=\"noreferrer noopener\">kubernetes cluster<\/a>, you need to deploy a NodePort service as well. <\/p>\n<p>Save the following manifest as <code>pgpool-svc-nodeport.yaml<\/code><\/p>\n<pre><code>apiVersion: v1\nkind: Service\nmetadata:\n  name: pgpool-svc-nodeport\nspec:\n  type: NodePort\n  sessionAffinity: None\n  ports:\n    - name: postgresql\n      port: 5432\n      nodePort: 32000\n      targetPort: postgresql\n      protocol: TCP\n  selector:\n    app: pgpool<\/code><\/pre>\n<p>Create the service.<\/p>\n<pre><code>kubectl apply -f pgpool-svc-nodeport.yaml -n database\n<\/code><\/pre>\n<h2 id=\"deploy-pgpool-deployment\"><strong>Deploy pgpool Deployment<\/strong><\/h2>\n<p>Save the following manifest as <code>pgpool-deployment.yaml<\/code><\/p>\n<pre><code>apiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: pgpool-deployment\nspec:\n  replicas: 1\n  selector:\n    matchLabels: \n      app: pgpool\n  template:\n    metadata:\n      labels: \n        app: pgpool\n    spec:          \n      securityContext:\n        fsGroup: 1001\n      containers:\n        - name: pgpool\n          image: docker.io\/bitnami\/pgpool:4.2.3-debian-10-r38\n          imagePullPolicy: \"IfNotPresent\"\n          securityContext:\n            runAsUser: 1001\n          env:\n            - name: BITNAMI_DEBUG\n              value: \"false\"\n            - name: PGPOOL_BACKEND_NODES\n              value: 0:postgres-sts-0.postgres-headless-svc:5432,1:postgres-sts-1.postgres-headless-svc:5432,2:postgres-sts-2.postgres-headless-svc:5432\n            - name: PGPOOL_SR_CHECK_USER\n              value: \"repmgr\"\n            - name: PGPOOL_SR_CHECK_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: postgres-secrets\n                  key: repmgr-password\n            - name: PGPOOL_SR_CHECK_DATABASE\n              value: \"postgres\"\n            - name: PGPOOL_ENABLE_LDAP\n              value: \"no\"\n            - name: PGPOOL_POSTGRES_USERNAME\n              value: \"postgres\"\n            - name: PGPOOL_POSTGRES_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: postgres-secrets\n                  key: postgresql-password\n            - name: PGPOOL_ADMIN_USERNAME\n              value: \"admin\"\n            - name: PGPOOL_ADMIN_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: pgpool-secrets\n                  key: admin-password\n            - name: PGPOOL_ENABLE_LOAD_BALANCING\n              value: \"yes\"\n            - name: PGPOOL_ENABLE_LOG_CONNECTIONS\n              value: \"no\"\n            - name: PGPOOL_ENABLE_LOG_HOSTNAME\n              value: \"yes\"\n            - name: PGPOOL_ENABLE_LOG_PER_NODE_STATEMENT\n              value: \"no\"\n            - name: PGPOOL_CHILD_LIFE_TIME\n              value: \"\"\n            - name: PGPOOL_ENABLE_TLS\n              value: \"no\"\n          ports:\n            - name: postgresql\n              containerPort: 5432\n              protocol: TCP\n          livenessProbe:\n            exec:\n              command:\n                - \/opt\/bitnami\/scripts\/pgpool\/healthcheck.sh\n            initialDelaySeconds: 30\n            periodSeconds: 10\n            timeoutSeconds: 5\n            successThreshold: 1\n            failureThreshold: 5\n          readinessProbe:\n            exec:\n              command:\n                - bash\n                - -ec\n                - PGPASSWORD=${PGPOOL_POSTGRES_PASSWORD} psql -U \"postgres\" -d \"postgres\" -h \/opt\/bitnami\/pgpool\/tmp -tA -c \"SELECT 1\" &gt;\/dev\/null\n            initialDelaySeconds: 5\n            periodSeconds: 5\n            timeoutSeconds: 5\n            successThreshold: 1\n            failureThreshold: 5<\/code><\/pre>\n<p>Create the deployment.<\/p>\n<pre><code>kubectl apply -f pgpool-deployment.yaml -n database<\/code><\/pre>\n<h2 id=\"postgresql-high-availability\">PostgreSQL <strong>High Availability<\/strong><\/h2>\n<p>The failover mechanism provided by RepMgr and the Load balancing provided by Pg-pool ensure that the Postgres cluster remains up for a long time.<\/p>\n<p>The two mechanisms together ensure the high availability of the PostgreSQL cluster.<\/p>\n<p>With pod anti-affinity configurations, you can instruct Kubernetes to deploy pods in different nodes. This ensures you don&#8217;t have a single point of failure if a Kubernetes node goes down.<\/p>\n<p>Pod anti-affinity requires Kubernetes nodes to be labeled with appropriate labels. You can even dedicate few nodes in the cluster for databases by labeling them. With node affinity, all the PostgreSQL pods get assigned to nodes with specific labels.<\/p>\n<h2 id=\"connect-to-postgresql-cluster-from-client\"><strong>Connect to PostgreSQL Cluster From Client<\/strong><\/h2>\n<p>Let&#8217;s create a <strong>psql<\/strong> client pod. We will use it to connect &amp; verify the cluster. Copy the manifest contents as <code>psql-client.yaml<\/code><\/p>\n<pre><code>---\napiVersion: v1\nkind: Pod\nmetadata:\n  name: pg-client\nspec:\n  containers:\n    - image: bitnami\/postgresql:11.12.0-debian-10-r13\n      name: postgresql\n      env:\n      - name: ALLOW_EMPTY_PASSWORD\n        value: \"yes\"<\/code><\/pre>\n<p>Let&#8217;s deploy the client.<\/p>\n<pre><code>kubectl apply -f psql-client.yaml -n database<\/code><\/pre>\n<p>Now let&#8217;s try to connect to the cluster.<\/p>\n<p>Copy the password.<\/p>\n<pre><code>kubectl get secret postgres-secrets -n database -o jsonpath=\"{.data.postgresql-password}\" | base64 --decode<\/code><\/pre>\n<p>Exec &amp; connect.<\/p>\n<pre><code>kubectl exec -it pg-client -n database -- \/bin\/bash<\/code><\/pre>\n<p>We will use the password and the pg-pool service endpoint <code>pgpool-svc<\/code> to connect to the cluster.<\/p>\n<pre><code>PGPASSWORD=WbrTpN3g7q psql -h pgpool-svc -p 5432 -U postgres<\/code><\/pre>\n<p>For connecting from outside the cluster.<\/p>\n<pre><code>PGPASSWORD=WbrTpN3g7q psql -h &lt;IP ADDR&gt; -p &lt;NODEPORT ADDR of pgpool-svc-nodeport&gt; -U postgres\n\n\/\/For minikube, IP ADDR can be found out by executing \"minikube ip\"<\/code><\/pre>\n<p>For example,<\/p>\n<pre><code>PGPASSWORD=WbrTpN3g7q psql -h 34.138.59.54 -p 32000 -U postgres<\/code><\/pre>\n<p>Try some basic sample commands:<\/p>\n<p>create database db1; \\c db1; \/\/to connect to new database create table test (id int primary key not null, value text not null); insert into test values (1, &#8216;value1&#8217;); select * from test;<\/p>\n<h2 id=\"verifying-the-postgresql-replication\"><strong>Verifying the PostgreSQL<\/strong> <strong>Replication<\/strong><\/h2>\n<p>Command to verify if the replication is taking place.<\/p>\n<pre><code>\/\/after connecting to the cluster using psql client.\nselect * from pg_stat_replication;<\/code><\/pre>\n<p>No. of entries you should see =  No. of replicas of Postgres running minus 1. <\/p>\n<p><strong>Reason for &#8220;minus 1&#8221;<\/strong>: Data is being replicated from the master to the follower. It&#8217;s logically impossible for the Data to be replicated from the master to itself!<\/p>\n<p>Verify the failover by deleting the pods randomly and seeing if the cluster becomes unresponsive or not. I assure you, it will continue to work without issues!<\/p>\n<p>As an exercise, verify thoroughly! I always work on databases with the principle of &#8211; Trust but verify.<\/p>\n<p>Sample logs of follower pod when the primary pod goes down: Note how the messages inform the user about a failover taking place!<\/p>\n<pre><code>NOTICE: promoting standby to primary\nDETAIL: promoting server \"postgres-sts-1\" (ID: 1001) using \"\/opt\/bitnami\/postgresql\/bin\/pg_ctl -o \"--config-file=\"\/opt\/bitnami\/postgresql\/conf\/postgresql.conf\" --external_pid_file=\"\/opt\/bitnami\/postgresql\/tmp\/postgresql.pid\" --hba_file=\"\/opt\/bitnami\/postgresql\/conf\/pg_hba.conf\"\" -w -D '\/bitnami\/postgresql\/data' promote\"\n2021-07-28 20:38:11.362 GMT [266] LOG:  received promote request\n2021-07-28 20:38:11.370 GMT [266] LOG:  redo done at 0\/8000028\n2021-07-28 20:38:11.370 GMT [266] LOG:  last completed transaction was at log time 2021-07-28 20:36:57.642182+00\n2021-07-28 20:38:11.698 GMT [266] LOG:  selected new timeline ID: 2\n2021-07-28 20:38:12.494 GMT [266] LOG:  archive recovery complete\nNOTICE: waiting up to 60 seconds (parameter \"promote_check_timeout\") for promotion to complete\nDEBUG: get_recovery_type(): SELECT pg_catalog.pg_is_in_recovery()\nINFO: standby promoted to primary after 0 second(s)\nDEBUG: setting node 1001 as primary and marking existing primary as failed\nDEBUG: begin_transaction()\nDEBUG: commit_transaction()\n2021-07-28 20:38:13.105 GMT [264] LOG:  database system is ready to accept connections\nNOTICE: STANDBY PROMOTE successful<\/code><\/pre>\n<h2 id=\"measuring-postgresql-performance\"><strong>Measuring PostgreSQL Performance<\/strong><\/h2>\n<p>As a beginner, let us discuss the things that should be kept in mind regarding Postgre&#8217;s performance and metrics.<\/p>\n<ol>\n<li><strong>Fine Tuning Configurations: <\/strong>Performance mainly depends on the configurations that have been set. For e.g. performance may take a hit at certain points in time due to increased workloads. Ensure that you have followed all the suggested <a href=\"https:\/\/wiki.postgresql.org\/wiki\/Performance_Optimization?ref=devopscube.com\" rel=\"noreferrer noopener\">performance optmizations.<\/a><\/li>\n<li><strong>Optimized Queries: <\/strong>Another factor that impacts the performance is the types of queries that are dominant. If a lot of write operations are performed in a given time &#8211; then performance will go down. It also depends on the optimization of queries. The more optimized the queries &#8211; the better performance can be expected.<\/li>\n<li><strong>Cluster Size:<\/strong> Resources also affect performance. Let&#8217;s say &#8211; your cluster is made up of 3 pods. For some use cases &#8211; it may be sufficient. For many, it will not be enough.<\/li>\n<li><strong>Monitor your Database Metrics &amp; Analyse Logs:<\/strong> Monitor the data based for CPU spikes, number of connections, disk space etc..If you have <a href=\"https:\/\/devopscube.com\/setup-prometheus-monitoring-on-kubernetes\/\" rel=\"noreferrer noopener\">Prometheus setup<\/a>, you can use the <a href=\"https:\/\/github.com\/prometheus-community\/postgres_exporter?ref=devopscube.com\" rel=\"noreferrer noopener\">PostgreSQL Prometheus exporter<\/a> to get all the metrics.<\/li>\n<li><strong>Other Factors: <\/strong>The workload itself depends on a variety of factors such as backup schedule, traffic spikes, etc.<\/li>\n<\/ol>\n<p>Performance mainly depends on your use case and business commitments. This should be explored even more according to your use case!<\/p>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>That&#8217;s all folks! We have covered the detailed guide on deploying the <strong>PostgreSQL<\/strong> statefulset on Kubernetes.<\/p>\n<p>We have also discussed high availability and replication using pgpool and repmgr.<\/p>\n<p>When it comes to production, careful consideration has to be done on persistent volume management, backup, high availability, and performance.<\/p>\n<hr>\n<p><strong>Ngu\u1ed3n:<\/strong> <a href=\"https:\/\/devopscube.com\/deploy-postgresql-statefulset\/\" target=\"_blank\" rel=\"noopener noreferrer\">How to Deploy PostgreSQL Statefulset Cluster on Kubernetes \u2014 DevOpsCube<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Source: https:\/\/devopscube.com\/deploy-postgresql-statefulset\/<\/p>\n","protected":false},"author":1,"featured_media":841,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-840","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops"],"_links":{"self":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts\/840","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=840"}],"version-history":[{"count":0,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts\/840\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/media\/841"}],"wp:attachment":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=840"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=840"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=840"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}