← Back to list

PostgreSQL on Kubernetes: A Deep Dive with CloudNativePG (CNPG)

Introduction: Mastering Stateful Workloads with CloudNativePG

Shashank Mayya · 2025-07-02 23:35 · 67 claps · 26.3 min read paywalled
#data-engineering #kubernetes #postgresql #database #analytics
Open on Medium ↗
Wiki topics: GRW · Growth & Analytics ☁️ · DevOps & Cloud 🔧 · Data Engineering

PostgreSQL on Kubernetes: A Deep Dive with CloudNativePG (CNPG)

Introduction: Mastering Stateful Workloads with CloudNativePG

For years, running stateful workloads like relational databases on Kubernetes was considered a frontier fraught with complexity and risk. The ephemeral nature of containers seemed fundamentally at odds with the persistence and stability required by a production database. However, the landscape has matured dramatically. The convergence of robust Kubernetes features, such as support for local persistent volumes, and the rise of sophisticated, “autopilot” operators has transformed this challenge into a solved problem. Today, running PostgreSQL on Kubernetes is not only viable but, in many cases, the superior architectural choice for achieving resilience, scalability, and operational efficiency.

At the forefront of this evolution is CloudNativePG, a CNCF Sandbox project that covers the full lifecycle of a highly available PostgreSQL cluster. Originally developed by EDB and now governed by a vendor-neutral community, CloudNativePG was engineered from the ground up to be truly Kubernetes-native. Unlike earlier operators that had to work around the limitations of a nascent Kubernetes ecosystem, CloudNativePG leverages a mature platform, allowing it to provide what the Operator Capability model calls a “Level V — Auto Pilot” experience.

Over the past year, I’ve put CNPG through its paces, exploring every feature from automated failover to advanced backup strategies. My conclusion is simple: it’s a game-changer for managing PostgreSQL in a cloud-native world.

This article serves as a comprehensive, hands-on guide for deploying and managing production-grade PostgreSQL clusters using the latest version as of this writing, CloudNativePG 1.26. We will move beyond simple examples to construct a realistic, enterprise-ready stack, demonstrating how to integrate CloudNativePG with popular, powerful tools: Rancher for Kubernetes management, Longhorn for resilient persistent storage, and MinIO for object storage backups. Through detailed explanations and complete, annotated YAML manifests, this guide will equip you to confidently deploy and operate PostgreSQL in the most demanding cloud-native environments.

High-level architecture of the guide’s technology stack.

High-level architecture of the guide’s technology stack.

By favoring convention over configuration, CNPG makes intelligent decisions for you on everything from replication setups to service endpoints. You simply declare your high-level intent, and the operator handles the implementation, while still allowing you to override any default for fine-grained control.

The CloudNativePG Philosophy: Why a Custom Controller Trumps StatefulSets

CloudNativePG’s effectiveness stems from a core architectural philosophy that sets it apart from many alternatives. It is designed to be “entirely declarative” and “exclusively relies on the Kubernetes API server” to manage the state of a PostgreSQL cluster. This approach has profound implications. It eliminates the need for external coordination tools like Patroni, etcd, or Zookeeper, which some other operators require for leader election and state management. By avoiding these extra dependencies, CloudNativePG reduces architectural complexity, minimizes potential points of failure, and adheres more closely to the native operational patterns of Kubernetes.

A cornerstone of this design is the operator’s decision to forgo Kubernetes StatefulSets in favor of its own custom pod controller. While StatefulSets provide useful abstractions for stateful applications — such as ordered pod naming, stable network identifiers, and persistent storage — their rigidity can be a hindrance for the nuanced lifecycle of a database cluster. Early approaches to running databases on Kubernetes relied on StatefulSets, but this was often due to the platform’s initial limitations rather than StatefulSets being the ideal tool for the job. The modern, recommended approach for complex stateful applications is a custom operator that extends Kubernetes’ capabilities. By directly managing Pods and their associated Persistent Volume Claims (PVCs), the CloudNativePG operator gains complete authority over the cluster topology and storage lifecycle, enabling more sophisticated and reliable automation.

Comparison of CloudNativePG’s custom controller vs. the generic StatefulSet controller.

Comparison of CloudNativePG’s custom controller vs. the generic StatefulSet controller.

This direct control addresses several key limitations of StatefulSets for managing a PostgreSQL cluster :

  • PVC Resizing: StatefulSets famously do not support the resizing of their associated PVCs, a critical limitation for a growing database. CloudNativePG’s custom controller, by managing PVCs directly, can orchestrate volume expansion seamlessly if the underlying storage class supports it.
  • Distinguishing Primary vs. Replicas: A StatefulSet uses a single template for all its pods, treating them as peers. However, a PostgreSQL cluster has distinct roles: one primary and multiple replicas. Operations must be role-aware. For example, a rolling update should apply changes to replicas first, then perform a controlled switchover to promote an updated replica before touching the old primary. Conversely, some configuration changes might need to be applied to the primary first. A generic, application-agnostic controller like StatefulSet cannot manage this PostgreSQL-specific logic.
  • Coherence of Multiple PVCs: Best practices often involve separating PostgreSQL’s data (PGDATA) and its Write-Ahead Logs (WAL) onto separate volumes for performance and reliability. These two PVCs are intrinsically linked; if the WAL volume is lost or recreated independently, the data volume becomes corrupted. A StatefulSet, unaware of this dependency, would simply recreate the missing PVC, leading to a broken instance. CloudNativePG's controller understands this relationship and would correctly create a new, coherent pair of PVCs for a new instance to join the cluster.
  • Flexible Failure Handling: The optimal response to a node failure depends on the storage architecture. If using remote storage, the pod can be rescheduled to another node and re-attach its PVC. If using high-performance local storage, it may be better to wait for the node to return or, for smaller databases, to provision a new instance from a clone. CloudNativePG’s controller can implement these varied strategies, offering a level of customized, intelligent recovery that a generic StatefulSet cannot provide.

This deep integration enables a powerful set of automated capabilities:

  • Self-Healing: The operator constantly monitors the health of the cluster. If a primary instance fails, it automatically promotes the most up-to-date replica to take its place. If a replica instance fails, it is automatically recreated to restore the desired level of redundancy.
  • Seamless Application Connectivity: For every Cluster resource, CloudNativePG automatically creates and manages three distinct Kubernetes Services :
  • -rw: This service always points to the single primary instance, providing a stable endpoint for write operations.
  • -ro: This service load-balances connections across all replica instances, ideal for scaling read workloads.
  • -r: This service load-balances connections across all instances in the cluster (primary and replicas), useful for read queries that can be served by any node. In the event of a failover, the operator instantly updates the endpoints of these services, ensuring that application traffic is transparently rerouted to the new primary with no manual intervention required.

The PostgreSQL Instance Manager: The Heart of the Pod

Instead of relying on an external tool for failover management, CloudNativePG uses a native key component called the Postgres instance manager. This self-contained Go application is the secret to CloudNativePG’s tight integration with Kubernetes and its robust lifecycle management.

When the operator creates a pod for a PostgreSQL instance, the instance manager is started as the parent process (PID 1) for the main container. It, in turn, runs and supervises the PostgreSQL postmaster process. This architecture allows the instance manager to act as an intelligent intermediary between the Kubernetes control plane (specifically the kubelet) and the PostgreSQL process itself.

Architecture of the PostgreSQL Instance Manager within a Pod.

Architecture of the PostgreSQL Instance Manager within a Pod.

Liveness and Readiness Probes

The instance manager is also responsible for handling the liveness and readiness probes sent by the kubelet.

  • Liveness Probe: This probe uses pg_isready to check if the PostgreSQL process is running. If the probe fails three consecutive times (with a 10-second interval), the kubelet will restart the container, assuming the instance is in a broken state.
  • Readiness Probe: This probe verifies that the database is fully up and able to accept connections. A pod is only considered “Ready” and added to service endpoints when this probe is successful.

To prevent premature restarts during a long database initialization, the .spec.startDelay parameter (defaulting to 30 seconds) delays the execution of the liveness probe.

A Deep Dive into CloudNativePG’s Replication Strategy

CloudNativePG’s high availability is built entirely on PostgreSQL’s robust, mature, and battle-tested native replication features. It does not use external tools, instead opting for what is known as application-level replication, where the database itself is responsible for keeping its copies in sync.

PostgreSQL’s Native Streaming Replication

The core mechanism is physical streaming replication. This process works by transferring the transaction log, known as the Write-Ahead Log (WAL), from the primary server to one or more standby servers (replicas) in real-time. The primary server has a “WAL sender” process for each replica, and each replica has a “WAL receiver” process. As transactions are committed on the primary, the changes are written to the WAL, and these WAL records are immediately streamed to the replicas. The replicas then apply these WAL records to their own data files, keeping them continuously updated and in sync with the primary. This allows replicas to serve as hot standbys, capable of handling read-only queries and being promoted to primary status almost instantly in case of a failure.

Asynchronous vs. Synchronous Replication

PostgreSQL offers two fundamental modes for streaming replication, each providing a different trade-off between performance and data consistency guarantees.

  • Asynchronous Replication (Default): In this mode, the primary server commits a transaction as soon as the WAL record is written to its own disk. It does not wait for confirmation that the replica has received the data. This is the default and most common mode because it has very little performance overhead on the primary server. The drawback is a small but non-zero risk of data loss. If the primary crashes before a committed transaction’s WAL record has been sent to the replica, that transaction will be lost upon failover.
  • Synchronous Replication: To eliminate the risk of data loss for committed transactions, you can configure synchronous replication. In this mode, the primary server will wait to confirm a transaction as committed until it receives acknowledgment from at least one (or a configured quorum of) synchronous replicas that they have received and persisted the WAL record. This guarantees that if the primary crashes, any transaction the application saw as “committed” will exist on the replica that takes over. The trade-off is higher transaction latency on the primary, as it must wait for the network round-trip to the replica.

Flow diagram comparing Asynchronous and Synchronous replication modes.

Flow diagram comparing Asynchronous and Synchronous replication modes.

Declarative Replication Management in CloudNativePG

CloudNativePG makes managing these complex replication modes simple and declarative.

  • Asynchronous replication is the default behavior. If you do nothing special in the Cluster manifest, all replicas will be configured as asynchronous hot standbys.
  • Synchronous replication is enabled and controlled through the .spec.postgresql.synchronous stanza (in versions 1.24+) or the now-deprecated minSyncReplicas and maxSyncReplicas fields. The modern approach allows for fine-grained control : — Quorum-Based (any): You can specify that a transaction must be replicated to ANY n number of standbys before being confirmed. This is ideal for high availability within a single cluster, as the operator dynamically manages which replicas are part of the quorum. — Priority-Based (first): You can define an ordered list of standbys. The primary will only wait for the highest-priority replicas available. This is useful for more complex DR scenarios, such as ensuring a transaction is committed to a replica in a different geographic region.

CloudNativePG also intelligently manages replication slots, which prevent the primary from deleting WAL files that a replica still needs, even if the replica is disconnected for a time. This is crucial for preventing replicas from desynchronizing and requiring a full re-clone.

Installation and Environment Setup on Rancher

This guide assumes a foundational environment consisting of a Rancher-managed Kubernetes cluster, with Longhorn installed for persistent storage and MinIO for S3-compatible object storage. While CloudNativePG can be installed via its Helm chart or OperatorHub, the most direct and transparent method is applying the official YAML manifest. This approach reinforces the Kubernetes-native interaction model and provides clarity on the resources being created.

Rancher itself acts as a powerful management and observability plane for the Kubernetes cluster, but the core interaction with CloudNativePG remains centered on kubectl and declarative YAML manifests. This is a testament to CloudNativePG's design; it integrates with the Kubernetes API, not a specific management UI, making it portable across any certified Kubernetes distribution.

Part 1: Install the CloudNativePG Operator

First, install the operator for version 1.26.0. It is critical to use the version-specific URL from the release-1.26 branch to ensure you are deploying the exact version discussed in this guide.

# Apply the manifest for CloudNativePG v1.26.0
kubectl apply --server-side -f \
  https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.26/releases/cnpg-1.26.0.yaml

This command creates the cnpg-system namespace and deploys the operator's controller manager. Verify the installation is complete and the pod is running:

# Check the rollout status of the operator deployment
kubectl rollout status deployment -n cnpg-system cnpg-controller-manager

Part 2: Install the kubectl cnpg Plugin

While not strictly required, the cnpg plugin for kubectl is an indispensable tool for managing and observing clusters. It simplifies common operations and provides rich, database-aware status outputs. Follow the official documentation to install the plugin for your operating system. For example, on Linux, you can download the Debian package for v1.26.0 and install it.

Part 3: Deploy a PostgreSQL Cluster

With the operator running, you can now deploy a PostgreSQL cluster. This is done by applying a Cluster manifest, which is a Custom Resource defined by CloudNativePG.

The following manifest creates a basic three-node cluster named cluster-example with 1Gi of storage.

The official Github Repo has many other sample Cluster configs https://github.com/cloudnative-pg/cloudnative-pg/tree/main/docs/src/samples

Further down in the article we will expand this cluster config with more options.

# cluster-example.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: cluster-example
spec:
  instances: 3
  storage:
    size: 1Gi

Deploy the cluster using kubectl:

kubectl apply -f cluster-example.yaml

After a few minutes, you can verify the status using the cnpg plugin:

kubectl cnpg status cluster-example

Part 4: Rancher Desktop Configuration

For users running this environment on Rancher Desktop, it’s important to be aware of a potential system limit that can affect performance. The cnpg-playground repository notes that the default open file limits may be insufficient. It is recommended to increase these limits by following the instructions in the Rancher Desktop guide, typically by modifying system control parameters like fs.inotify.max_user_watches and fs.inotify.max_user_instances.

Storage Strategy: Harmonizing CloudNativePG with Longhorn

The most critical decision when deploying a database on Kubernetes is the storage strategy. A misunderstanding of how application-level and storage-level replication interact can lead to poor performance and wasted resources.

CloudNativePG achieves high availability through PostgreSQL’s mature and robust native streaming replication. In a three-instance cluster, this means there are three complete, independent copies of the database, with changes streamed from the primary to the replicas. Meanwhile, distributed block storage solutions like Longhorn also provide data resilience by creating multiple replicas of each storage volume across different nodes.

If both layers are configured for replication (e.g., a 3-instance Postgres cluster where each instance’s volume is also replicated 3 times by Longhorn), the result is significant “write amplification.” Every write operation from the application would be written once by Postgres, streamed to two replicas, and then each of those three writes would be replicated three times by Longhorn, resulting in nine total disk writes. This is inefficient and unnecessary.

Visualizing the “write amplification” problem.

Visualizing the “write amplification” problem.

The established best practice is to delegate replication to the application layer (CloudNativePG) and configure the storage layer for single-replica, node-local persistence. This aligns perfectly with PostgreSQL’s shared-nothing architecture. The principle of “shared-nothing” extends beyond just avoiding shared disks; it means eliminating single points of failure at the storage host level. A single node failure should only ever impact a single PostgreSQL instance.

Longhorn’s dataLocality: "strict-local" setting is the key to enforcing this. It ensures that a volume's only replica is always stored on the same node where the pod using it is scheduled. When combined with pod anti-affinity rules (which CloudNativePG configures by default), this guarantees that a single host failure cannot take down more than one database instance and its corresponding data volume.

Real-World Example: Longhorn StorageClass for CloudNativePG

The following manifest creates a dedicated StorageClass for CloudNativePG that implements these best practices.

# longhorn-storageclass.yaml
# This StorageClass is optimized for use with CloudNativePG.
# It disables storage-level replication and ensures data is local to the pod.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: longhorn-cnpg-strict-local
# The provisioner must be Longhorn's driver.
provisioner: driver.longhorn.io
# allowVolumeExpansion is crucial for scaling database storage without downtime.
allowVolumeExpansion: true
# reclaimPolicy: Delete ensures that when a PVC is deleted, the underlying
# Longhorn volume is also removed, preventing orphaned storage.
reclaimPolicy: Delete
parameters:
  # This is the most important setting. We rely on CloudNativePG for replication,
  # so we only need one copy at the storage layer to avoid write amplification.
  numberOfReplicas: "1"
  # dataLocality: strict-local guarantees that the volume data will be stored
  # on the same node as the pod that uses it. This is essential for performance
  # and for aligning with a true shared-nothing architecture.
  dataLocality: "strict-local"
  # A longer timeout for stale replicas is suitable for database workloads.
  staleReplicaTimeout: "2880" # 48 hours in minutes
  # Default filesystem.
  fsType: "ext4"

Apply this manifest to your cluster to make the StorageClass available for your PostgreSQL deployments:

kubectl apply -f longhorn-storageclass.yaml

Deploying a Highly Available PostgreSQL Cluster

With the operator installed and the storage strategy defined, you can now deploy a production-ready, highly available PostgreSQL cluster. The following Cluster manifest defines a three-node cluster that leverages our custom Longhorn StorageClass and follows database administration best practices.

Architecture of a 3-node HA cluster.

Architecture of a 3-node HA cluster.

Real-World Example: HA PostgreSQL Cluster Manifest

This manifest creates a cluster named pg-prod-cluster with three instances. It separates the main data directory (PGDATA) from the Write-Ahead Log (WAL) onto distinct volumes, a standard practice for optimizing I/O performance and improving reliability. Both volumes will use the longhorn-cnpg-strict-local storage class.

# pg-prod-cluster.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pg-prod-cluster
  namespace: default
spec:
  # Defines the number of instances. 3 is the recommended minimum for a robust HA setup.
  instances: 3
# Always specify a concrete image tag, never 'latest', for predictable deployments.
  # This uses PostgreSQL 16, the default for CNPG 1.26.
  imageName: ghcr.io/cloudnative-pg/postgresql:16.2
  # 'unsupervised' enables fully automatic failover. If the primary goes down,
  # the operator will promote a replica without human intervention.
  primaryUpdateStrategy: unsupervised
  # Main storage configuration for the PGDATA directory.
  storage:
    storageClass: longhorn-cnpg-strict-local
    size: 50Gi
  # It is a best practice to separate WAL files onto their own volume.
  # This isolates I/O patterns (sequential writes for WALs, random R/W for data)
  # and prevents a full data volume from halting WAL archiving.
  walStorage:
    storageClass: longhorn-cnpg-strict-local
    size: 20Gi
  # Declaratively manage PostgreSQL configuration parameters.
  postgresql:
    parameters:
      shared_buffers: "1GB"
      max_connections: "200"
      log_statement: "ddl"
  # Enable the built-in Prometheus exporter for monitoring.
  monitoring:
    enablePodMonitor: true

Deploy the cluster using kubectl:

kubectl apply -f pg-prod-cluster.yaml

After a few minutes, you can verify the status.

  • Check the Cluster resource: This gives a high-level overview. kubectl get cluster pg-prod-cluster # EXPECTED OUTPUT: # NAME AGE INSTANCES READY STATUS PRIMARY # pg-prod-cluster 2m 3 3 Cluster in healthy state pg-prod-cluster-1
  • Check the Pods: Verify that three pods have been created and are running. The -o wide flag shows which Kubernetes node each pod is scheduled on, confirming they are distributed. kubectl get pods -l cnpg.io/cluster=pg-prod-cluster -o wide
  • Use the cnpg plugin: The status command provides a rich, detailed view of the cluster's health, including replication status and backup information. kubectl cnpg status pg-prod-cluster

The CNPG-I Plugin Architecture

A foundational concept in modern CloudNativePG is the CNPG-I (CloudNativePG Interface). This is a standardized, gRPC-based protocol that allows external, third-party plugins to integrate with and extend the operator’s core functionality.

This modular architecture was inspired by Kubernetes’ own Container Storage Interface (CSI) and was created to make the core operator leaner and more focused. By defining a stable API, CNPG-I empowers the community to develop independent plugins for a wide range of use cases without needing to modify the operator’s source code.

Potential use cases for plugins include:

  • Backup and Recovery (e.g., the Barman Cloud Plugin)
  • WAL Management
  • Metrics Exporting
  • Authentication and Authorization
  • And more…

This strategic pivot makes CloudNativePG a more flexible and future-proof framework, fostering a rich ecosystem of tools around it.

Robust Backup and Recovery with WAL Archiving

[embed]

A cornerstone of any production database strategy is a reliable backup and recovery plan. CloudNativePG provides a comprehensive framework built on two key PostgreSQL concepts: physical base backups and Write-Ahead Log (WAL) archiving.

  • Physical Base Backup: This is a complete, file-level copy of the entire PostgreSQL data directory (PGDATA). It serves as the starting point for any recovery operation.
  • WAL Archiving: The Write-Ahead Log is a continuous stream of records detailing every change made to the database. By archiving these WAL files to a separate, durable location (like an object store), you create a continuous record of transactions. This is the magic that enables Point-in-Time Recovery (PITR), allowing you to restore a database not just to the time of a backup, but to any specific moment covered by your WAL archive.

CloudNativePG sets a default archive_timeout of 5 minutes, ensuring that even on low-traffic systems, WAL files are regularly archived. This provides a deterministic Recovery Point Objective (RPO) of 5 minutes or less for disaster recovery scenarios.

The Shift to the Barman Cloud Plugin

With version 1.26, CloudNativePG has formally begun deprecating its built-in backup system in favor of the new, extensible plugin architecture powered by CNPG-I. The primary and officially supported backup tool is now the

Barman Cloud Plugin. This strategic move transforms CloudNativePG into a “backup/recovery-agnostic framework,” allowing users to choose from a growing ecosystem of backup solutions in the future. While the old in-tree barmanObjectStore configuration is still functional in v1.26, it will be removed in v1.28. All new deployments should use the plugin.

Real-World Backups with the Barman Plugin and MinIO

This section provides a complete, step-by-step guide to configuring backups for our PostgreSQL cluster using the new Barman Cloud Plugin and a MinIO object store.

Step 1: Install the Barman Cloud Plugin A key prerequisite for using the Barman Cloud Plugin is the presence of cert-manager in the cluster. The plugin uses it to enable secure TLS communication with the CloudNativePG operator.

Once cert-manager is running, you can install the plugin by applying its manifest.

# Install the Barman Cloud Plugin manifest
kubectl apply -f https://github.com/cloudnative-pg/plugin-barman-cloud/releases/download/v0.5.0/manifest.yaml

Step 2: Deploy MinIO and Create Credentials Next, we need an S3-compatible object store. The following manifests deploy a simple MinIO instance and a service to expose it within the cluster.

# minio-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: minio
spec:
  replicas: 1
  selector:
    matchLabels:
      app: minio
  template:
    metadata:
      labels:
        app: minio
    spec:
      containers:
      - name: minio
        image: minio/minio:RELEASE.2023-03-20T20-16-18Z
        args:
        - server
        - /data
        env:
        - name: MINIO_ROOT_USER
          value: "minioadmin"
        - name: MINIO_ROOT_PASSWORD
          value: "minioadmin"
        ports:
        - containerPort: 9000
---
apiVersion: v1
kind: Service
metadata:
  name: minio-service
spec:
  selector:
    app: minio
  ports:
    - protocol: TCP
      port: 9000
      targetPort: 9000

Create a Kubernetes secret to hold the MinIO credentials:

kubectl create secret generic minio-credentials \
  --from-literal=ACCESS_KEY_ID='minioadmin' \
  --from-literal=SECRET_ACCESS_KEY='minioadmin'

Step 3: Define the ObjectStore Configuration Instead of configuring the object store in the Cluster manifest, we now create a dedicated ObjectStore custom resource.

Architecture of the Barman Cloud Plugin for backups.

Architecture of the Barman Cloud Plugin for backups.

# minio-objectstore.yaml
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
  name: minio-backup-config
spec:
  destinationPath: s3://postgres-backups/
  s3Credentials:
    accessKeyId:
      name: minio-credentials
      key: ACCESS_KEY_ID
    secretAccessKey:
      name: minio-credentials
      key: SECRET_ACCESS_KEY
  endpointURL: http://minio-service.default.svc:9000

Apply the manifests:

kubectl apply -f minio-deployment.yaml
kubectl apply -f minio-objectstore.yaml

Step 4: Update the Cluster to Use the Plugin Now, we update our cluster-example to use this new backup configuration by adding a plugins section.

# cluster-example-with-backup.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: cluster-example
spec:
  instances: 3
  storage:
    size: 1Gi

  plugins:
    - name: barman-cloud
      parameters:
        objectStore: minio-backup-config
        isWalArchiver: true

Apply the updated cluster definition:

kubectl apply -f cluster-example-with-backup.yaml

Step 5: Schedule Regular Backups Finally, create a ScheduledBackup resource to define your backup policy. This example configures a daily backup.

# daily-backup-schedule.yaml
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
  name: daily-backup
spec:
  schedule: "0 0 0 * * *"
  method: plugin
  cluster:
    name: cluster-example
  pluginConfiguration:
    name: barman-cloud

Apply the schedule:

kubectl apply -f daily-backup-schedule.yaml

Comprehensive Recovery and Restoration

CloudNativePG provides robust recovery capabilities, leveraging PostgreSQL’s powerful Point-in-Time Recovery (PITR) features. It’s important to understand that recovery in CloudNativePG is not an in-place operation on an existing cluster. Instead, recovery is a method to bootstrap an entirely new cluster from a physical backup.

There are two primary methods for recovery:

  • Recovery from an Object Store: This is the recommended approach, using backups created by the Barman Cloud Plugin (or the deprecated native integration). You define the location of your backups in an externalClusters stanza, and the new cluster will bootstrap from the specified base backup and replay WAL files from the archive.
  • Recovery from Volume Snapshots: If your storage class supports volume snapshots, you can create a new cluster directly from a snapshot of an existing cluster’s PVC. This can be significantly faster for large databases. If a WAL archive is also available, you can perform a PITR from a volume snapshot.

High-level overview of the recovery process in CloudNativePG.

High-level overview of the recovery process in CloudNativePG.

Point-in-Time Recovery (PITR)

PITR allows you to restore a cluster to a specific moment, such as right before an accidental DROP TABLE command. This requires a complete WAL archive. To perform a PITR, you specify a recoveryTarget in the bootstrap section of the new cluster's manifest. You can define the target by a specific timestamp, a transaction ID, or other recovery targets supported by PostgreSQL.

The operator intelligently selects the correct base backup to start from. If you specify a targetTime, it will find the latest backup completed before that time and replay WALs up to the specified moment.

Seamless PostgreSQL Upgrades

CloudNativePG simplifies the often-daunting task of upgrading PostgreSQL, handling both minor and major versions declaratively.

Minor Version Upgrades

Minor version upgrades (e.g., from 16.1 to 16.2) are handled via a standard rolling update process. When you update the imageName in the Cluster manifest to a new minor version, the operator gracefully updates each instance one by one, culminating in an automated and transparent switchover to a newly updated replica. This process ensures minimal downtime.

Declarative In-Place Major Upgrades

Version 1.26 introduces a game-changing feature: declarative offline in-place major upgrades. Historically, major upgrades (e.g., from PostgreSQL 15 to 16) were complex because of on-disk format changes, often requiring a full dump and restore or complex logical replication setups.

Now, you can trigger a major upgrade simply by changing the imageName in your Cluster manifest to a new major version. CloudNativePG automates the entire pg_upgrade process:

  • The operator safely shuts down the cluster.
  • An upgrade job is initiated, which runs pg_upgrade using the --link option for space efficiency.
  • Upon successful completion, the upgraded data directory replaces the old one.
  • The cluster is brought back online with the new PostgreSQL version.

This feature dramatically simplifies what was once a high-stakes manual operation, making major upgrades as easy as minor ones from a user’s perspective. For scenarios requiring zero downtime, CloudNativePG also supports online major upgrades using PostgreSQL’s native logical replication capabilities.

Achieving Global Resilience: Automated Failover and Distributed Topologies

CloudNativePG provides robust mechanisms for both high availability (HA) within a single cluster and disaster recovery (DR) across multiple clusters.

Automated Failover (Intra-Cluster)

Within a single Kubernetes cluster, CloudNativePG provides fully automated failover to ensure high availability. The process is managed by the operator and the instance manager running in each pod:

  • Failure Detection: The operator continuously monitors the health of the primary instance via Kubernetes readiness probes. If the primary pod fails, is deleted, or the PostgreSQL process within it becomes unresponsive, the probe will fail.
  • Failover Trigger: After a configurable delay (failoverDelay), the operator initiates the failover process. It marks the old primary for shutdown to ensure it cannot accept new writes.
  • Leader Election: The operator orchestrates a leader election among the available replicas. The most up-to-date replica is chosen to become the new primary.
  • Promotion: The selected replica is promoted. The -rw service is instantly updated to point to the new primary, transparently redirecting application traffic.
  • Self-Healing: The old primary pod, upon restarting, detects it is no longer the primary and automatically reconfigures itself as a replica of the new primary, using pg_rewind to efficiently resynchronize.

Automated failover sequence within a CloudNativePG cluster.

Automated failover sequence within a CloudNativePG cluster.

Distributed Topologies with Replica Clusters (Inter-Cluster)

For true disaster recovery, CloudNativePG supports creating distributed PostgreSQL topologies using its Replica Cluster feature. This enables multi-cluster deployments across different data centers or cloud regions, making it ideal for private, public, hybrid, and multi-cloud strategies.

A Replica Cluster is an independent Cluster resource that continuously recovers from a primary cluster, acting as a read-only hot standby. Replication can be configured using either direct streaming replication over the network or, more commonly for DR, by fetching WAL files from a shared object store. This provides a robust defense against the failure of an entire Kubernetes cluster or region.

Important Note on Cross-Cluster Failover: While CloudNativePG provides all the declarative primitives to build and manage these distributed topologies, it cannot perform an automated cross-cluster failover. The operator’s authority is limited to the single Kubernetes cluster it manages. The decision to promote a replica cluster in a DR site must be performed manually by an operator or delegated to a higher-level, multi-cluster-aware orchestration tool.

Geo-distributed disaster recovery architecture using Replica Clusters and a shared object store.

Geo-distributed disaster recovery architecture using Replica Clusters and a shared object store.

Real-World Example: Two-Region DR Setup

Let’s configure a DR setup with our cluster-example in a US region as the primary and a new replica cluster in an EU region. Both will use the same MinIO bucket for WAL archiving and backups.

1. Primary Cluster Manifest (cluster-primary-us.yaml)

This is our existing cluster, now updated with an externalClusters section to be aware of its replica.

# cluster-primary-us.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: cluster-primary-us
spec:
  instances: 3
  storage:
    size: 1Gi
  plugins:
    - name: barman-cloud
      parameters:
        objectStore: minio-backup-config
        isWalArchiver: true
  replica:
    primary: cluster-primary-us
  externalClusters:
    - name: cluster-replica-eu

2. Replica Cluster Manifest (cluster-replica-eu.yaml)

This manifest defines the new read-only cluster in the EU region.

# cluster-replica-eu.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: cluster-replica-eu
spec:
  instances: 3
  storage:
    size: 1Gi
  bootstrap:
    recovery:
      source: cluster-primary-us
  externalClusters:
    - name: cluster-primary-us
      barmanObjectStore:
        destinationPath: s3://postgres-backups/
        serverName: cluster-primary-us
        s3Credentials:
          accessKeyId:
            name: minio-credentials
            key: ACCESS_KEY_ID
          secretAccessKey:
            name: minio-credentials
            key: SECRET_ACCESS_KEY
        endpointURL: http://minio-service.default.svc:9000
  replica:
    enabled: true
    primary: cluster-primary-us

After deploying both manifests to their respective Kubernetes clusters, the cluster-replica-eu will bootstrap itself from the latest backup of cluster-primary-us and then begin continuously replaying new WAL files from the MinIO bucket, staying closely in sync.

To perform a controlled switchover, an operator would edit both manifests, changing the replica.primary field in each to cluster-replica-eu. Applying these changes would trigger CloudNativePG to gracefully demote the US cluster to a replica and promote the EU cluster to be the new primary.

Managing Connections with the PgBouncer Pooler

For applications with a high number of concurrent, short-lived connections, directly connecting to PostgreSQL can be inefficient. Each new connection to PostgreSQL is a resource-intensive process. To solve this, CloudNativePG provides native support for PgBouncer, a lightweight connection pooler for PostgreSQL, through a dedicated Pooler custom resource.

The Pooler resource deploys a set of PgBouncer pods that sit between your application and the PostgreSQL cluster. Applications connect to the PgBouncer service, which maintains a pool of connections to the actual database. This dramatically reduces the overhead of connection management on the PostgreSQL primary, improving performance and scalability.

Architecture of the PgBouncer Pooler resource.

Architecture of the PgBouncer Pooler resource.

Real-World Example: Deploying a PgBouncer Pooler

The following manifest creates a Pooler named pg-prod-pooler for our cluster-example. It will deploy three PgBouncer replicas and expose them through a service.

# pgbouncer-pooler.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Pooler
metadata:
  name: pg-prod-pooler
spec:
  cluster:
    name: cluster-example
  type: rw
  instances: 3
  pgbouncer:
    poolMode: session
    parameters:
      max_client_conn: "1000"
      default_pool_size: "10"

Deploy the pooler with kubectl:

kubectl apply -f pgbouncer-pooler.yaml

Your applications can now connect to the pg-prod-pooler service instead of the cluster-example-rw service to take advantage of connection pooling. The pooler also comes with a built-in Prometheus exporter on port 9127 for monitoring its performance.

Enhancing Security with LDAP Authentication

For enterprise environments, centralizing user authentication is a critical security requirement. CloudNativePG supports integrating PostgreSQL with an LDAP (Lightweight Directory Access Protocol) server for client authentication. This is configured declaratively within the Cluster resource.

When LDAP is configured, CloudNativePG modifies the pg_hba.conf file to include the necessary rules for LDAP authentication. It's important to ensure that the LDAP rules do not conflict with other rules, especially broad ones like host all all, which could take precedence and bypass LDAP.

Note on PgBouncer and LDAP: Currently, authenticating via PgBouncer with LDAP is not directly supported by PgBouncer itself. However, a pull request (#731) adding native LDAP support to PgBouncer has been merged, which may enable this functionality in future versions of PgBouncer and, by extension, through CloudNativePG’s Pooler resource.

LDAP authentication flow with CloudNativePG.

LDAP authentication flow with CloudNativePG.

Real-World Example: Configuring LDAP

The following snippet shows how to configure the ldap section within a Cluster manifest. This requires a pre-existing secret (ldap-bind-password-secret) containing the password for the bindDN user.

# cluster-example-with-ldap.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: cluster-example
spec:
  #... other cluster settings...
  postgresql:
    #... other postgresql parameters...
    pg_hba:
      - hostssl all all 0.0.0.0/0 ldap ldapserver=ldap.example.com ldapscheme=ldaps ldapbasedn="ou=users,dc=example,dc=com"
    ldap:
      server: 'ldap.example.com'
      scheme: 'ldaps'
      port: 636
      bindSearchAuth:
        baseDN: 'ou=users,dc=example,dc=com'
        bindDN: 'cn=readonly,ou=system,dc=example,dc=com'
        bindPassword:
          name: ldap-bind-password-secret
          key: password
        searchFilter: '(uid=%u)'

By applying this configuration, CloudNativePG will manage the integration, allowing PostgreSQL to authenticate users against your central LDAP directory, streamlining user management and enhancing security.

Comprehensive Monitoring and Logging

CloudNativePG is designed for observability, providing deep insights into both the operator and the PostgreSQL clusters it manages.

Monitoring with Prometheus

Grafana Dashboard

Grafana Dashboard

The Grafana Dashboard for CNPG can be downloaded from https://github.com/cloudnative-pg/grafana-dashboards/blob/main/charts/cluster/grafana-dashboard.json

For each PostgreSQL instance, CloudNativePG includes a built-in Prometheus exporter that exposes metrics on port 9187. You can enable Prometheus to scrape these metrics by simply setting

.spec.monitoring.enablePodMonitor: true in your Cluster manifest. This will automatically create a PodMonitor resource for the cluster.

The exporter provides a default set of metrics covering:

  • WAL file statistics (count, total size)
  • Backup and archiving status
  • Replication status, including synchronous replica counts
  • Cluster state flags (e.g., replica mode, fencing)

Additionally, CloudNativePG offers a powerful system for defining user-defined metrics. You can write your own SQL queries in a ConfigMap or Secret and reference it in the Cluster spec. The operator will then execute these queries and expose the results as Prometheus metrics, allowing you to monitor application-specific data or any other aspect of PostgreSQL's internal state.

CloudNativePG monitoring architecture with Prometheus and Grafana.

CloudNativePG monitoring architecture with Prometheus and Grafana.

Centralized Logging

CloudNativePG adopts a cloud-native approach to logging. All logs from the operator and the PostgreSQL instances are written in a structured JSON format directly to standard output. This design choice avoids writing to persistent files within the container and facilitates seamless integration with standard Kubernetes logging pipelines like Fluentd, Loki, or Elastic.

Each log entry is enriched with metadata, including a logger field that identifies the source (e.g., postgres, pgaudit, barman-cloud-wal-archive), making it easy to filter and analyze logs from different components of the system. The operator also provides native support for the

pgaudit extension, automatically managing its configuration and parsing its output into the JSON log stream.

The kubectl cnpg Plugin

To streamline cluster management, CloudNativePG provides a powerful plugin for kubectl called cnpg. This CLI tool enhances and simplifies the daily operations of DBAs and developers working with CloudNativePG.

Key commands include:

  • kubectl cnpg status <cluster>: Provides a rich, detailed overview of a cluster's health, including replication status, backup information, and instance states.
  • kubectl cnpg promote <cluster> <instance>: Manually triggers a switchover, promoting a specified replica to become the new primary.
  • kubectl cnpg backup <cluster>: Initiates an on-demand backup of a cluster.
  • kubectl cnpg restart <cluster> [instance]: Orchestrates a rolling restart of an entire cluster or restarts a single instance.
  • kubectl cnpg psql <cluster>: Quickly opens a psql shell connected to the primary instance of the cluster, ideal for debugging or manual queries.
  • kubectl cnpg report <cluster|operator>: Bundles logs and resource definitions into a ZIP file for troubleshooting.

This plugin is an indispensable companion for anyone managing PostgreSQL clusters with CloudNativePG, turning complex operations into simple, repeatable commands.

The Competitive Landscape: Why CloudNativePG Stands Out

The Kubernetes ecosystem offers several mature PostgreSQL operators, each with its own philosophy and strengths. Understanding these differences is key to making an informed decision. The main competitors include the Crunchy Data Postgres Operator (PGO), the Zalando Postgres Operator, and StackGres.

[embed]

CloudNativePG’s primary distinction lies in its conscious decision to build directly on Kubernetes primitives rather than relying on intermediate tools like Patroni or abstractions like StatefulSets. This results in a leaner, more deeply integrated, and arguably more “cloud-native” solution that leverages the full power of the Kubernetes control plane for its operations. Its recent pivot to a plugin-based architecture further solidifies its position as a modern, extensible platform for the future.

Conclusion: The Premier Choice for Cloud-Native PostgreSQL

CloudNativePG has firmly established itself as a premier, production-ready solution for running PostgreSQL on Kubernetes. Its architecture, which is deeply rooted in Kubernetes-native principles, provides a level of automation and reliability that simplifies database operations and empowers developers and DBAs alike. By eschewing complex external dependencies and managing the cluster lifecycle directly through the Kubernetes API, it offers a clean, robust, and efficient platform.

The capabilities for high availability within a single cluster are comprehensive and battle-tested, relying on PostgreSQL’s own proven streaming replication and automated failover. For disaster recovery, the Replica Cluster feature provides the declarative building blocks needed to construct resilient, globally distributed topologies, giving organizations the tools to meet stringent RTO and RPO objectives. The operator's robust recovery options, integrated monitoring and logging, and advanced features like connection pooling with PgBouncer and LDAP authentication further solidify its enterprise-readiness.

With the release of version 1.26.0, CloudNativePG has taken a significant step forward. The strategic pivot to the CNPG-I plugin interface is not merely a technical change but a foundational move towards creating a more open, flexible, and future-proof ecosystem. Combined with game-changing features like declarative major version upgrades, CloudNativePG demonstrates a clear vision and a commitment to addressing the most complex challenges of running stateful workloads in the cloud. For any organization serious about leveraging the power of Kubernetes for its data tier, CloudNativePG represents a compelling, powerful, and community-driven choice.


메타데이터
post_id
59a3ea1fee63
slug
postgresql-on-kubernetes-a-deep-dive-with-cloudnativepg-cnpg-59a3ea1fee63
url
https://medium.com/@smayya/postgresql-on-kubernetes-a-deep-dive-with-cloudnativepg-cnpg-59a3ea1fee63
canonical_url
https://medium.com/@smayya/postgresql-on-kubernetes-a-deep-dive-with-cloudnativepg-cnpg-59a3ea1fee63
author_url
https://medium.com/@smayya
status
ok
fetched_at
2026-07-19 04:24:24