Kubernetes Veeam
Jakob Jozelj  

Veeam Kasten PostgresSQL logical backup and restore definitive guide

Link to the Veeam Kasten guide that this writing was Inspired by Logical PostgreSQL Backup

Useful video on the topic also from Veeam Kasten engineer on YouTube

  • In this guide we will be installing PostgreSQL instance into postgresql-k10-test namespace which we will be backing up later.
  • Veeam Kasten is deployed into kasten-io namespace.
  • For backup repository, (Profiles / Locations) we already have set up Cohesity S3 appliance.

Enviroment

I was using VMware Tanzu distribution of Kubernetes, Veeam Kasten 8.0.13 and PostgreSQL version 17.5 (Chart version 18.1.0)

Create namespace where our PostgreSQL will live

  1. Create namespace
jjozelj@jjozelj:~$ kubectl create namespace postgresql-k10-test
  1. Label the namespace
jjozelj@jjozelj:~$ kubectl label namespace postgresql-k10-test pod-security.kubernetes.io/enforce=baseline pod-security.kubernetes.io/enforce-version=latest

Install PostgresSQL

Install PostgreSQL server from a Bitnami Helm chart, we will be backing up this instance with Veeam Kasten and Kanister.

1. Add bitnami helm repository (If not done already)

jjozelj@jjozelj:~$ helm repo add bitnami https://charts.bitnami.com/bitnami
"bitnami" has been added to your repositories

2. Install PostgreSQL into the correct namespace

jjozelj@jjozelj:~$ helm install --namespace postgresql-k10-test postgres bitnami/postgresql --set global.security.allowInsecureImages=true --set volumePermissions.image.repository=bitnamilegacy/os-shell  --set image.repository=bitnamilegacy/postgresql
level=WARN msg="unable to find exact version; falling back to closest available version" chart=postgresql requested="" selected=18.1.10
NAME: postgres
LAST DEPLOYED: Thu Nov 20 11:00:12 2025
NAMESPACE: postgresql-k10-test
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
TEST SUITE: None
NOTES:
CHART NAME: postgresql
CHART VERSION: 18.1.10
APP VERSION: 18.1.0

⚠ WARNING: Since August 28th, 2025, only a limited subset of images/charts are available for free.
Subscribe to Bitnami Secure Images to receive continued support and security updates.
More info at https://bitnami.com and https://github.com/bitnami/containers/issues/83267

** Please be patient while the chart is being deployed **

PostgreSQL can be accessed via port 5432 on the following DNS names from within your cluster:

postgres-postgresql.postgresql-k10-test.svc.cluster.local - Read/Write connection

To get the password for "postgres" run:

export POSTGRES_PASSWORD=$(kubectl get secret --namespace postgresql-k10-test postgres-postgresql -o jsonpath="{.data.postgres-password}" | base64 -d)

To connect to your database run the following command:

kubectl run postgres-postgresql-client --rm --tty -i --restart='Never' --namespace postgresql-k10-test --image registry-1.docker.io/bitnamilegacy/postgresql:latest --env="PGPASSWORD=$POSTGRES_PASSWORD" \
--command -- psql --host postgres-postgresql -U postgres -d postgres -p 5432

> NOTE: If you access the container using bash, make sure that you execute "/opt/bitnami/scripts/postgresql/entrypoint.sh /bin/bash" in order to avoid the error "psql: local user with ID 1001} does not exist"

To connect to your database from outside the cluster execute the following commands:

kubectl port-forward --namespace postgresql-k10-test svc/postgres-postgresql 5432:5432 &
PGPASSWORD="$POSTGRES_PASSWORD" psql --host 127.0.0.1 -U postgres -d postgres -p 5432
.
.
.

3. Get the password from the secret

Make sure that the name of the namespace is correct. We will use that secret to login to the PostgreSQL and check the version and later to see if the restore finished successfully.

jjozelj@jjozelj:~$ kubectl get secret --namespace postgresql-k10-test postgres-postgresql -o jsonpath="{.data.postgres-password}" | base64 -d
dPnUZX2nZY
jjozelj@jjozelj:~$

4. Exec into the pod

jjozelj@jjozelj:~$ kubectl exec -it postgres-postgresql-0 -n postgresql-k10-test -- sh

5. Log in to the PostgreSQL, list the databases and get the version

$ psql -U postgres
Password for user postgres:
psql (17.5)
Type "help" for help.

postgres=# \l
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+-------------+-------------+--------+-----------+-----------------------
postgres | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | |
template0 | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
(3 rows)

postgres=# SELECT version();
version
---------------------------------------------------------------------------------------------------
PostgreSQL 17.5 on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14+deb12u1) 12.2.0, 64-bit
(1 row)

postgres=#

6. Lets also create new database

For testing the restore later we can create a test database.

postgres=# CREATE DATABASE mydb;
CREATE DATABASE

postgres=# \l
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+-------------+-------------+--------+-----------+-----------------------
mydb | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | |
postgres | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | |
template0 | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
(4 rows)

postgres=#

Lets fill the newly created database with some sample table and content.

  1. Connect to the database
\c mydb
  1. Create a table
CREATE TABLE planets (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
diameter_km INTEGER,
has_life BOOLEAN
);
  1. Insert a sample data
INSERT INTO planets (name, diameter_km, has_life)
VALUES
('Mercury', 4879, FALSE),
('Venus', 12104, FALSE),
('Earth', 12742, TRUE),
('Mars', 6779, FALSE);
  1. Make sure it was created
mydb=# SELECT * FROM planets;
id | name | diameter_km | has_life
----+---------+-------------+----------
1 | Mercury | 4879 | f
2 | Venus | 12104 | f
3 | Earth | 12742 | t
4 | Mars | 6779 | f
(4 rows)

mydb=#

Create and apply the Blueprint

Veeam Kasten is using Kanister in the background to create logical backups of databases.
To successfully create a logical backup with Veeam Kasten, we need to use Kanisters workflow.

When it comes to Kastens implementation of Kanisters features it comes down to these things:

Custom Resources

  1. Blueprints – Tells the Kanister how to do backup, restore, and deletion of restore points etc..
  2. ActionSets – Are created automatically by Kasten when the backup job is started
  3. Profiles – Can be set trough Kasten UI, and are repositories (where kasten/kanister is saving backups and restore points)

Blueprint Bindings – Can be set trough Kasten UI and tells the Kanister which blueprint (set of instructions on how to execute actions) is used for which application

The main parts of the blueprint are:

  1. Actions
    1. Which are
      1. backup – Implements logical backup
      2. restore – Implements restore of the logical backup
      3. delete – Implements the deletion of the logical restore points from the repository
  2. Phases
    1. Each action includes a phase which executes a func Kanister functions .
    2. phases also define args where the majority of the “action” is happening when the phases are executed

1. Create postgres-logical-blueprint yaml, with the following contents

Blueprint was copied from Kanister GitHub repository kanister/examples/postgresql/v10.16.2/postgres-blueprint.yaml
What was changed from the original Blueprint v10.16.2 was:

  • export PGPASSWORD “”postgresql-password”” to “postgres-password” in backup and restore actions
  • | gunzip -c -f | sed ‘s/LOCALE/LC_COLLATE/’ | which is used in PostgreSQL older than 15, so if you want to restore older versions you can leave it like this, but for newer versions you have to include only | gunzip -c -f |

The guide from Veeam Kasten is using the V2 version, which I could not get to run properly, and it also has an interesting comment in GitHub README.md

NOTE: v2 Blueprints are experimental and are not supported with standalone Kanister.

postgres-logical-blueprint.yaml

kind: Blueprint
apiVersion: cr.kanister.io/v1alpha1
metadata:
name: postgres-logical-blueprint
namespace: kasten-io
actions:
backup:
kind: StatefulSet
outputArtifacts:
cloudObject:
keyValue:
backupLocation: "{{ .Phases.pgDump.Output.backupLocation }}"
phases:
- func: KubeTask
name: pgDump
objects:
pgSecret:
kind: Secret
name: '{{ index .Object.metadata.labels "app.kubernetes.io/instance"
}}-postgresql'
namespace: "{{ .StatefulSet.Namespace }}"
args:
command:
- bash
- -o
- errexit
- -o
- pipefail
- -c
- >
export PGHOST='{{ index .Object.metadata.labels
"app.kubernetes.io/instance" }}-postgresql.{{
.StatefulSet.Namespace }}.svc.cluster.local'

export PGUSER='postgres'

export PGPASSWORD='{{ index .Phases.pgDump.Secrets.pgSecret.Data
"postgres-password" | toString }}'

BACKUP_LOCATION=pg_backups/{{ .StatefulSet.Namespace }}/{{
.StatefulSet.Name }}/{{ toDate
"2006-01-02T15:04:05.999999999Z07:00" .Time | date
"2006-01-02T15:04:05Z07:00" }}/backup.sql.gz

pg_dumpall --clean -U $PGUSER | gzip -c | kando location push
--profile '{{ toJson .Profile }}' --path "${BACKUP_LOCATION}" -

kando output backupLocation "${BACKUP_LOCATION}"
image: ghcr.io/kanisterio/postgres-kanister-tools:0.116.0
namespace: "{{ .StatefulSet.Namespace }}"
delete:
inputArtifactNames:
- cloudObject
phases:
- func: KubeTask
name: deleteDump
args:
command:
- bash
- -o
- errexit
- -o
- pipefail
- -c
- >
kando location delete --profile '{{ toJson .Profile }}' --path '{{
.ArtifactsIn.cloudObject.KeyValue.backupLocation }}'
image: ghcr.io/kanisterio/postgres-kanister-tools:0.116.0
namespace: "{{ .Namespace.Name }}"
restore:
kind: StatefulSet
inputArtifactNames:
- cloudObject
phases:
- func: KubeTask
name: pgRestore
objects:
pgSecret:
kind: Secret
name: '{{ index .Object.metadata.labels "app.kubernetes.io/instance"
}}-postgresql'
namespace: "{{ .StatefulSet.Namespace }}"
args:
command:
- bash
- -o
- errexit
- -o
- pipefail
- -c
- >
export PGHOST='{{ index .Object.metadata.labels
"app.kubernetes.io/instance" }}-postgresql.{{
.StatefulSet.Namespace }}.svc.cluster.local'

export PGUSER='postgres'

export PGPASSWORD='{{ index
.Phases.pgRestore.Secrets.pgSecret.Data "postgres-password" |
toString }}'

BACKUP_LOCATION={{
.ArtifactsIn.cloudObject.KeyValue.backupLocation }}

kando location pull --profile '{{ toJson .Profile }}' --path
"${BACKUP_LOCATION}" - | gunzip -c -f | psql -q -U "${PGUSER}"
image: ghcr.io/kanisterio/postgres-kanister-tools:0.116.0
namespace: "{{ .StatefulSet.Namespace }}"

2. Make sure the password to access the postgreSQL is defined corectly

In backup and restore actions we have defined

export PGPASSWORD='{{ index .Phases.pgDump.Secrets.pgSecret.Data "postgres-password" | toString }}'

We can double check this with “getting” the secret as a yaml

jjozelj@jjozelj:~$ kubectl get secret postgres-postgresql -n postgresql-k10-test -o yaml
apiVersion: v1
data:
postgres-password: WHUxdkt3TVRrcA==
kind: Secret
metadata:
annotations:
.
.
.

3. Apply the blueprint file

We can apply it trough CLI or trough Kasten UI under Blueprints > Blueprints > Create New Blueprint

Or trough CLI with the following command:

jjozelj@jjozelj:~$ kubectl --namespace kasten-io apply -f postgres-logical-blueprint.yaml

4. Create Blueprint Binding

Kasten and Kanister need Blueprint Binding, so that when the backup job knows which resources to back up. In this case we are backing up StatefulSet with annotations:

  • release-namespace: postgresql-k10-test
  • release-name: postgres
  • Blueprint=postgres-logical-blueprint

You can create Blueprint Binding in two ways:

  • One is trough the kubectl CLI
  • The other is trough Kasten UI under Blueprints > Bindings > Create New Binding

Create Blueprint Binding trough kubectl CLI

jjozelj@jjozelj:~$ kubectl --namespace postgresql-k10-test annotate statefulset/postgres-postgresql kanister.kasten.io/blueprint=postgres-logical-blueprint
statefulset.apps/postgres-postgresql annotated

Here we can see that it added annotations to the statefulset

jjozelj@jjozelj:~$ kubectl describe statefulset postgres-postgresql -n postgresql-k10-test
Name: postgres-postgresql
Namespace: postgresql-k10-test
CreationTimestamp: Tue, 18 Nov 2025 15:00:50 +0000
Selector: app.kubernetes.io/component=primary,app.kubernetes.io/instance=postgres,app.kubernetes.io/name=postgresql
Labels: app.kubernetes.io/component=primary
app.kubernetes.io/instance=postgres
app.kubernetes.io/managed-by=Helm
app.kubernetes.io/name=postgresql
app.kubernetes.io/version=18.1.0
helm.sh/chart=postgresql-18.1.10
Annotations: kanister.kasten.io/blueprint: postgres-logical-blueprint
meta.helm.sh/release-name: postgres
meta.helm.sh/release-namespace: postgresql-k10-test
Replicas: 1 desired | 1 total

Create Backup Policy

Backup policy is created in Veeam Kasten UI:

Navigate to Policies > Policies > Create New Policy

Run the Backup Policy

Under Policies > Policies select the three dots on the right side of the policy name and select Run Once, and click Yes, Continue.
We can follow the process of the backup trough the Veeam Kasten UI on the Dashboard or trough the CLI with this commands:

kubectl get actionset -A -o wide --watch

Disaster strikes

1. Delete the namespace

Someone mistakenly deletes the whole namespace where our super important PostgreSQL StatefulSet lived.

kubectl delete namespace postgresql-k10-test

This will also delete Persistant Volume Claim, the Persistant Volume will also get deleted, as we have set persistentVolumeReclaimPolicy: Delete. But the PV can also be deleted manually.

We can check the status of objects with the following command:

kubectl get all -n postgresql-k10-test

2. Recreate the namespace

Follow the steps from the beginning of this guide at Create namespace where our PostgreSQL will live We can set the same name or we can change it to something else.

3. Restore the PostgreSQL

  • Restore is done trough the Veeam Kasten UI select the Restore Points then select the three dots on the right side of the application name postgresql-k10-test and restore point time you wish to restore it to.
  • Select Restore

Restore process can be monitored trough the Veeam Kasten UI on the Dashboard or trough the CLI, with the following command:

kubectl get actionset -A -o wide --watch

4. verify that restore process

  1. Check if the resources are back in the appropriate namespace
jjozelj@jjozelj:~$ kubectl get all -n postgresql-k10-test
NAME READY STATUS RESTARTS AGE
pod/postgres-postgresql-0 1/1 Running 0 2m35s

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/postgres-postgresql ClusterIP 10.124.148.176 <none> 5432/TCP 3m35s
service/postgres-postgresql-hl ClusterIP None <none> 5432/TCP 3m35s

NAME READY AGE
statefulset.apps/postgres-postgresql 1/1 2m35s
  1. Exec into the pod
jjozelj@jjozelj:~$ kubectl exec -it postgres-postgresql-0 -n postgresql-k10-test -- sh
  1. Log in to the PostgreSQL, list the databases and the content of mydb
$ psql -U postgres
Password for user postgres:
psql (17.5)
Type "help" for help.

postgres=# \l
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+-------------+-------------+--------+-----------+-----------------------
mydb | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | |
postgres | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | |
template0 | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | | postgres=CTc/postgres+
| | | | | | | | =c/postgres
(4 rows)

postgres=# \c mydb
You are now connected to database "mydb" as user "postgres".
mydb=# SELECT * FROM planets;
id | name | diameter_km | has_life
----+---------+-------------+----------
1 | Mercury | 4879 | f
2 | Venus | 12104 | f
3 | Earth | 12742 | t
4 | Mars | 6779 | f
(4 rows)

mydb=#

FIN

Leave A Comment