Lyon, le 27 février 2025

CloudNativePG ou comment embarquer un éléphant sur un porte-conteneurs !

Les deux premiers articles étaient l’occasion de découvrir l’opérateur et comment il nous aide à embarquer notre éléphant favori sur un porte-conteneurs 🐘. Embarquer un éléphant est une chose, le repêcher s’il tombe à l’eau en est une autre… Attaquons-nous à un sujet très important, les sauvegardes des instances PostgreSQL 🛟 !

moteur

Un peu de données

Pour le moment nos instances ne contiennent pas grand-chose. Il est temps d’insérer quelques données, notamment avec l’outil pgbench. C’est l’occasion pour moi de présenter l’option pgbench du plugin cnpg. pgbench est un outil très connu dans le monde PostgreSQL. Il permet notamment de générer des jeux de tests et d’effectuer des tests de charge (à ce sujet, avez-vous lu notre dernier article de blog ?). Les mainteneurs CloudNativePG ont intégré cet outil du projet cœur PostgreSQL, dans leurs images, nous permettant ainsi de l’utiliser.

$ kubectl cnpg pgbench postgresql -- --initialize --scale 10 
job/postgresql-pgbench-446336 created

$ kubectl logs job/postgresql-pgbench-446336
dropping old tables...
creating tables...
generating data (client-side)...
100000 of 1000000 tuples (10%) of pgbench_accounts done (elapsed 0.02 s, remaining 0.21 s)
200000 of 1000000 tuples (20%) of pgbench_accounts done (elapsed 0.08 s, remaining 0.30 s)
300000 of 1000000 tuples (30%) of pgbench_accounts done (elapsed 0.14 s, remaining 0.33 s)
400000 of 1000000 tuples (40%) of pgbench_accounts done (elapsed 0.20 s, remaining 0.30 s)
500000 of 1000000 tuples (50%) of pgbench_accounts done (elapsed 0.25 s, remaining 0.25 s)
600000 of 1000000 tuples (60%) of pgbench_accounts done (elapsed 0.30 s, remaining 0.20 s)
700000 of 1000000 tuples (70%) of pgbench_accounts done (elapsed 0.36 s, remaining 0.15 s)
800000 of 1000000 tuples (80%) of pgbench_accounts done (elapsed 0.44 s, remaining 0.11 s)
900000 of 1000000 tuples (90%) of pgbench_accounts done (elapsed 0.52 s, remaining 0.06 s)
1000000 of 1000000 tuples (100%) of pgbench_accounts done (elapsed 0.57 s, remaining 0.00 s)
vacuuming...
creating primary keys...
done in 0.96 s (drop tables 0.09 s, create tables 0.00 s, client-side generate 0.61 s, vacuum 0.11 s, primary keys 0.15 s).

La commande a généré 157 Mo de données dans la base app (qui pour rappel, est créée automatiquement par l’opérateur)… Oui je sais ! C’est une toute petite base, mais c’est pour de la démo ! Les sujets liés à de la volumétrie importante arriveront plus tard 😇.

Sauvegarde logique

À l’heure actuelle l’opérateur ne permet pas de déclencher une sauvegarde logique de manière déclarative. Pour autant, dès lors que l’instance est accessible, il est tout à fait possible de créer une sauvegarde avec l’outil pg_dump (ou pg_dumpall).

Par exemple, si je rends mon instance accessible avec kubectl port-forward :

kubectl port-forward --address 0.0.0.0 postgresql-2 5432:5432
Forwarding from 0.0.0.0:5432 -> 5432
Handling connection for 5432
[...]

Je peux appeler la commande pg_dump, renseigner le mot de passe et obtenir mon dump.

$ pg_dump -h localhost -U app -Fc > sauvegarde.dump
Password:
$ ls -lh sauvegarde.dump
-rw-rw-r-- 1 pierrick pierrick 2,7M janv. 22 17:36 sauvegarde.dump

L’exposition du port 5432 de l’instance est grandement facilitée par la commande kubectl port-forward. Dans un environnement de production, la mise en œuvre demandera un peu plus de travail 🙃.

En bref, il vous sera toujours possible d’effectuer des sauvegardes logiques d’une instance déployée par CloudNativePG.

Sauvegarde par Volume Snapshot

L’opérateur supporte aussi la méthode de sauvegarde par Volume Snapshot. C’est une fonctionnalité Kubernetes que ne supporte pas encore tous les Containers Storage Interface (CSI). Ce ne sera pas l’objet de l’article aujourd’hui, mais jetez tout de même un œil à cette fonctionnalité (Backup on Volume Snapshot).

Sauvegarde physique PITR

À l’inverse d’une sauvegarde logique, qui se limite à une base de données spécifique, une sauvegarde physique permet de sauvegarder l’instance dans sa globalité. Le principe est de sauvegarder toute l’arborescence PostgreSQL, que ce soit les fichiers de données, le contenu des index, les fichiers de configuration ou les éléments propres au fonctionnement de PostgreSQL.

Si en plus de cette sauvegarde, vous archivez régulièrement les journaux de transactions (WAL) alors vous avez mis en place une sauvegarde PITR (Point In Time Recovery) qui vous permettra, en rejouant ces fichiers là, de restaurer votre instance soit complètement, soit jusqu’à un certain point dans le passé.

Notre formation DBA3 traite de ce sujet de manière plus détaillée 😎.

Ce petit rappel étant fait, passons au vif du sujet !

Stockage objets

L’opérateur CloudNativePG repose sur l’outil Barman Cloud pour les sauvegardes physiques et l’archivage continu. Il est embarqué dans l’image qui est utilisée lors de la création d’une instance. Le stockage des journaux et des sauvegardes se fera obligatoirement sur un système de stockage objets. Cette page de documentation indique les principales solutions supportées.

Pour cette série d’articles, je vais utiliser la solution Object Storage de Scaleway, compatible S3. Les informations qui me seront nécessaires sont :

  • Le nom du Bucket à utiliser : ce sera backup-postgresql ;
  • Une clé d’accès API : jenevaispasvousladonner ;
  • Le secret lié à cette clé encoremoinslesecret ;
  • La région de stockage : fr-par.

Ces informations là sont à ajouter dans un objet Secret qui sera par la suite renseigné dans la configuration de notre instance. Les informations doivent être encodées en BASE64 dans le YAML. Voici un exemple de Secret.

---
apiVersion: v1
kind: Secret
metadata: 
  name: s3-scaleway
type: Opaque
data:
  ACCESS_KEY_ID: amVuZXZhaXNwYXN2b3VzbGFkb25uZXLCoA==
  ACCESS_REGION: ZnItcGFy
  ACCESS_SECRET_KEY: ZW5jb3JlbW9pbnNsZXNlY3JldMKg

N’oubliez pas de créer cet objet avec kubectl apply.

Configuration

Maintenant que nous avons un espace de stockage, voyons la configuration de notre instance pour la mise en place de cette sauvegarde PITR. Elle se fait dans la section backup.barmanObjectStore de notre objet Cluster. Voilà à quoi ressemblerait notre définition :

---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgresql
spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:17.0
  instances: 2
  storage:
    size: 20Gi
  backup:
    barmanObjectStore:
      destinationPath: "s3://backup-postgresql/"
      endpointURL: "https://s3.fr-par.scw.cloud"
      s3Credentials:
        accessKeyId:
          name: s3-scaleway
          key: ACCESS_KEY_ID
        secretAccessKey:
          name: s3-scaleway
          key: ACCESS_SECRET_KEY
        region:
          name: s3-scaleway
          key: ACCESS_REGION

destinationPath et endpointURL sont propres à la solution de stockage choisie. Des exemples existent pour vous aider à configurer cela (voir la page Samples).

Lors de la création du Cluster PostgreSQL, les informations du Secret s3-scaleway sont récupérées et l’archivage des journaux se fait automatiquement sur le Bucket S3.

Archivage des journaux

Dans les traces de notre instance, nous voyons désormais que les journaux sont bien archivés. Voici l’exemple d’une trace JSON mise en forme avec l’outil jq où nous pouvons voir, dans le champ msg et endTime par exemple, que le journal a bien été archivé.

{
  "level": "info",
  "ts": "2025-01-23T09:57:03.786672436Z",
  "logger": "wal-archive",
  "msg": "Archived WAL file",
  "logging_pod": "postgresql-1",
  "walName": "/var/lib/postgresql/data/pgdata/pg_wal/000000010000000000000005",
  "startTime": "2025-01-23T09:57:00.327727678Z",
  "endTime": "2025-01-23T09:57:03.786579567Z",
  "elapsedWalTime": 3.458851895
}

Côté Scaleway, toute une arborescence a été créée dans le Bucket backup-postgresql :

backup-postgresql
└── postgresql
    └── wals
        └── 0000000100000000
  • postgresql, qui correspond au name de notre cluster PostgreSQL ;
  • wals, qui contient les dossiers correspondant aux différentes timelines de l’instance ;
  • 0000000100000000, qui contient les journaux.

Sauvegarde

L’opérateur CloudNativePG étend l’API Kubernetes avec une nouvelle ressource appelée Backup. En créant une ressource de ce type, une sauvegarde physique sera déclenchée et stockée dans le Bucket associé à notre Cluster. Le nom du Cluster doit évidemment être mentionné dans la ressource pour que les informations du stockage S3 soient récupérées.

Créons le fichier sauvegarde.yaml avec le contenu suivant :

---
apiVersion: postgresql.cnpg.io/v1
kind: Backup
metadata:
  name: premiere-sauvegarde
spec:
  cluster:
    name: postgresql

Et créons cette ressource avec kubectl apply -f sauvegarde.yaml :

$ kubectl apply -f sauvegarde.yaml
backup.postgresql.cnpg.io/premiere-sauvegarde created

Le Bucket contient désormais le dossier base qui contiendra les différentes sauvegardes effectuées. Celle qui vient d’être faite est bien présente dans le dossier.

backup-postgresql
└── postgresql
    ├── base
    │   └── 20250123T105602
    │      ├── backup.info
    │      └── data.tar
    └── wals
        └── 0000000100000000

Pour information, l’opérateur CloudNativePG est configuré pour effectuer la sauvegarde à partir d’un secondaire pour ne pas charger l’instance primaire. C’est ce que l’on peut voir dans les traces de l’opérateur. Le champ pod indique bien que c’est le secondaire qui a été choisi.

{
  "level": "info",
  "ts": "2025-01-23T10:56:02.112865913Z",
  "msg": "Starting backup",
  "controller": "backup",
  "controllerGroup": "postgresql.cnpg.io",
  "controllerKind": "Backup",
  "Backup": {
    "name": "premiere-sauvegarde",
    "namespace": "default"
  },
  "namespace": "default",
  "name": "premiere-sauvegarde",
  "reconcileID": "a91635aa-0c72-4c88-98c8-f7de4c2da83a",
  "cluster": "postgresql",
  "pod": "postgresql-2"
}

Cette configuration par défaut peut être surchargée avec l’option target: "primary" de la ressource Backup.

Réutiliser cette sauvegarde

J’aurais pu titrer cette partie Restauration de l’instance, mais je ne l’ai volontairement pas fait pour proposer un autre angle pour aborder ce sujet. Évidemment, dès lors qu’une sauvegarde existe, vous pouvez l’utiliser pour restaurer une instance en panne. Ce sera très probablement un article de blog (ça y est la liste d’articles s’allonge…😏).

Les équipes de développement ont souvent besoin de données de production à jour pour tester leur nouvelle version (je fais simple et ne pas rentrer dans les problématiques d’anonymisation des données par exemple). L’idée ici est de déployer une nouvelle instance sur un nouveau cluster Kubernetes à partir de cette sauvegarde là.

Sur un autre cluster Kubernetes, appelé k8s-demo, j’installe l’opérateur CloudNativePG et crée le cluster postgresql-dev défini de cette manière :

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgresql-dev
spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:17.0
  instances: 1
  storage:
    size: 20Gi
  bootstrap:
    recovery:
      source: postgresql
  externalClusters:
    - name: postgresql
      barmanObjectStore:
        destinationPath: "s3://backup-postgresql/"
        endpointURL: "https://s3.fr-par.scw.cloud"
        s3Credentials:
          accessKeyId:
            name: s3-scaleway
            key: ACCESS_KEY_ID
          secretAccessKey:
            name: s3-scaleway
            key: ACCESS_SECRET_KEY
          region:
            name: s3-scaleway
            key: ACCESS_REGION

Les deux instructions intéressantes ici sont bootstrap et externalClusters. Elles permettent d’indiquer que la création de notre instance PostgreSQL, doit se faire à partir de la sauvegarde qui se trouve sur le stockage S3 indiqué. Si vous faites le test, n’oubliez pas de créer le Secret s3-scaleway dans le cluster k8s-dev. Il contient les informations de connexion au S3.

Un Pod spécifique va être déployé. Son nom est plutôt parlant : postgresql-dev-1-full-recovery-xxxxx. Une restauration de type full est en train de se faire à partir de la sauvegarde S3. Lorsque celle-ci est terminée, le Pod est créé et votre instance est accessible, les données sont bien présentes sur l’instance du pod postgresql-dev.

$ kubectl exec -it postgresql-dev-1 -- psql -d app -c "\dt"
Defaulted container "postgres" out of: postgres, bootstrap-controller (init)
             List of relations
 Schema |       Name       | Type  | Owner 
--------+------------------+-------+-------
 public | pgbench_accounts | table | app
 public | pgbench_branches | table | app
 public | pgbench_history  | table | app
 public | pgbench_tellers  | table | app
(4 rows)

Vous avez donc une nouvelle instance, créée à partir d’une sauvegarde de votre instance de production.

Petit truc à savoir 💡 : l’opérateur utilise par défaut la sauvegarde la plus récente disponible dans le Bucket. Ce fonctionnement est ajustable selon votre besoin.

Conclusion

Nous venons de voir comment sauvegarder notre instance avec CloudNativePG et l’outil Barman sur une solution de stockage objets 🛟. La mise en place est plutôt simple. J’ai volontairement fait une sauvegarde basique, sans rentrer dans les détails. Sachez qu’il existe de nombreuses options, notamment pour ce qui est de la compression ou de la rétention des sauvegardes. À vous de jouer !

Des questions, des commentaires ? Écrivez-nous !


DALIBO

DALIBO est le spécialiste français de PostgreSQL®. Nous proposons du support, de la formation et du conseil depuis 2005.