Skip to content

CloudNative PostgreSQL

Example Cluster

Create the S3 Bucket

Create a Bucket in an Exoscale zone that is NOT at-vie-1 by following the instructions:

---
apiVersion: appcat.vshn.io/v1
kind: ObjectBucket
metadata:
   name: <project>-s3-bucket-pgbackup
   namespace: aris-<project>
spec:
   parameters:
     bucketName: <project>-s3-bucket-pgbackup
     region: at-vie-2
   writeConnectionSecretToRef:
     name: <project>-s3-bucket-pgbackup

This creates a bucket in Exoscale region at-vie-2 and writes the connection information to a secret, that can then be used by PostgreSQL.

Now that you have a S3 bucket, it is time to configure your database and database backup. How this is done depends on when you create your database. The way CloudNative PostgreSQL handles backups and restores has changed in recent releases and the old way is now considered 'legacy' but has not been removed yet. Although a date for the deprecation of the old backup has not been announced as of writing, we strongly recommend you use the new method for all newly created clusters.

About security

This method creates an S3 bucket using your own credentials, which is ok for development and staging, but not for production. An attacker who has your credentials can also delete the bucket, destroying all your backups. We can create a bucket for you which you do not have permissions to delete. Additionally, we will enable object versioning on the bucket for 180 days, so your data cannot be deleted even if an attacker gains access to the bucket credentials. Regardless of which method you use, you can follow the next section with your credentials.

Disaster recovery

Your backup is still hosted on ARISnet's underlying platform, Exoscale, which is why your backup should at the very least be stored in a different region. Depending on how sensitive your data is, you should consider creating an off-site backup that is independent of ARISnet.

Create the ObjectStore configuration for Backup

Note

The following text describes the new method of database creation and backup. The old method can be found here for reference: postgresql-legacy.md.

Prepare the ObjectStore configuration object that holds the information necessary for PostgreSQL to perform regular scheduled backups as well as backups of the write ahead logs (WALs):

---
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
  name: backup-store
  namespace: aris-<project>
spec:
  # configuration for s3 storage
  # credentials are stored in an external secret
  configuration:
    destinationPath: s3://<project>-s3-bucket-pgbackup/
    endpointURL: https://sos-at-vie-2.exo.io
    s3Credentials:
      accessKeyId:
        name: <project>-s3-bucket-pgbackup
        key: AWS_ACCESS_KEY_ID
      secretAccessKey:
        name: <project>-s3-bucket-pgbackup
        key: AWS_SECRET_ACCESS_KEY
    wal:
      compression: gzip
  # do not reserve too much, but give the backup some flexibility
  instanceSidecarConfiguration:
    resources:
      requests:
        cpu: "10m"
        memory: "128Mi"
      limits:
        cpu: "1"
        memory: "256Mi"
  retentionPolicy: "30d"

Create the Database

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  namespace: aris-<project>
  name: <project>-db

spec:
  description: "Postgres cluster for <project>"
  imageName: ghcr.io/cloudnative-pg/postgresql:17.7-standard-trixie
  instances: 3
  primaryUpdateStrategy: unsupervised
  startDelay: 300
  stopDelay: 300
  monitoring:
    enablePodMonitor: true

  # activate backup plugin and use it for WAL archiving
  plugins:
    - name: barman-cloud.cloudnative-pg.io
      isWALArchiver: true
      parameters:
        barmanObjectName: backup-store

  # Persistent storage configuration: start small, you can always grow
  # Use at least the amount of storage you reserved for max_slot_wal_keep_size
  # plus your database size
  storage:
    storageClass: rbd-storagepool-cluster
    size: 2Gi

  postgresql:
    parameters:
      max_slot_wal_keep_size: "1GB"

  # this creates an empty database with random credentials for the owner
  # if you want to or have to set the credentials have a look at the docs
  bootstrap:
    initdb:
       database: xxx
       owner: xxx

  # these are the resources for a very small database that is allowed
  # to use more resources during startup or for peaks
  resources:
    requests:
      memory: "150Mi"
      cpu: "50m"  # should be 2
    limits:
      memory: "1Gi"

  # always place the replicas on different nodes!
  affinity:
    enablePodAntiAffinity: true
    topologyKey: kubernetes.io/hostname
    podAntiAffinityType: required

This YAML when applied will create 3 PostgreSQL Pods with their respective storage volumes. One of the Pods will be the primary database node, the others are followers.

Services for read-write and readonly connections are automatically created by the operator.

But first create a base backup while your database is still small.

Backup the database

The YAML from above will create a database and backups of the write ahead logfiles (WAL). To restore the database from this backup you will need every WAL ever written since the last base backup.

We therefore recommend you create a backup schedule, e.g. backup your database by creating a ScheduledBackup custom resource:

apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
  name: <project>-db-scheduledbackup
  namespace: aris-<project>
spec:
  backupOwnerReference: self
  cluster:
    name: <project>-db
  schedule: "0 30 10 * * *"
  method: plugin
  pluginConfiguration:
    name: barman-cloud.cloudnative-pg.io
  target: primary

This will create a full backup of your database every day at 10:30 a.m. UTC, i.e. at 11:30 CET/12:30 CEST. The ScheduledBackup will use your S3 bucket to save the backups. It will keep 30 full backups. the Objectstores retentionPolicy and delete the oldest when that limit is reached.

Use the database

The credentials are placed in a secret called xxx-app where xxx is the name of your database. The secret contains everything you need to connect to your database in several formats, ready to use. If one of the formats fits your use case, just mount the secret into your deployment and you are ready to go.

Restore or clone the database

From a technical standpoint a database restore and a database clone are basically the same. The name of a cloned database has to be different than the original name (you can't call your database <project>-db if that database already exists), but that's about it.

When creating a database clone, a new database is created by crafting a variant of the YAMLs that were used for the creation of the original database.

For backups of the clone you'll need a new S3 bucket:

---
apiVersion: appcat.vshn.io/v1
kind: ObjectBucket
metadata:
   name: <project>-clone-s3-bucket-pgbackup
   namespace: aris-<project>
spec:
   parameters:
     bucketName: <project>-clone-s3-bucket-pgbackup
     region: at-vie-2
   writeConnectionSecretToRef:
     name: <project>-clone-s3-bucket-pgbackup

This Bucket is then referenced in an ObjectStore like for the original database:

---
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
  name: backup-clone-store
  namespace: aris-<project>
spec:
  # configuration for s3 storage
  # credentials are stored in an external secret
  configuration:
    destinationPath: s3://<project>-s3-bucket-pgbackup/
    endpointURL: https://sos-at-vie-2.exo.io
    s3Credentials:
      accessKeyId:
        name: <project>-clone-s3-bucket-pgbackup
        key: AWS_ACCESS_KEY_ID
      secretAccessKey:
        name: <project>-clone-s3-bucket-pgbackup
        key: AWS_SECRET_ACCESS_KEY
    wal:
      compression: gzip
  # do not reserve too much, but give the backup some flexibility
  instanceSidecarConfiguration:
    resources:
      requests:
        cpu: "10m"
        memory: "128Mi"
      limits:
        cpu: "1"
        memory: "256Mi"
  retentionPolicy: "30d"

Then create a database that uses the original ObjectStore as the data source:

Clone the Database

The database creation stanza is the same as the one for the original database, but instead of spec.bootstrap.initdb, you have to specify spec.bootstrap.recovery and spec.externalClusters. The barmanObjectName referenced in spec.externalClusters has to be the ObjectStore that is used by the original database, not the one just created!

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  namespace: aris-<project>
  name: <project>-clone-db

spec:
[...]
  # this creates a cloned database
  # the source is an "external cluster" that references the old database 
  bootstrap:
    recovery:
      source: clone-source
  externalClusters:
  - name: clone-source
    plugin:
      name: barman-cloud.cloudnative-pg.io
      parameters:
        barmanObjectName: backup-store
        serverName: <project>-db
[...]

After the Cluster is created the operator starts recovering the database. It is ready when all 3 database nodes are ready. Should you want to keep your database make sure to create a backup schedule.

The procedure outlined so far creates a clone of the database that contains whatever the state of the original database is at the time of the clone.

Point in time restore

The procedure for creating a point in time restore is the same as for creating a database clone. First create a Bucket, then an ObjectStore and finally the new Cluster:

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  namespace: aris-<project>
  name: <project>-clone-db

spec:
[...]
  # this creates a cloned database
  # the source is an "external cluster" that references the old database 
  bootstrap:
    recovery:
      source: clone-source
      recoveryTarget:
        # Time base target for the recovery
        targetTime: "2026-10-10 10:00:00"
  externalClusters:
  - name: clone-source
    plugin:
      name: barman-cloud.cloudnative-pg.io
      parameters:
        barmanObjectName: backup-store
        serverName: <project>-db
[...]

This will create a new database that contains the database as of 2026-10-10 10:00:00 UTC. You can also specify the timezone for the recovery point. If a timezone is missing the targetTime is interpreted as UTC.