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 🛟 !
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 serabackup-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 auname
de notre cluster PostgreSQL ;wals
, qui contient les dossiers correspondant aux différentestimelines
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 !