CloudNativePG

Photo de Walter Gehr, Creative Commons licence.


Objectifs

  • Découverte de l’opérateur CloudNativePG
  • Déploiement d’instances PostgreSQL via l’opérateur
  • Tests et découvertes de fonctionnalités

Différents sujets seront abordés pour présenter l’opérateur du mieux possible, que ce soit sur le projet CloudNativePG (historique, développement, CNCF…), sur ses fonctionnalités, ses mécanismes internes ou encore les images utilisées.

L’objectif de ce module est la découverte de l’opérateur CloudNativePG mais également de sa prise en main. L’opérateur facilite le déploiement de clusters PostgreSQL dans Kubernetes. Une présentation générale de ce qu’est un opérateur et de son rôle sera faite pour bien poser le contexte.

Le TP permet d’installer l’opérateur CloudNativePG, de déployer un cluster PostgreSQL et d’effectuer différentes opérations comme la configuration d’une instance, la mise en place de sauvegardes ou de la restauration. Des exercices optionnels sont également présents.


Avant propos Kubernetes


Avant propos Kubernetes

Quelques explications concernant Kubernetes

  • Kubernetes, K8s
  • Orchestrateur de conteneurs
  • Initialement prévu pour des applications dites Stateless
  • De plus en plus d’applications de type base de données

Kubernetes est une plateforme qui permet d’orchestrer le déploiement de nombreuses applications sous la forme de conteneurs. Cette plateforme, initialement imaginée pour des applications dites Stateless (sans état), est devenue petit à petit une pierre angulaire de l’infrastructure informatique de nombreuses sociétés. Particulièrement appréciée des développeurs, cette solution se voit être de plus en plus utilisée pour héberger des applications de type base de données, quelles soient relationnelles ou non.


Avant propos Kubernetes

Ce schéma montre très simplement le principe d’un cluster Kubernetes, composé de trois nœuds de travail, ou Workers, et d’un plan de contrôle, ou Control Plane qui est en charge de gérer les ressources et la distribution des conteneurs sur l’ensemble des nœuds du cluster.

Sur chacun des nœuds se trouvent un environnement d’exécution de conteneur (ou Container Runtime, comme DockerEngine, containerd ou encore CRI-O,) qui permet au Kubelet (composant Kubernetes) de créer les Pods et les conteneurs qui les composent.

Lorsqu’une application conteneurisée doit être déployée, un nœud est choisi en suivant certaines règles et un Pod est créé. Il peut contenir un ou plusieurs conteneurs qui partageront alors une certains nombre de ressources (mémoire, processeur, Huge Pages, pile réseau, etc).

Tout l’objet de ce module est de voir comment il est possible de déployer des instances PostgreSQL conteneurisées grâce au concept d’opérateur Kubernetes.


Avant propos Kubernetes

  • Quelques objets basiques de Kubernetes
    • Pod : un ou plusieurs conteneurs applicatifs
    • Service : permet d’accéder durablement à un ou plusieurs Pods
    • Deployment, Secret, Configmap, …
  • Domaines spécifiques
  • Certains génériques

Kubernetes permet la création de certains objets (Resources), au sein d’un cluster. Il y a de nombreux objets ayant chacun un rôle bien précis dans différents domaines (réseau, stockage, déploiement de conteneur, configuration, tâche planifiée, …). C’est à partir de ces objets de base que l’on peut définir les applications à déployer en écrivant des manifests au format YAML.

Un Pod par exemple est le plus petit objet dans Kubernetes. Il représente la plus petite unité déployable au sein d’un cluster. Il embarque un ou plusieurs conteneurs applicatifs, une identité réseau pour les rendre joignables, des options supplémentaires et des ressources (ram, cpu, …) qui, le cas échéant, seront partagées entre les différents conteneurs.

Les Services quant à eux permettent d’accéder aux Pods quelque soit l’emplacement où ils se trouvent, c’est à dire que, si un Pod est redéployé sur un autre nœud, il saura acheminer les paquets réseaux au bon endroit.

Ces objets, bien que nombreux peuvent être assez génériques, notamment les Pods qui se “contentent” de déployer des conteneurs. Ils n’ont aucune connaissance sur le type d’application qu’ils contiennent.

C’est exactement à ce moment que rentrent en jeu les opérateurs avec la définition de ressources spécifiques et leur gestion fine des applications déployées.


Principe d’un opérateur

  • Nouvelles ressources disponibles
  • Custom Resource Definitions, Custom Resources
  • Cluster, Database, Backup, …
  • L’opérateur crée certaines ressources pour nous
    • Pod, Service, Secret, …
  • Des fonctionnalités plus poussées

Un opérateur étend les possibilités d’un cluster Kubernetes grâce à la définition de Custom Resource Definition. Elles indiques les spécifications de nouvelles ressources qui seront désormais déployables dans le cluster.

Par exemple, l’opérateur CloudNativePG ajoute les ressources suivantes à l’API de Kubernetes.

kubectl api-resources --api-group=postgresql.cnpg.io
NAME                   SHORTNAMES   APIVERSION              NAMESPACED   KIND
backups                             postgresql.cnpg.io/v1   true         Backup
clusterimagecatalogs                postgresql.cnpg.io/v1   false        ClusterImageCatalog
clusters                            postgresql.cnpg.io/v1   true         Cluster
databases                           postgresql.cnpg.io/v1   true         Database
imagecatalogs                       postgresql.cnpg.io/v1   true         ImageCatalog
poolers                             postgresql.cnpg.io/v1   true         Pooler
publications                        postgresql.cnpg.io/v1   true         Publication
scheduledbackups                    postgresql.cnpg.io/v1   true         ScheduledBackup
subscriptions                       postgresql.cnpg.io/v1   true         Subscription

Au lieu de devoir déployer manuellement des ressources de bases de Kubernetes (Pod, Secret, Service, …) pour former une instance PostgreSQL, un opérateur nous permet d’instancier de nouvelles ressources directement de type instance PostgreSQL. La création des ressources de base est laissée à la discrétion de l’opérateur.

Au delà de ces nouvelles ressources, un opérateur apportera également des fonctionnalités plus poussées, comme, en ce qui concerne des opérateurs dédiés à PostgreSQL, l’automatisation des montées de versions, de la bascule automatique en cas de panne, de la gestion des sauvegardes, etc.


Opérateurs PostgreSQL

  • Maturité variable en fonction des projets
  • Des spécificités à étudier
    • Extensions PostgreSQL
    • Licence
    • Patroni / Kubernetes
    • Outils de sauvegarde

Les opérateurs existants ont chacun leurs spécificités et particularités. Le site operatorhub liste les principaux opérateurs qui existent.

Leur maturité est différente, allant du niveau 1, correspondant à des opérateurs facilitant l’installation et la configuration, jusqu’au niveau 5 où un opérateur se doit de pouvoir faire face à des situations plus complexes (auto-healing, bascule automatique, …). Voir à ce propos le site Operator Framework qui explique les différents niveaux existants.

Le premier opérateur qui a vu le jour est celui de Zalando. Il a été écrit pour répondre à un des besoins de leurs équipes de développeurs. Ils voulaient un outil leur permettant de déployer facilement des instances PostgreSQL sans pour autant passer par des cloud providers et en changeant un outil web interne devant obsolète qui était utilisé jusqu’à présent. Le cas d’usage d’instances de tests ou jetables était présent. Kubernetes pouvait répondre à ce besoin mais nécessitait un outil spécifique pour gérer le déploiement et la configuration des instances.

Chacun de ces opérateurs à des spécificités propres dans différents domaines. Par exemple, l’ajout d’une nouvelle extension PostgreSQL est gérée différemment selon l’opérateur. La plupart imposent qu’elles soient présentes dans l’image de conteneur utilisée, d’autres mettent en place des mécanismes pour en rajouter à chaud. Les licences de publication des opérateurs et des images sont elles aussi différentes (MIT, Apache 2.0, GNU AGPLv3, …). L’opérateur CloudNativePG se repose uniquement sur l’API de Kubernetes pour gérer la haute disponibilité et la bascule automatique en cas de panne. Les autres embarquent l’outil Patroni dans les images. Enfin, les outils de sauvegardes physiques varient. Barman ou pgBackRest restent tout de même principalement utilisés.


Attraction Github

Ces opérateurs ont leurs projets sur Github (que ce soit leur vrai dépôt ou un miroir de celui‑ci). Chaque utilisateur sur Github a la possibilité de marquer un projet comme intéressant en lui attribuant une étoile.

Il est donc possible de connaître l’attrait de chaque projet. Le graphique présenté montre l’évolution au fil des années de l’attrait des cinq opérateurs les plus connus. CloudNativePG est celui qui connaît la courbe d’adoption la plus spectaculaire.


L’opérateur CloudNativePG


L’opérateur CloudNativePG

Le projet a été initialement développé par la société 2ndQuadrant en 2019. La société a été rachetée par EnterpriseDB (EDB) en 2020. Le développement de cet opérateur a continué en interne avant que le projet ne soit rendu Open Source en 2022.

La gouvernance du projet se rapproche de celle de PostgreSQL avec une core team et l’ouverture à la contribution communautaire. L’intégration d’un nouveau mainteneur est soumise au vote des mainteneurs actuels du projet.

Le projet est bien engagé dans une démarche communautaire et Open Source avec notamment l’acceptation du projet au programme Sandbox de la Cloud Native Computing Foundation en janvier 2025. Une démarche est en cours depuis novembre 2025 pour être accepté au programme Incubation.

La documentation du projet est particulièrement claire et fournie.


Ce que permet CloudNativePG

  • Gestion déclarative
    • d’instances, de bases de données, …
  • Mise en place de réplication automatique
    • par streaming replication
  • Archivage des journaux de transactions

D’une manière générale, si vous souhaitez déployer des instances PostgreSQL dans Kubernetes, nous vous recommandons d’utiliser un opérateur, quel qu’il soit. Les opérateurs sont spécialisés dans la gestion d’instances PostgreSQL. Nous déconseillons vivement le déploiement de conteneurs PostgreSQL sans opérateur, d’autant plus pour de la production.

Les fonctionnalités de CloudNativePG sont nombreuses. Il vous permet de gérer de manière déclarative un ensemble d’éléments PostgreSQL (bases, rôles, tablespaces…). Ils seront détaillés dans la suite du module.

Le déploiement d’instances, qu’elles soient primaires ou secondaires est très nettement facilité par l’opérateur : l’utilisateur de réplication est créé automatiquement, le déploiement d’un secondaire se fait en modifiant un seul champ dans la définition YAML, un slot de réplication est automatiquement créé pour protéger la réplication.

L’archivage des journaux de transactions est également supportée nativement par l’opérateur. Le paramètre archive_command est positionné par défaut.


Ce que permet CloudNativePG

  • Sauvegardes PITR, sauvegardes planifiées
  • Bascule automatique ou manuelle
  • Haute disponibilité, hibernation, fencing, plugin kubectl, …

Tout ceci sera détaillé au fur et à mesure du module, n’ayez crainte !

Les sauvegardes PITR sont supportées nativement et faites, par défaut, via le plugin Barman Cloud sur des stockages “cloud” (S3, GCP, Azure Blob).

La haute disponibilité est nativement supportée et gérée par l’intermédiaire des objets Services de Kubernetes et une utilisation astucieuse des Labels (un article de blog a d’ailleurs été écrit à ce sujet.)

Toutes ces fonctionnalités sont très alléchantes, il n’en reste pas moins que de nombreux changements surviennent en déployant PostgreSQL sur une infrastructure conteneurisée comme Kubernetes. Un certain temps doit être alloué à l’étude de cette migration d’infrastructure tant les sujets impactés sont variés et importants : système de stockage, extensions PostgreSQL, images utilisées, accessibilité des instances, récupération des traces…

Des changements d’habitudes de travail auront nécessairement lieu et impliqueront également un accompagnement des équipes DBAs.


Versions supportées

  • PostgreSQL
    • 14, 15, 16, 17 et 18
  • CloudNativePG
    • 1.27 et 1.28 (décembre 2025)
    • 2 versions majeures supportées en même temps
    • Uniquement des versions de PostgreSQL supportées
  • Sans oubliez les versions Kubernetes

Le PostgreSQL Global Development Group supporte chaque version majeure pendant une durée minimale de 5 ans. Par exemple, n’est plus supportée la version 13 depuis novembre 2025. Il n’y aura pour elle plus aucune mise à jour mineure, donc plus de correction de bug ou de faille de sécurité. Le support de la dernière version majeure, la 18, devrait durer jusqu’en 2030. Sur une année, il y a donc cinq versions de PostgreSQL simultanément supportées.

Cette politique de support de version est suivie par le projet CloudNativePG. L’opérateur permet de déployer uniquement des versions de PostgreSQL qui sont supportées par le PGDG.

En ce qui concerne les versions de l’opérateur, deux versions sont supportées simultanément. Actuellement ce sont les versions 1.27 et 1.28. Chaque version est également supportée pour des versions spécifiques de Kubernetes.

La fréquence de montée de version sera donc plus élevée que d’habitude comme l’opérateur doit être mis à jour fréquemment. La durée de vie d’une version de CloudNativePG est de 6 mois.

N’hésitez pas à regarder la documentation à ce sujet : Supported releases.

Il vous sera important d’avoir des créneaux de maintenance pour effectuer ces montées de versions.


Installation de l’opérateur

  • Deux éléments :
    • L’opérateur (dans un ou plusieurs Pods)
    • Les Custom Resource Definitions (extension de l’API Kubernetes)
  • Installation :
    • Manifest YAML
    • Helm Chart (version packagée)
    • OLM (Operator Lifecycle Manager)
  • Un opérateur par cluster Kubernetes

Principe de fonctionnement

Un opérateur Kubernetes est, de manière simplifiée, composé de deux éléments :

  • Un contrôleur ;
  • Des Custom Resource Definitions.

Le premier élément n’est ni plus ni moins que le code de l’opérateur, son intelligence. Il embarque un ensemble de fonctions pour gérer convenablement PostgreSQL. L’opérateur CloudNativePG est écrit en Go et se base sur le framework de développement de CLI Cobra. Nous ne rentrerons pas dans le détail du développement de l’opérateur, mais sachez que pour chaque Custom Resource définie, il existe dans le code un controller. Cet élément est en charge de la gestion de chacune des ressources qui peuvent être gérées par l’opérateur (Backup, Cluster, etc ).

Les Custom Resource Definitions contiennent la définition des nouvelles ressources Kubernetes qu’apporte l’opérateur. À partir du moment où les définitions sont déclarées dans le cluster Kubernetes, elles pourront être créées par un utilisateur. Ce sera alors l’opérateur qui saura les gérer (création, modification…).

L’installation de l’opérateur peut se faire de plusieurs manières différentes :

  • soit en appliquant directement les fichiers YAML ;
  • soit en utilisant le Helm Chart fourni par le projet ;
  • soit via OLM (Operator Lifecycle Manager).

Les fichiers YAML se trouvent sur le githubusercontent.com du projet CloudNativePG. Pour la version 1.28.0 de l’opérateur, il est possible de les trouver ici : https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.28/releases/cnpg-1.28.0.yaml.

Pour installer l’opérateur sur un cluster Kubernetes auquel vous avez accès, rien de plus simple. Il suffit d’appeler kubectl apply --server-side -f suivi de l’URL.

kubectl apply --server-side \
-f  https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/main/releases/cnpg-1.28.0.yaml

Différentes ressources Kubernetes seront créées (Namespace, ServiceAccount, Configmap, Deployment etc). Toutes sont importantes évidemment, mais la Deployment nommée cnpg-controller-manager est la plus centrale puisqu’elle correspond concrètement à l’intelligence de l’opérateur CloudNativePG.

namespace/cnpg-system serverside-applied
customresourcedefinition.apiextensions.k8s.io/backups.postgresql.cnpg.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/clusterimagecatalogs.postgresql.cnpg.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/clusters.postgresql.cnpg.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/imagecatalogs.postgresql.cnpg.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/poolers.postgresql.cnpg.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/scheduledbackups.postgresql.cnpg.io serverside-applied
serviceaccount/cnpg-manager serverside-applied
clusterrole.rbac.authorization.k8s.io/cnpg-manager serverside-applied
clusterrolebinding.rbac.authorization.k8s.io/cnpg-manager-rolebinding serverside-applied
configmap/cnpg-default-monitoring serverside-applied
service/cnpg-webhook-service serverside-applied
deployment.apps/cnpg-controller-manager serverside-applied
mutatingwebhookconfiguration.admissionregistration.k8s.io/cnpg-mutating-webhook-configuration serverside-applied
validatingwebhookconfiguration.admissionregistration.k8s.io/cnpg-validating-webhook-configuration serverside-applied

L’autre possibilité est d’utiliser le Helm Chart proposé par le projet CloudNativePG (ou vos propres Helm Chart si vous êtes suffisamment à l’aise). Il est également disponible publiquement sur Github (voir https://github.com/cloudnative-pg/charts/tree/main/charts/cloudnative-pg). Il vous faudra avant tout ajouter le dépôt cnpg pour retrouver les Charts :

helm repo add cnpg https://cloudnative-pg.github.io/charts

Puis installer avec la commande helm upgrade --install :

helm upgrade --install cnpg \
  --namespace cnpg-system \
  --create-namespace \
  cnpg/cloudnative-pg

La dernière option, via OLM, nécessite l’installation du manager OLM dans votre cluster Kubernetes puis d’installer CloudNativePG via ce manager.

Le choix de la méthode d’installation vous revient entièrement. Il doit être adapté à votre méthode de déploiement (à la main ? avec une chaîne CI/CD ?).

Quelque soit la manière dont est installé l’opérateur, vous ne pourrez installer qu’un seul opérateur par cluster Kubernetes.


Les images utilisées

  • Pod CloudNativePG
    • ghcr.io/cloudnative-pg/cloudnative-pg:1.28.0
  • Pod PostgreSQL
    • ghcr.io/cloudnative-pg/postgresql
    • Tag minimal ou standard avec OS
      • 18.1-minimal-trixie
  • Images personnalisées

Il est nécessaire de distinguer deux types d’images.

La première concerne l’opérateur CloudNativePG qui est une brique logicielle, développée en Go, et qui est mise à disposition sous forme d’image par l’équipe en charge du projet. Par défaut, l’image utilisée est cloudnative-pg:1.28.0. Elle se trouve sur la registry de Github (ghcr.io) associée au projet CloudNativePG.

L’opérateur va se charger de déployer PostgreSQL pour nous. Ces déploiements se font à partir d’images également fournies par le projet. Elles se trouvent également sur cette même registry.

Les images utilisées pour déployer PostgreSQL reposent sur une image fournie quant à elle par le projet Debian. Le Dockerfile nous permet de voir que PostgreSQL est installé depuis le dépôt du PGDG. Deux versions existent : minimal et standard. La dernière version contient notamment les extensions PGAudit ou encore pgvector.

Une récente refonte de la chaîne de fabrication des images a été faite (août 2025). Parmi les choses à retenir, il faut savoir que les images sont reconstruites tous les lundis pour garantir que les correctifs de bugs ou de sécurité système soit bien appliqués. Chaque nouvelle image se voit taggée avec un timestamp, par exemple : 16.10-202509090953-minimal-trixie.

D’autres tags valides ressemblent par exemple à 17.7-minimal-trixie ou 17.7-standard-trixie. Ils pointent sur la dernière version de l’image (générée donc le lundi) qui contient PostgreSQL en version 17.7 sur une Debian trixie.

Il vous est également possible de créer vos propres images et de les utiliser avec CloudNativePG. Certains pré-requis sont nécessaires comme par exemple la présence dans le PATH des outils initdb, pg_ctl et d’exécutables Barman Cloud. Tous les pré-requis sont listés dans la documentation.


⤿ Travaux pratiques

  • Prise en main du cluster Kubernetes
  • Installation de l’opérateur CloudNativePG

Gestion déclarative


Principe

  • Fichiers YAML
  • On décrit juste, on laisse l’outil faire pour nous
    • Même idée qu’Ansible
  • État désiré <-> État actuel
  • Boucle de réconciliation

Le principe de la gestion déclarative est de décrire ce que l’on souhaite (une instance PostgreSQL, une sauvegarde, une base de données, etc) et de laisser le système, en l’occurrence Kubernetes et l’opérateur CloudNativePG, faire le travail de déploiement pour nous.

Leurs rôles est de faire en sorte que l’état d’une ressource corresponde toujours à l’état désiré (i.e défini dans le fichier YAML). C’est ce qui est appelé une boucle de réconciliation. L’opérateur suit les changements, voulus ou exceptionnels, qui ont lieu sur la ressource en question et réagira en conséquence.

Passons en revue quelques ressources qui sont désormais créables dans Kubernetes.


Cluster

  • Définition minimale d’un Cluster PostgreSQL
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgresql
spec:
  instances: 1
  storage:
    size: 2Gi
  walStorage: # Bonne pratique
    size: 2Gi

Quelques lignes de YAML suffisent pour décrire un cluster PostgreSQL. Voici une explication des différents champs présents :

  • apiVersion : la version de l’API de Kubernetes utilisée (ici celle apportée par l’opérateur CloudNativePG) ;
  • kind : le type d’objet créé; ici un cluster PostgreSQL (donc une ou plusieurs instances) ;
  • metadata : des informations pour identifier l’objet, notamment son nom (name) ;
  • spec : les spécifications de l’objet en question ;
    • instances : le nombre d’instances voulues (1 primaire, plus des secondaire(s)) ;
    • storage : les informations sur le stockage souhaité pour PGDATA, par exemple la taille (size) des Persistant Volumes ;
    • walStorage : les informations sur le stockage souhaité pour les journaux de transactions.

Vous noterez qu’il n’est pas obligatoire d’indiquer quelle image sera utilisée. Par défaut, l’image utilisée pour ce(s) Pod sera celle correspondant à la dernière version à majeure de PostgreSQL.

Une bonne pratique dans PostgreSQL, souvent recommandée mais peu appliquée, est d’avoir au moins deux espaces de stockage bien distincts :

  • un pour les données ;
  • un pour les journaux de transactions (WAL).

CloudNativePG respecte cette bonne pratique avec le paramètre spec.walStorage. Lorsque le cluster est créé, deux Persistant Volumes distincts sont créés automatiquement, un pour PGDATA et un pour les journaux de transactions (WAL). Si ce paramètre n’est pas utilisé, les journaux se trouveront dans le même volume que PGDATA, ce que nous ne recommandons pas.

De nombreux paramètres supplémentaires existent pour configurer nos instances PostgreSQL. Des subtilités existent aussi dans le déploiement du Pod, notamment sur le fait qu’un init-container est déployé avant le conteneur PostgreSQL. Tout ceci sera détaillé plus tard dans ce module.

En cas de panne de l’opérateur CloudNativePG, les objets Cluster, et autres ressources, restent disponibles et fonctionnlles dans Kubernetes : les instances ne sont pas arrêtées ! Cependant, un cas de modifications de configuration ou d’incident, l’opérateur n’étant plus présent, aucune action automatique ne se fera.


Éléments initiaux

  • PostgreSQL
    • Base de données : app
    • Rôles : app, streaming_replica
    • Règles : pg_hba
  • Kubernetes
    • Secrets : postgresql-app (contient le mot de passe du rôle app)
      • kubectl describe secrets postgresql-app
    • Services : postgresql-r, postgresql-ro, postgresql-rw

Lorsque vous déployez une instance, plusieurs éléments sont automatiquement créés par l’opérateur, que ce soit dans l’instance avec la création de rôles ou d’une base de données, ou dans Kubernetes comme des Secrets ou des Services.

L’instance déployée est partiellement configurée pour fonctionner dès sa création. Plusieurs paramètres PostgreSQL sont déjà configurés, le fichier pg_hba.conf est en partie renseigné, des certificats sont générés, etc.

Deux nouveaux rôles existent : app et streaming_replica. Le premier est un rôle basique avec le droit de connexion. Le deuxième est un rôle utilisé par les instances secondaires lors de la mise en place de la réplication physique. Le rôle postgres existe lui aussi mais n’est pas créé par CloudNativePG.

Une base de données nommée app est d’office créée avec la configuration suivante :

  • Name : app
  • Owner : app
  • Encoding : UTF8
  • Locale Provider : libc
  • Collate : C
  • Locale Provider : C

Du côté de Kubernetes aussi, des ressources sont automatiquement créées. Certaines nous concernent directement en tant qu’administrateur PostgreSQL. Il y a notamment un Secret qui est créé et qui contient le mot de passe du rôle PostgreSQL app. D’autres objets comme des Services sont utilisés pour la connectivité de l’instance, ou des instances si des réplications existent.

Pour chaque cluster PostgreSQL déployé, trois services dédiés sont créés. Par exemple, pour le Cluster nommé postgresql :

  • un service qui permet d’accéder au primaire : postgresql-rw qui est en lecture/écriture ;
  • un service qui permet d’accéder uniquement aux secondaires : postgresql-ro qui sont en lecture seule ;
  • un service qui permet d’accéder à toutes les instances : postgresql-r.

Nous verrons lorsque le sujet de la haute disponibilité sera abordé à quoi ces Services peuvent servir.


Modification des éléments initiaux

  • Modification possible de certains éléments
    • Uniquement lors du premier démarrage
  • Partie spec.bootstrap.initdb du fichier YAML du Cluster
  • La commande initdb est utilisée
spec: # Cluster
[]
  bootstrap:
    initdb:
      database: mabase
      owner: monrole
  instances: 1
  storage:
    size: 2Gi
  walStorage: # Bonne pratique
    size: 2Gi

Les éléments déployés par défaut par CloudNativePG peuvent être modifiés si ils ne vous conviennent pas. Cependant, vous devez le faire à l’initialisation de l’instance. Cela ne sera plus possible après coup.

La section bootstrap correspond à la manière dont est initiée l’instance. Par défaut c’est la méthode initdb qui est utilisée, mais nous verrons que d’autres méthodes peuvent être utlisées, notamment pour créer une instance à partir d’une sauvegarde.

Les changements que vous voulez apporter doivent être inscrits dans la section bootstrap.initdb. Dans l’exemple suivant, le nom de la base automatiquement créée et le nom du rôle sont modifiés par mabase et monrole.

  bootstrap:
    initdb:
      database: mabase
      owner: monrole

De nombreuses autres options peuvent être passées à initdb, comme les plus notables :

  • dataChecksums : pour activer les sommes de contrôle sur l’instance (false par défaut ) ;
  • encoding : pour choisir l’encodage des caractères (UTF8 par défaut) ;
  • walSegmentSize : si voulez changer la taille par défaut (16 Mo) des WALs.

Database

  • Définition minimale d’une Database
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
  name: mabase
spec:
  name: mabase
  owner: monrole
  ensure: present
  isTemplate: false
  cluster:
    name: postgresql

Vous pouvez créer des ressources Database depuis la version 1.25.0 de l’opérateur. L’exemple ci-dessus permet de créer la base mabase dans l’instance PostgreSQL référencée par le paramètre spec.cluster.name. Le rôle monrole sera le propriétaire de cette base. Il doit exister dans l’instance pour que la création de cette ressource se fasse correctement. Autrement, un message d’erreur apparaîtra dans la description de la ressource. Par exemple :

kubectl get databases.postgresql.cnpg.io -o=custom-columns=MESSAGE:..message

MESSAGE
while creating database "mabase": ERROR: role "monrole" does not exist (SQLSTATE 42704)

Il existe là aussi de nombreux paramètres pour configurer une ressource Database. En ce qui concerne l’exemple ci-dessus :

  • apiVersion : la version de l’API de Kubernetes est utilisée (ici celle apportée par l’opérateur CloudNativePG) ;
  • kind : le type d’objet créé, ici une base de données ;
  • metadata : des informations pour identifier l’objet, notamment son nom (name) ;
  • spec : les spécifications de l’objet en question ;
    • name : le nom de la base de données ;
    • owner : le nom du rôle PostgreSQL qui sera le propriétaire de la base de données. Il doit exister dans l’instance;
    • cluster : le nom du cluster PostgreSQL dans laquelle doit être créée cette base de données.
    • ensure : indique si la base doit être présente (present) ou absente (absent) de l’instance. Attention, si une base de données existe et qu’un CRD Database est appliquée avec ensure: absent, elle sera supprimée par l’opérateur.

Vous pouvez trouver de manière détaillée les autres paramètres sur cette page. Voici d’autres paramètres de la section spec qui nous semblent intéressants :

  • encoding: permet d’indiquer l’encodage de la base (UTF8, LATIN1… ) ;
  • template : correspond au nom du modèle de base qui doit être utilisé pour la base créée ;
  • isTemplate : permet d’indiquer si la base créée est un modèle ou pas ;
  • allowConnections : permet d’indiquer s’il sera possible de se connecter à cette base ;
  • connectionLimit : correspond au nombre de connexions simultanées autorisées à cette base ;
  • tablespace : correspond au TABLESPACE où sera créée la base de données.

Ces paramètres ne sont ni plus ni moins que les options de la commande CREATE DATABASE de PostgreSQL.

De nouvelles possibilités sont offertes avec la version 1.28 de l’opérateur. Le CRD Database a été modifié pour prendre en compte la création de Foreign Data Wrappers de manière déclarative avec les sections spec.fdws et spec.servers.

Si la création de la ressource ne peut se faire, par exemple si le rôle n’existe pas dans l’instance, une erreur sera retournée. Elle pourra être visible dans la description de la ressource Database. Si le rôle est créé après la création de la Database, la boucle de réconciliation fera en sorte de créer la base de données dès que possible.

La suppression d’un objet Database ne supprime pas le base de données dans l’instance ciblée.

Le renommage d’une base de données n’est pas possible.


Schema

  • Pas de Custom Resource Definition
  • Déclaré dans la ressource Database
  • spec.schemas
apiVersion: postgresql.cnpg.io/v1
kind: Database
[...]
spec:
  schemas:
  - name: monschema
    owner: moi
    ensure: present

La création d’un schéma dans une base de données se fait avec l’instruction CREATE SCHEMA, mais vous pouvez aussi le faire en l’indiquant dans le fichier YAML de l’objet Database.

apiVersion: postgresql.cnpg.io/v1
kind: Database
[...]
spec:
  schemas:
  - name: monschema
    owner: moi
    ensure: present

ensure peut prendre la valeur present ou absent. Dans le deuxième cas de figure, CloudNativePG va faire en sorte que le schéma ne soit pas présent et le supprimera s’il existe ! Attention au nom que vous renseignez.


Extensions au sein d’une Database

  • Indiquées dans le YAML (v1.26)
  • Présentes dans l’image utilisée
  • spec.extensions de l’objet Database
apiVersion: postgresql.cnpg.io/v1
kind: Database
[...]
spec:
  extensions:
  - name: vector
    ensure: present

Suivant le même principe que pour les schémas, l’ajout d’une extension peut se faire avec l’instruction CREATE EXTENSION. Vous pouvez le faire à la main lorsque vous êtes connectés à la base, ou alors le demander à CloudNativePG lors de la création de l’objet Database. Pour cela, indiquer les extensions que vous souhaitez en les renseignant dans la partie spec.extensions. Par exemple :

apiVersion: postgresql.cnpg.io/v1
kind: Database
[...]
spec:
  extensions:
  - name: vector
    ensure: present

D’autres paramètres peuvent être utilisés pour indiquer la version de l’extension à installer ou le schéma dans lequel elle doit l’être.

Les fichiers de l’extensions doivent évidement être présents dans l’image du conteneur. Certaines sont embarquées dans l’image fournie par CloudNativePG, d’autres non. Voyons omment intégrer de nouvelles extension avec le mécanisme d’ajout dynamique (v1.27).


Ajout dynamique d’extensions

  • Version 1.27 de l’opérateur
  • Version 18 de PostgreSQL
    • Nouveau paramètre GUC extension_control_path
  • Images externes à gérer
  • Peuvent être ajoutées après le déploiement
spec:
  postgresql:
    extensions:
      - name: ext
        image:
          reference: maregistry/monimage:tag # image dédiée

Jusqu’à présent, une extension devait nécessairement se trouver dans l’image utilisée pour déployer PostgreSQL. CloudNativePG en embarquait par défaut et ils nous était possible d’en ajouter en créant des images personnalisées.

À partir de la version 1.27 de l’opérateur et de la version 18 de PostgreSQL, il est possible de tirer parti du chargement dynamique d’une extension. Ce mécanisme repose sur le concept d’ImageVolume de Kubernetes disponible en version 1.33.

Le principe est de décorréler l’image utilisée pour déployer PostgreSQL et celles utilisées pour ajouter un ou des extensions.

Les images utilisées pour ajouter une extension doivent respecter certaines contraintes pour pouvoir être utilisées par CloudNativePG (voir les indications disponibles sur cette page).

Une attention particulière est à apporter sur les versions des extensions utilisées ainsi que sur les distributions utilisées pour ces images : elles doivent être compatibles au niveau système et architecture CPU.


Role

  • Pas de Custom Resource Definition
  • Déclaré dans la ressource Cluster
  • spec.managed.roles
  • Nécessite la création d’un Secret
spec: # Cluster
[]
  managed:
    roles:
    - name: dalibo
      ensure: present
      comment: Support Account
      login: true
      superuser: true
      passwordSecret:
        name: dalibo-password

Les rôles PostgreSQL sont définis directement dans l’objet Cluster, dans la section spec.managed.roles. Il n’existe pas à l’heure actuelle de Custom Resource rôles.

  • roles: est la liste des rôles qui doivent être présents dans l’instance ;
    • name : est évidemment le nom du rôle. N’oubliez pas le - en début de ligne indiquant qu’il s’agit d’un nouvel élément de la liste ;
    • ensure : positionné à present (valeur par défaut), l’opérateur vérifiera si le rôle existe et si ce n’est pas le cas, le créera. Le comportement est l’inverse s’il est positionné à absent ;
    • comment: simple champ commentaire ajouté au rôle si renseigné ;
    • login: indique si le rôle peut se connecter (true, valeur par défaut), false sinon ;
    • superuser: indique si le rôle est un super-utilisateur, false (valeur par défaut) ;
    • passwordSecret.name : indique le nom du Secret Kubernetes ou se trouve le mot de passe du rôle.

Vous pouvez trouver de manière détaillée les autres paramètres sur cette page. Voici d’autres paramètres de la section spec.managed.roles qui nous semblent intéressants :

  • validUntil : la date à partir de laquelle le rôle ne sera plus valide (exemple validUntil: "2025-06-17T15:00:00Z" );
  • inRoles : la liste des rôles auxquels le rôle créé doit appartenir ;
  • replication : indique si le rôle doit avoir le rôle REPLICATION, par défaut à false par défaut.

Il est possible d’indiquer le mot de passe que doit avoir un nouveau rôle. Pour cela, il faut utiliser le paramètre passwordSecret.name qui doit contenir le nom d’un objet Secret dans le cluster Kubernetes. Il s’agit donc d’un pré-requis à la création d’un rôle avec mot de passe.

Dans l’exemple de la slide, le Secret dalibo-password doit être créé en amont.

apiVersion: v1
kind: Secret
type: kubernetes.io/basic-auth
metadata:
  name: dalibo-password
  labels:
    cnpg.io/reload: "true"
data:
  username: ZGFsaWJv
  password: Q0hBTkdFTUU=

Les champs username et password doivent contenir les valeurs encodées en BASE64. Cela peut se faire en ligne de commande avec printf et base64 :

$ printf "dalibo" | base64
ZGFsaWJv
$ printf "CHANGEME" | base64
Q0hBTkdFTUU=

Tablespace

  • Pas de Custom Resource Definition
  • Déclaré dans la ressource Cluster
  • spec.tablespaces
spec: # Cluster
[]
  tablespaces:
    - name: data
      storage:
        size: 1Gi
      owner: dalibo
    - name: fast
      storage:
        size: 2Gi
        storageClass: fast
      owner:
        name: dalibo

À l’image des rôles, les tablespaces sont également déclarés dans la ressource Cluster. Il est possible de renseigner une liste de tablespaces et de configurer chacun d’eux de manière spécifique.

  • tablespaces: est la liste des tablespaces qui doivent être créés ;
    • name : est évidemment le nom du tablespace. N’oubliez pas le - en début de ligne qui indique qu’il s’agit d’un nouvel élément de la liste ;
    • storage: contient la configuration au niveau du stockage du tablespace ;
      • size: la taille du volume où sera créé le tablespace ;
      • storageClass: indique quelle classe de stockage doit être utilisée pour ce volume-là ;
    • owner : indique le nom du propriétaire de ce tablespace;
    • temporary : permet d’indiquer si le tablespace doit être créé avec la clause TEMPORARY.

Si le propriétaire du tablespace n’est pas renseigné, l’utilisateur app le sera par défaut. Par défaut aussi, le tablespace est créé avec le paramètre temporary à false.

L’option storageClass est particulièrement intéressante pour configurer la classe de stockage sous-jacente au tablespace et donc, in fine, au volume. Cela vous permet d’utiliser des classes de stockage plus ou moins rapides selon vos besoins.

Lorsque le Cluster est créé, ou modifié, l’opérateur se charge de créer les objets Persistant Volumes / Persistant Volume Claims nécessaires et les associe au Pod de l’instance. Par exemple, lors d’un premier déploiement sans tablespace supplémentaire nous sommes dans la situation suivante avec un seul PVC et un seul PV.

kubectl get pvc
NAME                    STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
postgresql-1            Bound    pvc-b0eb7368-0795-48ea-89be-88475dc2a486   2Gi        RWO            standard       <unset>                 3m8s

kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                           STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pvc-67dda23b-5b3c-4e15-950d-681db723d62f   1Gi        RWO            Delete           Bound    default/postgresql-1-tbs-data   standard       <unset>                          3m10s

Après l’ajout des deux tablespaces, la situation est la suivante, avec trois PVC et trois PV. Le premier couple PV / PVC correspond au tablespace par défaut.

kubectl get pvc     
NAME                    STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
postgresql-1            Bound    pvc-b0eb7368-0795-48ea-89be-88475dc2a486   2Gi        RWO            standard       <unset>                 6m41s
postgresql-1-tbs-data   Bound    pvc-67dda23b-5b3c-4e15-950d-681db723d62f   1Gi        RWO            standard       <unset>                 4m4s
postgresql-1-tbs-fast   Bound    pvc-934a3ab9-123e-481c-af44-d3b741961859   2Gi        RWO            standard       <unset>                 4m4s

kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                           STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pvc-67dda23b-5b3c-4e15-950d-681db723d62f   1Gi        RWO            Delete           Bound    default/postgresql-1-tbs-data   standard       <unset>                          3m59s
pvc-934a3ab9-123e-481c-af44-d3b741961859   2Gi        RWO            Delete           Bound    default/postgresql-1-tbs-fast   standard       <unset>                          3m59s
pvc-b0eb7368-0795-48ea-89be-88475dc2a486   2Gi        RWO            Delete           Bound    default/postgresql-1            standard       <unset>                          6m46s

Lors de l’ajout d’un ou de plusieurs tablespaces dans un Cluster en fonctionnement, le Pod PostgreSQL est redémarré, générant ainsi une interruption de service.


Configuration de l’instance

  • Déclaratif, tout se fait en YAML
  • Accès direct à ces fichiers interdit !
    • postgresql.conf
    • pg_hba.conf
    • pg_ident.conf

Comme vous le savez, installer une instance PostgreSQL ne suffit pas. Il faut en plus la configurer. Généralement, la configuration se fait dans le fichier postgresql.conf et nécessite soit un rechargement, soit un redémarrage de l’instance selon le paramètre modifié.

Avec l’utilisation de CloudNativePG, il n’est plus possible de modifier directement le fichier de configuration. Tout se fait dans la définition YAML de l’instance. Voyons quels sont les changements auxquels s’attendre.


postgresql.conf

  • Tous les paramètres ne sont pas modifiables
  • ALTER SYSTEM désactivé
    • allow_alter_system à false
  • Des vérifications sont mises en place
  • Redémarrage ou rechargement automatique
    • Un garde-fou existe (primaryUpdateStrategy)

Voici par exemple comment passer le paramètre shared_buffersà 1GB, et activer la compression des journaux de transactions.

[]
spec:
  postgresql:
    parameters:
      shared_buffers: "1GB"
      wal_compression: "on"
[]

La configuration d’une instance se fait dans la section .spec.postgresql.parameters de la définition YAML du Cluster. Les noms des paramètres sont les mêmes que ceux de PostgreSQL. CloudNativePG ne les renomme pas d’une manière ou d’une autre.

Un bon nombre de paramètres PostgreSQL ne sont pas modifiables dans la section .spec.postgresql.parameters de l’instance. La liste est disponible dans la documentation.

Cette interdiction peut paraitre surprenante, d’autant plus qu’avec un logiciel open source et libre comme l’est PostgreSQL, nous nous attendons plutôt à être libres de nos mouvements. Le parti pris par les développeurs est que tous ces paramètres sont liés à des fonctionnalités dont l’opérateur a la charge (sauvegarde, réplication, archivage des journaux, gestion des traces, etc).

Certains d’entre eux, comme allow_alter_system, sont modifiables par l’utilisation d’éléments présents dans d’autres sections de configuration, comme par exemple enableAlterSystem, qui se trouve dans la section .spec.postgresql.

Des vérifications sont faites sur certains. Pour reprendre l’exemple de shared_buffers, si vous renseignez une valeur plus grande que resources.requests.memory (allouée au Pod), vous lèverez une alerte, et votre modification ne sera pas appliquée.

Exemple de message d’erreur remonté :

The Cluster "postgresql" is invalid: spec.resources.requests.memory:
Invalid value: "512Mi": Memory request is lower than PostgreSQL `shared_buffers` value

Vous n’êtes pas sans savoir que la modification de certains paramètres nécessite soit un rechargement de la configuration, soit un redémarrage de l’instance.

Par défaut l’opération de rechargement ou de redémarrage est déclenchée automatiquement lorsque le nouveau paramètre est appliqué. Concrêtement, si vous modifiez max_connections, vos instances seront automatiquement redémarrées.

Ce dernier point sera détaillé lorsque l’on traitera de la stratégie de mise à jour que vous voulez mettre en place avec le paramètre primaryUpdateStrategy.


pg_hba.conf et pg_ident.conf

  • Pré-configurés
    • FIXED RULES, DEFAULT-RULES
  • Dans la définition du Cluster
    • USER-DEFINED RULES
[]
postgresql:
  pg_hba:
    - host app 10.244.0.0/16 scram-sha-256
[]

Il existe trois sections dans le fichier pg_hba.conf :

  • FIXED RULES : qui sont des règles fixées par l’opérateur. On retrouve une règle concernant la réplication par exemple ;
  • USER-DEFINED RULES : qui correspond aux règles qui seront créées par les administrateurs ;
  • DEFAULT RULES : qui autorise par défaut toutes les connexions par mot de passe à toutes les bases de données.

Il existe deux sections dans le fichier pg_ident.conf :

  • FIXED RULES : qui sont des règles fixées par l’opérateur. On retrouve une ligne concernant l’association de l’utilisateur système postgres au rôle postgres ;
  • USER-DEFINED RULES : qui correspond aux règles qui seront créées par les administrateurs.

⤿ Travaux pratiques

  • Déploiement d’instances PostgreSQL
  • Éléments initiaux

Administration de l’instance


Administration de l’instance

  • Plugin kubectl
  • Connexion
  • Traces
  • Réplication
  • Archivage
  • Sauvegarde
  • Restauration
  • Montée de version de PostgreSQL
  • Montée de version de CloudNativePG
  • Haute disponibilité et bascule
  • Hibernation et fencing

Plugin kubectl

  • kubectl cnpg --help
  • Ligne de commande écrite en Go
  • Interaction avec l’opérateur, un Cluster, une instance spécifique
  • Commandes :
    • status
    • psql
    • promote
    • backup
    • logs
    • reload
    • restart

Un plugin CloudNativePG (cnpg) existe pour l’outil kubectl. Il permet d’obtenir très simplement un ensemble d’informations sur un cluster PostgreSQL ou de se connecter à une instance, déclencher une sauvegarde, promouvoir un secondaire en primaire, etc. Il est écrit en Go et se base, comme le code de l’opérateur, sur le framework Cobra.

Il existe plusieurs méthodes d’installation : par script, par paquets .rpm ou .deb ou encore via krew ou homebrew. À vous de choisir ce qui convient le mieux à vos utilisateurs et administrateurs, qu’ils soient sur Linux, MacOS ou Windows.

La liste des fonctionnalités offertes est assez longue. Elles couvrent des thèmes très variés allant de la simple connexion psql à une instance, à la création de publication pour des réplications logiques ou encore le déclenchement de tests de charges avec fio ou pgbench. Voici quelques unes des commandes qui paraissent essentielles à connaitre dans un premier temps :

  • kubectl cnpg status [cluster] : génère un résumé sur l’état du cluster PostgreSQL (nombre d’instances, sauvegardes, secondaires, réplications…) ;
  • kubectl cnpg psql [cluster] : permet de se connecter avec psql à l’instance primaire ;
  • kubectl cnpg logs [cluster] : affiche les traces PostgreSQL. La commande pretty permet d’afficher les traces de manière plus lisible. L’outil jq peut également s’avérer utile ;
  • kubectl cnpg reload [cluster] : déclenche une boucle de réconciliation pour prendre en compte les modifications apportées au cluster PostgreSQL ;
  • kubectl cnpg restart [cluster] [node] : redémarre soit le cluster en entier, soit une seule instance si [node] est renseigné ;
  • kubectl cnpg promote [cluster] [node] : promeut l’instance indiquée comme nouvelle primaire ;
  • kubectl cnpg backup [cluster] : déclenche une sauvegarde physique de l’instance mentionnée. La configuration de la sauvegarde doit être faite dans la définition du cluster.

Connexion

  • Plus d’accès au serveur
    • Comme avec du PGaaS
  • Question d’accessibilité
    • en interne
    • depuis l’extérieur
  • Offuscation des adresses IP des les traces PostgreSQL
    • %h du paramètre log_line_prefix

En embarquant PostgreSQL dans un conteneur et dans Kubernetes, il ne vous sera plus possible d’accéder au serveur sur lequel est installé PostgreSQL. Cela vous demandera davantage de configuration et de connaissances (notamment sur les différentes couches qui existent dans Kubernetes).

Vous n’aurez plus accès au serveur sous-jacent, comme ce serait le cas avec une solution de PGaaS. Si vous gérez vous même votre cluster Kubernetes, il vous sera évidemment possible d’accéder aux nœuds Workers.

Il faut différencier deux types d’accès à une instance PostgreSQL :

  1. les accès inités depuis un client déployé dans le cluster Kubernetes (interne) ;
  2. et ceux initiés depuis un client en dehors du cluster Kubernetes (externe).

Dans le premier cas, l’application pourra accéder à l’instance PostgreSQL si les Network Policies l’autorisent. Dans le second cas, vos administrateurs Kubernetes devront mettre en place un Load Balancer en frontal du cluster pour autoriser les accès externes. Dès lors que votre instance est accessible depuis l’extérieur (interface et port exposés), vous pourrez vous y connecter avec psql par exemple. Dans le cas contraire, vous ne pourrez pas vous y connecter directement.

L’outil kubectl permet à des utilisateurs de se connecter à une instance spécifique. La commande kubectl exec permet d’exécuter une commande dans un conteneur. Par « chance », l’image utilisée pour déployer PostgreSQL contient psql. Dès lors que vous avez accès au cluster Kubernetes et que vous avez les bons droits pour le faire, kubectl exec -it POD CONTAINER -- psql vous permet de vous connecter à l’instance avec le rôle postgres.

kubectl exec -it postgresql-1 -c postgres -- psql        
psql (17.2 (Debian 17.2-1.pgdg110+1))
Type "help" for help.

postgres=# 

Le plugin cnpg permet la même chose avec sa commande psql :

kubectl cnpg psql postgresql   
psql (17.2 (Debian 17.2-1.pgdg110+1))
Type "help" for help.

postgres=# 

Au sein du cluster Kubernetes, vous pouvez utile le DNS attribué au Pod ou au Service pour vous connecter à une instance.


Traces

  • Format JSON
  • Un seul flux pour différents logger, pas que PostgreSQL
  • Sortie standard du Pod
  • Certains paramètres log_* non modifiables
  • Outil de centralisation de traces obligatoire (Loki, Fluentd, …)
  • Exploitables par pgBadger

Traces

{
  "level": "info",
  "ts": 1619781249.7188137,
  "logger": "postgres",
  "msg": "record",
  "record": {
    "log_time": "2021-04-30 11:14:09.718 UTC",
    "user_name": "",
  […]
  }
}
  • Parfois emballées dans du JSON

Dès lors que vous déploierez PostgreSQL avec CloudNativePG, les traces que vous connaissez ne seront ni accessibles dans le fichier postgresql.log ni du même format.

Concernant les traces de l’opérateur, vous pouvez gérer le niveau de celles-ci avec l’argument --log-level du Deployment de l’opérateur. Les valeurs error, warning, info, debug et trace sont disponibles. La valeur par défaut est info.

Concernant le(s) Pod(s) PostgreSQL, les traces contiennent les traces de l’instance, mais également les traces d’autres éléments comme celles de l’instance manager ou encore de la solution de sauvegarde. Toutes les traces sont renvoyées sur la sortie standard du Pod au format JSON et sont mélangées. La clé logger de la trace JSON indique qui est responsable de cette ligne. Par exemple, la trace suivante a été générée par l’instance. On peut le voir avec le paramètre logger qui est à postgres.

{
  "level": "info",
  "ts": 1619781249.7188137,
  "logger": "postgres",
  "msg": "record",
  "record": {
    "log_time": "2021-04-30 11:14:09.718 UTC",
    "user_name": "",
  […]
  }
}

Concernant les paramètres de traces PostgreSQL, vous ne pouvez pas modifier les paramètres PostgreSQL suivants, CloudNativePG l’interdit.

log_destination
log_directory
log_file_mode
log_filenameas de `Custom Resource Definition`
log_rotation_age
log_rotation_size
log_truncate_on_rotation
logging_collector

Il est possible de suivre les traces d’un Pod en ligne de commande avec la commande kubectl logs -f <cluster> ou kubectl cnpg logs cluster <cluster> si vous avez installé le plugin cnpg pour kubectl.

Les traces ne sont pas persistées dans le Pod. Il est donc essentiel d’avoir une solution de centralisation des traces pour que vos administrateurs puissent y avoir accès. Des outils comme Loki, Fluentd.

L’outil pgBadger supporte le format des traces générées par CloudNativePG.


Réplication

  • Mise en place facilité
    • instances: N
    • 1 primaire et N-1 secondaires
  • Streaming Replication
  • Répartition sur les nœuds
    • Configuration de l’affinité/anti-affinité
  • Slot de réplication créé par défaut
  • Asynchrone par défaut

Le déploiement d’instances secondaires est très grandement facilité par CloudNativePG. En modifiant uniquement le nombre d’instances dans l’objet Cluster, une ou plusieurs nouvelles instances sera créée et configurée pour suivre le primaire grâce à la réplication physique (Streaming Replication). Un nouveau Pod sera donc créé, reprenant le nom du Cluster suivi d’un chiffre incrémenté de 1 pour chaque nouvelle instance. Avec la sortie de la commande kubectl get pod suivante, nous pouvons comprendre qu’au sein du cluster Kubernetes, deux Pods PostgreSQL existent et appartiennent au même Cluster (au sens de CloudNativePG) nommé postgresql.

kubectl get pod
NAME                   READY   STATUS    RESTARTS   AGE
postgresql-1           1/1     Running   0          14h
postgresql-2           1/1     Running   0          7h

Le chiffre qui suit le nom du cluster, ici 1 et 2, n’indique PAS le rôle de l’instance (primaire ou secondaire). Se baser sur ce chiffre pour connaitre le rôle d’une instance est une erreur.

Il existe plusieurs méthodes pour retrouver l’instance primaire d’un cluster. Par exemple :

kubectl get cluster postgresql                                           
NAME         AGE   INSTANCES   READY   STATUS                     PRIMARY
postgresql   14h   2           2       Cluster in healthy state   postgresql-1

L’opérateur et la configuration de base font en sorte que les instances secondaires soient réparties, si cela est possible, sur les différents workers qui composent le cluster Kubernetes. L’idée est de ne pas déployer au même endroit toutes les instances, auquel cas, en cas de panne, l’intégralité du Cluster PostgreSQL serait perdu.

Un slot de réplication sera automatiquement créé pour chaque nouveau secondaire. Le principal avantage est de ne plus avoir de décrochage du secondaire en conservant les journaux de transactions nécessaires. La conséquence directe est le risque d’accumulation de ces mêmes journaux qui pourraient saturer l’espace disque du primaire. Le nom du slot de réplication est automatiquement généré avec cnpg et le nom de l’instance. Voici un extrait de la vue pg_stat_replication_slot qui montre cela.

-[ RECORD 1 ]-------+-------------------
slot_name           | _cnpg_postgresql_2
plugin              | 
slot_type           | physical
[…]

Vous trouverez davantage d’informations sur le concept de slot de réplication dans notre module W2B.

Par défaut, la réplication mise en place est asynchrone. Il est possible de mettre en place de la réplication synchrone. Cette fonctionnalité sera traitée dans un second module de formation.


⤿ Travaux pratiques

  • Déploiement d’une instance secondaire
  • Tests de bascules

Archivage et Sauvegarde - Prérequis

  • Changement de méthode (v1.26)
  • Nécessite un plugin
  • Barman Cloud Plugin
    • Nouvelle CRD ObjectStore

La version 1.26 de l’opérateur apporte un changement majeure dans la mise en place de l’archivage et des sauvegardes des instances. Il est désormais nécessaire d’utiliser un plugin pour gérer cette partie. L’ancienne méthode, qui consistait à tout définir dans l’objet Cluster est considérée comme obsolète et disparaitra avec la version 1.28. Un message d’erreur est remonté en cas d’utilisation :

Warning: Native support for Barman Cloud backups and recovery is deprecated and will be completely removed in CloudNativePG 1.28.0. Found usage in: spec.backup.barmanObjectStore. Please migrate existing _cluster_s to the new Barman Cloud Plugin to ensure a smooth transition.

L’intérêt est de laisser la possibilité d’utiliser d’autres outils de sauvegarde, sans devoir les intégrer aux images de conteneur. Pour les plus connaisseurs, c’est un conteneur sidecar qui sera créé à côté du conteneur PostgreSQL. On peut notamment penser à pgBackRest. Cet [article(https://blog.dalibo.com/2025/04/03/cnpg-6.html)] sur notre blog présente d’ailleurs de cette possibilité.

Le projet maintient le plugin Barman Cloud Plugin. C’est celui-que nous utiliserons pour la présentation et les travaux pratiques. Il peut tout à fait être utilisé en production. Aussi, chaque plugin aura ses propres prérequis. Référez-vous à sa documentation d’installation pour les connaitre.

En ce qui concernant Barman Cloud Plugin, il doit être installé dans le même Namespace que l’opérateur et nécessite que cert-manager soit installé dans le cluster Kubernetes. Il apporte une nouvelle ressource : ObjectStore.


Plugin de sauvegarde

  • Indiqué dans le Cluster
  plugins:
  - name: barman-cloud.cloudnative-pg.io
    isWALArchiver: true
    parameters:
      barmanObjectName: objectstore-demo  

ObjectStore - Barman Cloud

  • Emplacement de stockage
  • Stockage objet
    • Amazon S3 (ou compatible S3)
    • Google Cloud Storage
    • Microsoft Azure Blob Storage
  • Informations de connexion
  • Réutilisable

ObjectStore - Barman Cloud

apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
  name: scaleway-store
spec:
  configuration:
      destinationPath: "s3://<bucket>/<folder>/"
      endpointURL: "https://s3.<region>.scw.cloud" 
      s3Credentials:
        accessKeyId:
          name: scaleway-api-secret
          key: ACCESS_KEY_ID
        secretAccessKey:
          name: scaleway-api-secret
          key: ACCESS_SECRET_KEY
        region:
          name: scaleway-api-secret
          key: ACCESS_REGION

Cette ressource est fournie par Barman Cloud Plugin. Elle représente un emplacement de stockage objet. Trois providers sont supportés : Amazon S3, Microsoft Azure Blob Storage ou encore Google Cloud Storage. Des services compatibles S3 peuvent être également utilisés comme MinIO ou encore Scaleway Object Storage.

D’un fournisseur de stockage à un autre, les champs destinationPath et endpointURL peuvent changer. En plus de la connaissance du point d’entrée de votre solution de stockage, vous devez renseigner les informations de connexion dans la section s3Credentials.

Ces informations là doivent être enregistrées dans un objet Secret (objet Kubernetes). Dans l’exemple, le nom de ce Secret est scaleway-api-secret. Voici ce à quoi il pourrait ressembler.

apiVersion: v1
kind: Secret
metadata: 
  name: scaleway-api-secret
type: Opaque
data:
  ACCESS_KEY_ID: bWEgY2xlIGQgYWNjZXMK
  ACCESS_REGION: ZnItcGFy
  ACCESS_SECRET_KEY: bW9uIHNlY3JldCBiaWVuIGdhcmRlCg==

Les valeurs de chacun des trois champs data doivent être encodées en BASE64.


Archivage

  • Activé par défaut (archive_mode à on)
  • archive_command :
    • /controller/manager wal-archive …
  • Non modifiable
  • Nécessite un stockage de type S3
  • Utilisé pour les sauvegardes PITR
  • Se repose sur le plugin (isWALArchiver)

L’archivage des journaux de transaction est fortement conseillé lorsque qu’il est question de sauvegarde physique et permet notamment la mise en place de sauvegardes dites PITR (voir notre module I2).

CloudNativePG supporte ce mécanisme par défaut. L’archivage des journaux se fait via l’intermédiaire d’un plugin. Le choix du plugin reste libre.

L’archive ne peut se faire que sur un stockage de type objet (type S3, Google Cloud Storage, Azure Blob Storage ou MinIO). C’est le cas quelque soit la méthode de sauvegarde utilisée.

Il est nécessaire de créer une ressource ObjectStore au sein du cluster Kubernetes qui sera réutilisée plus tard par les objets Cluster PostgreSQL.

Pour configurer l’archivage sur un Cluster, il est demandé de renseigner spec.plugins avec le nom du plugin, s’il a la capacité d’archiver des journaux (isWALArchiver) et l’ObjectStore sur lequel seront envoyés les journaux. Par exemple :

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
[...]
spec:
  plugins:
  - name: barman-cloud.cloudnative-pg.io
    isWALArchiver: true
    parameters:
      barmanObjectName: scaleway-store

Le paramètre archive_command de l’instance PostgreSQL sera automatiquement mise à jour.


Sauvegarde

  • Sauvegarde physique uniquement
  • 2 méthodes
    • Volume Snapshot
    • Object Storage
  • Nouvelles CRD Backup et ScheduledBackup
  • Configuration spec.backup d’un objet Cluster

L’opérateur sait gérer pour vous des sauvegardes dites physiques. C’est le seul type de sauvegarde que vous allez pouvoir déclencher via l’opérateur. Autrement dit, il existe une nouvelle ressource Kubernetes, appelée Backup qui correspond à une sauvegarde physique d’un Cluster PostgreSQL.

Les sauvegardes logiques (faites avec pg_dump ou pg_dumpall par exemple) pourront toujours se faire avec ces outils dès lors que votre instance est accessible.

Plusieurs méthodes de sauvegarde existent. Quelle que soit la méthode, la configuration se fait dans l’objet Cluster correspondant à vos instances. Certains paramètres peuvent également être renseignés dans les objets Backup ou ScheduledBackup. C’est ce que nous allons découvrir par la suite.

Aussi, vous pourrez effectuer ces sauvegardes à partir des instances secondaires pour décharger les instances primaires, et ce, quelle que soit la méthode choisie.


Méthode par stockage objets

  • Méthode par défaut
  • Nécessite un stockage de type S3
  • Sauvegarde à chaud, PITR
spec:
  plugins:
  - name: barman-cloud.cloudnative-pg.io
    isWALArchiver: true
    parameters:
      barmanObjectName: scaleway-store

La première méthode consiste à effectuer les sauvegardes sur un stockage objet de type S3, Azure Blob Storage, Google Cloud Storage ou MinIO. Cette méthode se repose à l’heure actuelle sur l’outil Barman Cloud. C’est le même fonctionnement que pour l’archivage.

Couplé avec l’archivage des journaux de transactions, vous obtenez une sauvegarde PITR fonctionnelle pour votre instance.


Méthode par Volume Snapshot

  • Fonctionnalité Kubernetes (API)
  • spec.backup.volumeSnapshot
  • Dépend de
    • StorageClass
    • Container Storage Interface
  • Sauvegarde à chaud ou à froid

La seconde méthode utilise quant à elle un mécanisme propre à l’API de Kubernetes, le Volume Snapshot. C’est une fonctionnalité qui doit être supportée par la Storage Class (et donc in fine par le CSI) avec laquelle les volumes de votre instance ont été créés.

Cette méthode va créer un objet Volume Snapshot dans votre cluster Kubernetes qui contiendra un instantané du Persistent Volume ciblé. Cette sauvegarde sera donc locale à votre système de stockage et non plus envoyée sur un stockage objet. Un snapshot sera créé pour chaque volume de votre instance (storage et walStorage).

Des mécanismes plus complexes comme les sauvegardes incrémentales ou différentielles sont possibles si la Storage Class le permet.

Couplé avec l’archivage des journaux de transactions, vous obtenez une sauvegarde PITR fonctionnelle pour votre instance. Les journaux de transaction sont quant à eux stockés sur un stockage objet.


Ressource Backup

  • Custom Resource Definition
  • Définit l’exécution d’une sauvegarde
apiVersion: postgresql.cnpg.io/v1
kind: Backup
metadata:
  name: masauvegarde
spec:
  cluster:
    name: postgresql

Voici un exemple d’un objet Backup qui, une fois créé dans le cluster Kubernetes, déclenchera une sauvegarde physique du Cluster nommé postgresql. Si rien n’est mentionné, la sauvegarde se fera sur un stockage objet.

Il existe d’autres paramètres de configuration, comme :

  • target : qui indique à partir de quelle instance doit être faite la sauvegarde ;
    • prefer-standby : pour demander à la faire depuis un secondaire ;
    • primary : pour demander à la faire depuis le primaire.
  • method : permet de définir par quel moyen la sauvegarde physique doit être faite ;
    • barmanObjectStore : avec l’outil Barman Cloud sur un stockage objet (type S3, Google Cloud Storage, Azure Blob Storage ou MinIO). C’est le choix par défaut ;
    • volumeSnapshot : en se basant sur la fonctionnalité de Volume Snapshot. Votre CSI doit supporter cette fonctionnalité pour utiliser cette méthode ;
    • plugin : si la sauvegarde se fait à partir d’un plugin autre que vous aurez déployé au préalable. Fonctionnalité encore en test.

Pour les sauvegardes qui utilisent la méthode barmanObjectStore, les informations sur l’emplacement de stockage seront reprises de la configuration spec.backup.barmanObjectStore du Cluster référencé dans le champ spec.cluster.name. Il y aura deux dossiers, un contenant les journaux archivés, un contenant les sauvegardes.

Pour les sauvegardes qui utilisent la méthode volumeSnapshot, la configuration sera récupérée depuis spec.backup.volumeSnapshot.

Par défaut, les sauvegardes se font à chaud. Seule la méthode par Volume Snapshot permet de faire des sauvegardes à froid (i.e instance arrêtée).


Ressource ScheduledBackup

  • Custom Resource Definition
  • Définit la planification de sauvegardes
  • Une ressource Backup créée à chaque exécution
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
  name: masauvegardequotidienne
spec:
  schedule: "0 0 20 * * *"
  backupOwnerReference: self
  cluster:
    name: postgresql

Il existe une deuxième ressource appelée ScheduledBackup qui, comme son nom l’indique, permet de déclencher une sauvegarde régulièrement. L’exemple donné correspond au déclenchement d’une sauvegarde quotidienne à 20h00:00.

Quelques paramètres de configuration sont spécifiques à cet objet, comme :

  • schedule : définit le moment où la sauvegarde sera déclenchée. Il y a bien six arguments, correspondant aux secondes, minutes, heures, jours du mois, mois et jour de la semaine ;
  • backupOwnerReference : indique à quelle ressource sera rattachée cette sauvegarde ;
    • self : au ScheduledBackup qui a déclenché cette sauvegarde ;
    • cluster : au Cluster mentionné ;
    • none: à aucune ressource.

D’autres paramètres comme method ou target peuvent être renseignés dans un ScheduledBackup.

Le paramètre backupOwnerReference a un impact sur la manière dont sont conservés les objets Kubernetes Backup. Dans l’exemple ci-dessus, si l’objet ScheduledBackup masauvegardequotidienne est supprimé, tous les objets Backup qui auraient été créés par la sauvegarde planifiée seront supprimés. Dans le cas où backupOwnerReference est configuré à cluster, les objets Backup seront supprimés si l’objet Cluster est supprimé. À none ils seront tout le temps conservés. Notez bien qu’il s’agit bien des objets Kubernetes. Les sauvegardes qui se trouvent sur le stockage S3 par exemple, ne seront pas supprimées.

Il est tout à fait possible de combiner ces deux types de déclenchements (manuel ou régulier) de sauvegardes.


⤿ Travaux pratiques

  • Mise en place d’une sauvegarde PITR

Restauration

  • Création d’une nouvelle instance
  • À partir d’une sauvegarde physique
    • spec.bootstrap.recovery
      • barmanObjectStore
      • volumeSnapshots
  • PITR supporté
  • Indiquer le plugin à utiliser

La première chose à noter est qu’une restauration d’une instance avec CloudNativePG se fera toujours par la création d’une nouvelle instance. Autrement dit, il n’est pas possible de faire une restauration sur une instance déjà déployée. La restauration in-place n’est pas possible.

CloudNativePG se base sur une sauvegarde physique pour créer cette nouvelle instance. Le paramètre de configuration spec.bootstrap.recovery permet de configurer cette restauration. Lorsque ce paramétre est utilisée dans le fichier YAML, CloudNativePG comprend qu’il doit créer (bootstrap) une instance à partir d’une sauvegarde.

Comme l’archive_command, le paramètre restore_command est déjà positionné par l’opérateur et utilise l’instance-manager. Cette fois-ci c’est la commande wal-restore qui est utilisée.

/controller/manager wal-restore --log-destination /controller/log/postgres.json %f %p

La source utilisée pour la restauration peut être de nature différente selon la méthode (method) de la sauvegarde.

  • Si vous repartez d’une sauvegarde faite sur un stockage objets, utilisez le paramètre bootstrap.recovery.source associé à externalClusters. La partie barmanObjectStore contiendra alors toutes les informations du stockage objets où se trouve la sauvegarde. Par exemple :
  spec:
  []

  bootstrap:
    recovery:
      source: clusterBackup

  externalClusters:
    - name: clusterBackup
      barmanObjectStore:
      []
  • Si vous repartez d’un Volume Snapshot, vous devrez utiliser bootstrap.recovery.volumeSnapshots.storage. Dans le cas où le snapshot a été fait depuis une instance ayant deux espaces de stockage (storage et walStorage) vous devez également le renseigner.

Quelques éléments supplémentaires sont à prendre en compte. Tout d’abord, CloudNativePG part du principe que la base app existe dans la sauvegarde et qu’elle a pour propriétaire app. Si vous utilisez d’autres noms, vous devez le renseigner dans la partie recovery. Aussi, si vous souhaitez conserver un mot de passe en particulier pour le rôle par défaut, vous devez renseigner le Secret à utiliser. Autrement, CloudNativePG se chargera d’en générer un aléatoirement.

Par défaut, la sauvegarde se fera en rejouant l’intégralité des journaux de transactions disponibles sur la dernière timeline. Il est possible de faire une restauration de type Point In Time Recovery en renseignant le champs bootstrap.recovery.recoveryTarget. Par exemple :

[]
      recoveryTarget:
        # Time base target for the recovery
        targetTime: "2023-08-11 11:14:21.00000+02"

D’autres cibles de restauration existent, comme :

  • targetTime : l’horodatage auquel vous souhaitez restaurer votre instance ;
  • targetXID : l’ID de transaction jusqu’auquel vous souhaitez restaurer votre instance ;
  • targetName : le nom du point de restauration que vous aurez créé au préalable avec pg_create_restore_point() ;
  • targetLSN : La position dans les journaux de transactions à laquelle arrêter la restauration ;
  • targetImmediate : Indique si la restauration doit s’arrêter dès qu’un point de consistance est atteint.

⤿ Travaux pratiques

  • Procéder à une restauration PITR

Montée de version d’une instance PostgreSQL

  • Version PostgreSQL indiquée dans l’image
    • Modifier l’image pour monter de version
    • Modifier l’image à utiliser dans lImageCatalog ou ClusterImageCatalog
  • Version mineure
  • Version majeure (v1.26)

Une montée de version consiste à la mise à jour des binaires de PostgreSQL. En mode conteneur, et donc sur Kubernetes, il n’est pas envisageable de les mettre à jour via apt ou yum. Cela se fait en modifiant l’image utilisée dans la définition YAML de votre Cluster` PostgreSQL. Une nouvelle image contenant la nouvelle version souhaitée sera alors téléchargée.

Il faut distinguer deux types de montées de versions d’une instance :

  • Les montées de versions mineures ;
  • Les montées de versions majeurs.

Selon le type de montée de version, le déroulé des étapes sera différent. Voyons en détails ces deux types de montées de version.


Montée de version mineure

  • Version mineure
    • Mode Rolling Update
  • primaryUpdateStrategy et primaryUpdateMethod
    • Définis dans le YAML du Cluster

La modification de l’image entraine une montée de version de toutes les instances du Cluster. Cette montée de version se fera en mode Rolling Update. Les instances secondaires seront les premières à être mises à jour, une par une. L’instance primaire est la dernière à être mise à jour.

La mise à jour de l’instance primaire peut se faire automatiquement ou de manière supervisée selon le paramètre de primaryUpdateStrategy qui peut prendre deux valeurs.

  • unsupervised : après que toutes les instances secondaires aient été mises à jour, l’instance primaire est automatiquement mise à jour. C’est la valeur par défaut ;
  • supervised : le mécanisme de montée de version attend une opération manuelle pour effectuer la montée de version de l’instance primaire.

En mode unsupervised, le paramètre primaryUpdateMethod est pris en compte pour savoir comment procéder à la mise à jour de l’instance primaire. Il peut lui aussi prendre deux valeurs :

  • restart : l’instance primaire est redémarrée. Il faudra attendre la fin de la mise à jour pour que le primaire soit de nouveau accessible. C’est la valeur par défaut ;
  • switchover : une bascule sur un secondaire est effectuée et le primaire devient secondaire. Le nouveau primaire est déjà accessible comme il a déjà été mis à jour.

En mode supervised, vous devrez donc vous même procéder à ce redémarrage ou à ce switchover avec le plugin cnpg de kubectl. Par exemple pour le switchover vous pouvez utiliser la commande suivante pour passer l’instance postgresql-2 en tant que nouvelle primaire :

kubectl cnpg promote postgresql 2

Si au contraire, vous souhaitez procédre à un redémarrage du primaire vous pouvez utiliser la commande restart du plugin.

kubectl cnpg restart postgresql 1

Montée de version majeure

  • Plusieurs méthodes
    • pg_dump/pg_restore
      • microservice ou monolith
    • Réplication logique
      • Plus complexe
    • In-Place Major Upgrade (v1.26)
      • Offline avec pg_upgrade

Changer de version majeure de PostgreSQL est un peu moins trivial qu’une montée de version mineure. Des changements structurels peuvent avoir lieu dans les nouvelles versions et le passage dans une nouvelle version majeure ne peut pas se faire par une simple installation de binaires. Différentes méthodes existent pour mettre à jour une instance. Il existe globalement trois méthodes pour y parvenir :

  • Avec les outils pg_dump / pg_restore ;
  • Avec la réplication logique de PostgreSQL ;
  • Avec l’outil pg_upgrade.

Chaque méthodes a ses avantages et inconvénients. Toutes trois sont supportées par CloudNativePG (comprendre qu’il est possible de les déclenchée de manière déclarative). Voir par exemple cette page de documentation sur l’import de base à la création d’un nouveau Cluster ou alors celle-ci qui explique les quelques étapes à suivre pour la réplication logique.

Depuis la version 1.26 de l’opérateur, il est possible de faire des In-Place Major Upgrade qui permet de mettre à jour une instance sans devoir en recréer d’autres avant. Cette méthode va arrêter toutes les instances (liées au Cluster à mettre à jour) qui existeraient, va utiliser l’outil pg_upgrade et va manipuler les objets Kubernetes (PV, PVC) pour transférer les données présentes dans PGDATA vers un autre emplacement de stockage. Si tout se déroule correctement, les instances secondaires seront recréées entièrement. Avec cette méthode, les noms des ressources Kubernetes sont conservés.


Montée de version de l’opérateur

  • L’opérateur et les Custom Resource Definitions
  • L’instance-manager
  • Redémarrage des instances
  • Étalement des redémarrages dans le temps
    • CLUSTERS_ROLLOUT_DELAY
    • INSTANCES_ROLLOUT_DELAY

La mise à jour de l’opérateur se fait en plusieurs temps et impacte les instances PostgreSQL qu’il gère. Le principe de mise à jour est simple, il suffit d’appliquer les nouveaux manifestes de l’opérateur sur Kubernetes. Si vous avez installé l’opérateur avec Helm, mettez le à jour avec Helm. Même chose pour les autres méthodes de déploiement.

Ces nouveaux manifestes mettent à jour les Custom Resource Definitions (comme Cluster, ScheduledBackup, etc) et l’opérateur en tant que tel, c’est à dire le Pod qui contient le controller.

Cette première étape terminée, la seconde va être automatiquement déclenchée. Elle consiste à la mise à jour de tous les Pods des instances PostgreSQL déployées. En effet, il existe dans le Pod de l’instance, un composant appelé instance-manager. Celui-ci est étroitement lié au controller CloudNativePG. Ils doivent être dans la même version. La mise à jour des instances suit le modèle de Rolling Update que nous verrons plus tard lorsque nous évoquerons la montée de version mineure d’une instance PostgreSQL.

Par défaut toutes les instances gérées par l’opérateur seront mises à jour en même temps. Ceci peut poser problème si vous avez de très nombreuses instances. Il est possible de répartir ces redémarrages dans le temps avec les paramètres :

  • CLUSTERS_ROLLOUT_DELAY : permet de définir un délai entre le redémarrage de deux Cluster différents. Par défaut positionné à 0 ;
  • INSTANCES_ROLLOUT_DELAY : permet de définir un délai entre le redémarrage de deux instances différentes qui font partie du même Cluster. Par défaut positionné à 0 ;

Ces paramètres là sont à définir dans le Configmap de configuration de l’opérateur, à savoir le Configmap cnpg-controller-manager-config qui doit être créé dans le même Namespace que l’opérateur.

Il existe une méthode pour éviter ce comportement (redémarrage des Pods). Cependant elle ne garantit pas le critère immuable que devrait suivre un Pod. À titre d’information, voici la méthode à suivre pour y parvenir. Il faut modifier la configuration de l’opérateur en passant le paramètre ENABLE_INSTANCE_MANAGER_INPLACE_UPDATES à true dans son ConfigMap. L’image du init-container qui contient le manager ne sera pas mis à jour dans les Pods.

Si il y a bien une chose à retenir, c’est qu’une mise à jour de l’opérateur déclenche la mise à jour d’un composant au sein des Pods PostgreSQL. Opération qui nécessite un redémarrage du Pod. Aussi,la configuration primaryUpdateStrategy est également prise en compte dans le cadre d’une montée de version de l’opérateur.


⤿ Travaux pratiques

  • Montée de version mineure de PostgreSQL
  • Montée de version de l’opérateur

Haute disponibilité et bascule

  • Répartition sur les nœuds
  • Bascule
    • Automatique
    • Manuelle

Nous l’avons vu, l’ajout de secondaire se fait très simplement en modifiant le paramètre spec.instances. Par défaut, CloudNativePG essaye de répartir les instances PostgreSQL d’un même Cluster sur des nœuds différents. Cette configuration est modifiable dans la partie spec.affinity de la définition YAML.

Voici quatre paramètres intéressants :

  • enablePodAntiAffinity : par défaut à true, il indique si ce mécanisme d’affinité / anti-affinité doit être utilisé ou non ;
  • topologyKey : indique sur quel paramètre va se reposer la répartition des Pods. Par défaut à kubernetes.io/hostname, le répartition se fera selon le nom du nœud. Il est possible d’utiliser d’autres valeurs comme topology.kubernetes.io/zone pour répartir les Pods non pas selon les nœuds mais sur des zones de disponibilité ;
  • podAntiAffinityType : ce paramètre permet d’indiquer le type d’affinité qui doit être utilisé. À prefered, le scheduler de Kubernetes tente de respecter la règle d’affinité. Si il ne peut pas, il déploiera tout de même le Pod sur un nœud. À required, le Pod sera déployé uniquement si la règle est respectée.
  • nodeSelector : vous permet de sélectionner les nœuds où pourra être déployé un Pod. Les nœuds qui possèdent ces labels seront les cibles potentielles du déploiement.

CloudNativePG gère nativement la bascule, qu’elle soit manuelle ou automatique. Par bascule, on entend la promotion d’un secondaire en primaire. Vous pouvez promouvoir une instance secondaire en primaire grâce au plugin cnpg pour kubectl.

kubectl cnpg promote CLUSTER ID-INSTANCE

Il faut s’avoir qu’à chaque instance est associé un ou plusieurs labels. Celui qui nous intéresse particulièrement est cnpg.io/instanceRole. Chacun des Pod PostgreSQL possède ces labels, qui sont ajoutés automatiquement par l’opérateur CloudNativePG.

Ce label permet donc de connaitre le rôle de l’instance. Les Services Kubernetes, automatiquement créés, permettent de se connecter à un Pod. L’association Service - Pod se fait grâce à ces labels, et plus précisement grâce aux Selector créés sur le Service.

Lorsque la commande de promotion est lancée, en plus d’autres opérations qui sont faites au niveau de PostgreSQL, le label cnpg.io/instanceRole va être mis à jour sur chaque Pod. L’instance qui était jusqu’à présent la secondaire, devient la nouvelle instance primaire. Le label cnpg.io/instanceRole est mis à jour. Ainsi, si vos applications utilisent le Service postgresql-rw (où postgresql est le nom du Cluster) elles devront uniquement initier une nouvelle connexion pour interagir avec la nouvelle instance primaire.

Les deux schémas si dessous montrent ce principe. Entre ces deux schémas, une promotion de l’instance postgresql-1 en tant que nouveau primaire a eu lieu. L’instance postgresql-2 est redevenue secondaire. Le Service postgresql-rw a suivi ce changement grâce au mécanisme de Label - Selector.


Hibernation et fencing

  • Hibernation
    • Arrêter le Pod en conservant les volumes
    • Déclarative ou via le plugin cnpg
  • Fencing
    • Arrêter uniquement le service postmaster

Le principe d’hibernation vous permet d’arrêter un Cluster PostgreSQL tout en conservant les volumes de données. Les Pods PostgreSQL seront arrêtés un à un en commençant par le primaire. Les PVC et PV de chaque instance, qu’elle soit primaire ou secondaire, existeront toujours.

La première méthode pour passer un Cluster en hibernation est de lui ajouter l’annotation cnpg.io/hibernation=on, avec par exemple, la commande suivante :

kubectl annotate cluster <cluster-name> --overwrite cnpg.io/hibernation=on

Voici un exemple de situation lorsqu’une instance est en hibernation.

$ kubectl get pod
No resources found in default namespace.

$ kubectl get pvc
NAME           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
postgresql-1   Bound    pvc-bb811ce2-c4db-4f3d-8286-1187bcd91174   2Gi        RWO            standard       <unset>                 7m30s
postgresql-2   Bound    pvc-b8b35ea1-073c-4dd9-8ff1-3fc9d6f60418   2Gi        RWO            standard       <unset>                 2m2s

Il n’y a plus de Pod mais les PVC sont bien encore présents. Si vous souhaitez redéployer les Pods (primaire et secondaire) du Cluster, utilisez la même commande avec l’option off cette fois-ci.

Il est également possible d’hiberner un Cluster avec le plugin cnpg de kubectl. Une différence notable existe entre ces deux méthodes. Avec cette deuxième méthode, seul le PVC de l’instance primaire est conservé.

$ kubectl cnpg hibernate on postgresql                                   
hibernation process starting...
waiting for the cluster to be fenced
cluster is now fenced, storing primary pg_controldata output
primary pg_controldata output fetched
annotating the PVC with the cluster manifest
PVC annotation complete
destroying the primary instance while preserving the pvc
Instance postgresql-1 of cluster postgresql has been destroyed and the PVC was kept
primary instance destroy completed
deleting the cluster resource
cluster resource deletion complete
Hibernation completed
$ kubectl get pvc
NAME           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
postgresql-1   Bound    pvc-bb811ce2-c4db-4f3d-8286-1187bcd91174   2Gi        RWO            standard       <unset>                 10m

Lorsque le Cluster sortira d’hibernation (avec kubectl cnpg hibernate off postgresql) les PVCs nécessaires aux secondaires seront recréés. Selon la volumétrie des instances, cette copie pourra représenter beaucoup de volume et de temps.

Le fencing quant à lui permet d’arrêter le service postmaster de PostgreSQL sans pour autant arrêter le Pod. Cela revient à faire une arrêt propre de l’instance PostgreSQL au sein du Pod.

Il est possible de passer en fencing une instance spécifique (qu’elle soit primaire ou secondaire), une liste d’instances, ou toutes les instances d’un Cluster. Là encore, ce mécanisme est géré par une annotation, en l’occurence cnpg.io/fencedInstances. Par exemple, avec :

  • cnpg.io/fencedInstances: '["postgresql-1"]', seule cette instance sera en fencing  ;
  • cnpg.io/fencedInstances: '["postgresql-1","postgresql-3"]', ces deux instances seront passées en fencing;
  • cnpg.io/fencedInstances: '["*"]', toutes les instances du Cluster qui seront annotés passeront en fencing.

Vous pouvez, soit utiliser la commande kubectl annotate … soit le plugin cnpg avec par exemple kubectl cnpg fencing on postgresql 1.

Lorsqu’une instance est passée en fencing, le Pod ne sera plus marqué READY, comme le montre cet exemple.

$ kubectl cnpg fencing on postgresql 1
postgresql-1 fenced
$ kubectl get pod
NAME           READY   STATUS    RESTARTS   AGE
postgresql-1   0/1     Running   0          19m
postgresql-2   1/1     Running   0          19m

Le Pod est toujours accessible, permettant par exemple de procéder à du débogage.


⤿ Travaux pratiques

  • Exercices optionnels

Conclusion

  • Un opérateur complet, open-source, communautaire
  • Déploiement et configuration facilités
  • Approche déclarative
  • Nouveaux mécanismes et configuration à connaitre
  • Connaissance de PostgreSQL nécessaire

À travers ce module, vous a été présenté l’opérateur CloudNativePG, son principe de fonctionnement, son installation et une grande partie de ce qu’il permet de faire. Cela représente une bonne découverte de l’opérateur, tant sur ses fonctionnalités que sur certains aspects critiques de son utilisation.

L’opérateur CloudNativePG est celui qui connait la plus forte adoption ces dernières années. Il est complet (niveau 5 sur le site https://operatorhub.io/), open-source, avec un souhait de gouvernance partagée. Il fait d’ailleurs partie du projet d’incubation de la_ Cloud Native Computing Foundation_, garant de certains principes open-source.

Le déploiement d’instances PostgreSQL est facilité par la nature déclarative de la gestion par CloudNativePG. En se reposant sur les fonctionnalitées de Kubernetes, l’opérateur permet la mise en place de mécanismes complexes comme la haute disponibilité ou la bascule automatique.

Des mécanismes propres à l’opérateur sont à connaitre, comme sa mise à jour et les conséquences sur les instances, les différents paramètres et spécifications utilisables dans les fichiers YAML ou encore ce qui est automatiquement créé par l’opérateur (rôle, base, Secret, etc).

L’opérateur se repose sur un certain nombre de concepts liés à PostgreSQL. Il s’agit donc de bien les connaître (réplication par flux, sauvegarde PITR, etc) pour comprendre comment l’opérateur les utilise ou les configure.

L’intégration de PostgreSQL dans Kubernetes est grandement facilité par CloudNativePG. En contrepartie, cela demande à un administrateur PostgreSQL de vraies connaissances sur Kubernetes (qu’est-ce qu’un Pod ? un Service ? un Secret ?) et de revoir sa manière de travailler avec son SGBD favori.


Questions ?


Travaux pratiques

Prise en main du cluster Kubernetes

But : Prendre en main le cluster Kubernetes.

Se connecter à la machine qui vous est dédiée.

Trouver la version de l’utilitaire kubectl.

Lister les nœuds du cluster Kubernetes.

Cet utilitaire sait avec quel cluster Kubernetes interagir grâce au fichier ~/.kube/config qui se trouve dans le répertoire de votre utilisateur.

Installation de l’opérateur CloudNativePG

But : Installer l’opérateur dans le cluster Kubernetes ainsi que des modules complémentaires.

Il existe plusieurs méthodes pour installer l’opérateur : soit en appliquant directement les fichiers YAML soit en utilisant le Helm Chart fourni par le projet. Pour cet atelier, nous utiliserons la première méthode, plus simple et rapide.

Installer la version 1.27.1 de l’opérateur avec la commande kubectl apply -f :

kubectl apply --server-side -f \
  https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.27/releases/cnpg-1.27.1.yaml

Lister les Pods présents dans le namespace cnpg-system.

Retrouver la liste des nouvelles ressources Kubernetes disponibles grâce à l’opérateur CloudNativePG.

Déploiement d’instances PostgreSQL

But : Déployer un cluster PostgreSQL mono-instance, s’y connecter et suivre les traces de l’opérateur et de l’instance.

Voici un exemple de fichier YAML très simple qui permet de déployer une instance PostgreSQL en version 17.0 avec 5 Go de volume associés au PGDATA et 5 autres aux journaux de transactions.

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgresql-demo
spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:17.5-standard-bookworm
  instances: 1
  storage:
    size: 5Gi
  walStorage:
    size: 5Gi
  affinity:
    enablePodAntiAffinity: true 
    topologyKey: kubernetes.io/hostname 
    podAntiAffinityType: preferred
  resources:
    requests:
      memory: "256Mi"
      cpu: "0.5"
    limits:
      memory: "512Mi"
      cpu: "1"

Quelques informations supplémentaires sur le contenu de ce fichier :

  • apiVersion : La version de l’API de Kubernetes est utilisée ;
  • kind : Le type d’objet créé ;
  • metadata : Des informations pour identifier l’objet ;
  • spec : La définition de l’objet en question (“l’état désiré”) ;
  • imageName : Le nom de l’image utilisée ;
  • instances : Le nombre d’instances voulues (sera toujours 1 primaire + le reste en secondaire(s) ;)
  • storage : Les informations sur le stockage souhaité pour le PGDATA ;
  • walStorage : Les informations sur le stockage souhaité pour les WALs ;
  • affinity : Indique où et comment seront déployées les instances de ce Cluster ;
  • resources : L’indication de requests et limits sur la RAM et CPU.

Créer le fichier postgresql-demo.yaml dans le home directory de dalibo et copier le contenu YAML ci-dessus.

Dans un autre terminal sur la VM, suivre les traces du controller avec la commande kubectl logs -f -n cnpg-system <POD> et l’utilitaire jq. Pour retrouver le nom du Pod du controlleur, vous pouvez utiliser kubectl get pod -A.

Retourner dans l’ancienne session SSH et créer l’instance PostgreSQL à partir du fichier ~/postgresql-demo.yaml avec kubectl. En parallèle regarder ce qu’il se passe dans les traces du controller.

Se connecter à l’instance et vérifier la version de celle-ci. Vous pouvez utiliser kubectl […] comme ceci :

kubectl exec -it postgresql-demo-1 -c postgres -- psql

ou, via le plugin :

kubectl cnpg psql postgresql-demo

Suivre les traces de l’instance avec la commande kubectl logs -f postgresql-demo-1.

Éléments initiaux

But : Découvrir les éléments automatiquement créés par l’opérateur.

Avec quelques lignes de YAML et peu de commandes, une instance PostgreSQL est déployée et accessible. De nombreuses choses sont créées automatiquement pour nous. Voyons de quoi il s’agit.

Bases de données

Retrouver la liste des bases de données dans l’instance déployée. La meta-commande \l de psql vous permet de récupérer la liste des bases.

Rôles et Secret

Retrouver la liste des rôles dans l’instance déployée. La méta-commande psql \du peut vous aider.

Se déconnecter de l’instance.

Se connecter à la base de données app avec le rôle app. Une erreur devrait vous être retournée.

Se connecter à la base de données app avec le rôle app et en passant par la stack TCP/IP.

Récupérer la liste des Secrets du cluster avec kubectl get secrets.

Récupérer le mot de passe présent dans le Secret postgresql-demo-app. N’oubliez pas qu’il est encodé en Base64, il faut décoder le contenu obtenu.

Se connecter à l’instance en utilisant le mot de passe.

Services

Un Service est une couche d’abstraction qui permet d’accéder à un ensemble de Pods spécifiques. L’association Service - Pods se fait via des labels. Un label est une étiquette, un tag, apposée à une ressource.

Retrouver la liste des Services dans le cluster Kubernetes.

Retrouver les labels définis sur le Pod de votre instance.

Retrouver la description du Service postgresql-demo-ro et retrouver la partie Selector qui indique à quel(s) Pod(s) sera associé ce Service.

Modifications de paramètres de configuration

Installer une instance PostgreSQL ne suffit pas. Il faut en plus la configurer. Habituellement, la configuration se fait dans le fichier postgresql.conf et nécessite soit un rechargement, soit un redémarrage de l’instance selon le paramètre modifié. Nous allons voir comment le faire sur notre instance postgresql-demo-1.

Dupliquer le fichier ~/postgresql-demo.yaml pour conserver une copie de la définition initiale de l’instance.

Modifier le fichier ~/postgresql-demo.yaml et ajouter la section postgresql.parameters comme dans l’exemple. Nous allons tout d’abord modifier les paramètres shared_buffers et max_connections. Positionnez-les respectivement à 256MB et 10.

Suivre les traces du Pod et de l’instance avec kubectl logs -f postgresql-demo-1 | jq.

Utiliser kubectl apply -f ~/postgresql-demo.yaml pour appliquer les modifications.

Modifier le paramètre work_mem et réappliquer la définition YAML avec kubectl apply -f ~/postgresql-demo.yaml.

Vérifier que la modification a bien été prise en compte. Vous pouvez le voir dans les traces ou alors directement en vous connectant à l’instance et en utilisant show work_mem dans le prompt psql;

Créer un rôle

Il existe plusieurs méthodes pour créer un rôle dans une instance. L’ordre SQL CREATE ROLE ... peut évidemment être utilisé, mais pour cet exemple, nous allons passer par la méthode déclarative et demander à l’opérateur de faire en sorte que le rôle soit présent dans l’instance.

Créer un rôle dba ayant les droits SUPERUSER dans l’instance. Cela peut se faire grâce à la spécification spec.managed.roles dans l’objet Cluster.

Appliquer la modification avec kubectl apply -f ~/postgresql-demo.yaml.

Vérifier que le rôle a bien été créé.

Encoder le nom du rôle (dba) en base64.

Encoder le mot de passe (ilovemydba) en base64.

Créer un fichier ~/secret.yaml avec le contenu suivant puis créer le Secret.

apiVersion: v1
data:
  username: ZGJh
  password: aWxvdmVteWRiYQ==
kind: Secret
metadata:
  name: secret-password-dba
  labels:
    cnpg.io/reload: "true"
type: kubernetes.io/basic-auth

Ajouter ce mot de passe à la définition du rôle dba dans le fichier ~/postgresql-demo.yaml, via l’information passwordSecret.

Appliquer les modifications.

Se connecter avec ce nouveau rôle à la base postgres.

Créer une base de données

Créer une base de données db1 dans l’instance postgresql-demo. Le propriétaire de cette base doit être le rôle app. Pour cela, créer un fichier db1.yaml contenant la définition d’une ressource Database.

Créer la nouvelle ressource avec kubectl apply -f ~/db1.yaml.

Vérifier la présence de cette base de données dans l’instance.

Déploiement d’une instance secondaire

But : Déployer une instance secondaire dans le cluster postgresql-demo.

Notre instance actuellement déployée ne possède pas de secondaire. L’ajout de secondaire se fait facilement en modifiant le paramètre instances dans la section spec de notre fichier YAML.

Déployer un secondaire à votre instance en modifiant le paramètre instances à 2.

Configuration par défaut

Regardons la configuration qui est mise en place par défaut.

Se connecter avec psql au secondaire nouvellement créé.

Récupérer le contenu du paramètre primary_conninfo.

Se connecter avec psql au primaire.

Récupérer le contenu de la table pg_stat_replication.

Récupérer le contenu de la table pg_replication_slots.

Sur le primaire, créer une table dans la base app.

Vérifier qu’elle se trouve également sur le secondaire.

Emplacement des instances

L’opérateur CloudNativePG veille à déployer les instances PostgreSQL sur des nœuds différents afin de garantir la disponibilité. Cela permet de réduire les risques liés à un incident en s’assurant que toutes les instances ne sont pas affectées simultanément. Cette configuration permet également la répartition de la charge entre plusieurs nœuds pour des opérations de lecture.

Trouver le nom du nœud où est déployée chaque instance.

Tests de bascules

But : Tester et comprendre le mécanisme de bascule entre primaire et secondaire.

Trouver quelle est l’instance primaire du cluster postgresql-demo.

Bascule manuelle

Promouvoir l’instance postgresql-demo-2 comme nouvelle primaire avec le plugin cnpg de kubectl.

Vérifier que le cluster est en bonne santé et que postgresql-demo-2 est désormais l’instance primaire.

Bascule automatique

Lorsqu’une erreur survient sur le primaire le mécanisme de failover va être déclenché. Ce mécanisme sera démarré après une certaine durée modifiable via le paramètre .spec.failoverDelay (par défaut à 0) dans la définition du cluster PostgreSQL.

L’erreur peut être, par exemple, un problème sur le volume associé, le Pod primaire qui serait supprimé, le conteneur PostgreSQL qui serait KO, etc… (voir la documentation sur les probes).

Ajouter le paramètre .spec.failoverDelay à votre instance et le positionner à 10 secondes.

Le prendre en compte avec kubectl apply -f ~/postgresql-demo.yaml.

Dans une session, lancer la commande watch kubectl get pod.

Détruire le Pod correspondant à l’instance primaire.

Regarder comment réagit le Cluster.

Mise en place d’une sauvegarde PITR

But : Mettre en place une sauvegarde PITR sur un stockage S3 (archivage et sauvegarde complète).

Comme vous le savez certainement, il existe le concept de sauvegarde physique PITR comme mécanisme de sauvegarde d’une instance. Pour mettre en place cela, il est d’abord nécessaire de faire une sauvegarde physique de l’arborescence de l’instance. Ceci peut être fait à chaud. Le second élément essentiel est l’archivage des journaux de transactions (WAL) qui seront rejoués après une restauration pour rétablir un état cohérent.

En déployant une instance avec CloudNativePG, la seule solution de sauvegarde PITR utilisable est Barman Cloud. Très connu dans l’éco-système PostgreSQL, cet outil nous permet de faire la sauvegarde physique et l’archivage des WALs. Les commandes passées pour la mettre en place le seront de manière automatique mais une configuration doit être rajoutée dans le fichier YAML de notre Cluster.

La page de documentation du projet Barman Cloud CNPG-I plugin peut vous aider.

Installation

La mise en place d’une solution de sauvegarde nécessite, depuis la version 1.26, l’installation d’un plugin dédié aux sauvegardes. Le projet CloudNativePG met à disposition le plugin Barman Cloud CNPG-I.

Nous allons donc l’installer sur notre cluster Kubernetes. Aussi, l’outil cert-manager doit être présent dans le cluster Kubernetes. Il est utilisé pour la génération de certificats pour la communication entre l’opérateur et le plugin de sauvegarde.

Installer cert-manager avec la commande suivante.

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.19.2/cert-manager.yaml

Patienter quelques minutes, le temps que cert-manager s’installe, puis installer le plugin avec la commande suivante.

kubectl apply -f https://github.com/cloudnative-pg/plugin-barman-cloud/releases/download/v0.11.0/manifest.yaml

Vérifier que le plugin est bien installé dans le Namespace cnpg-system.

Configuration

Créer le fichier ~/s3-creds.yaml avec le contenu suivant.

Les paramètres ACCESS_* doivent contenir l’information encodée en BASE64. Si vous utilisez la commande echo, n’oubliez l’option -n qui empêche la prise en compte du saut de ligne. Les informations vous seront données par le formateur.

---
apiVersion: v1
kind: Secret
metadata:
  name: s3-creds
type: Opaque
data:
  ACCESS_KEY_ID: bWluaW9hZG1pbg==
  ACCESS_REGION: ZnItcGFy
  ACCESS_SECRET_KEY: bWluaW9hZG1pbg==

Créer le Secret dans votre cluster Kubernetes avec la commande kubectl apply -f ~/s3-creds.yaml.

Pour cette partie du TP, nous allons créer une autre instance PostgreSQL (postgresql-with-backup-demo), donc un objet de type Cluster et un nouveau nom.

Créer le fichier ~/postgresql-with-backup-demo.yaml avec le contenu suivant.

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgresql-with-backup-demo
spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:17.5-standard-bookworm
  instances: 1
  storage:
    size: 5Gi
  walStorage:
    size: 5Gi
  postgresql:
    parameters:
      shared_buffers: '256MB'
      max_connections: '10'
      work_mem: "8MB"
      archive_timeout: "20min"
  resources:
    requests:
      memory: "256Mi"
      cpu: "0.5"
    limits:
      memory: "512Mi"
      cpu: "1"
  plugins:
  - name: barman-cloud.cloudnative-pg.io
    isWALArchiver: true
    parameters:
      barmanObjectName: objectstore-demo   

La partie backup indique quel plugin de sauvegarde utilisé, ici Barman Cloud. Il est également indiqué que c’est ce plugin qui sera en charge de l’archivage des journaux de transactions grâce à le paramètre isWALArchiver à true.

Créer le fichier ~/objectstore-demo.yaml pour créer l’ ObjectStore qui sera utilisé par le nouveau Cluster. N’oubliez pas de modifier le paramètre endpointURL avec l’adresse IP du conteneur minio.

apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
  name: objectstore-demo
spec:
  configuration:
    destinationPath: "s3://cnpg/"
    endpointURL: "http://172.18.0.5:9000"
    s3Credentials:
      accessKeyId:
        name: s3-creds
        key: ACCESS_KEY_ID
      secretAccessKey:
        name: s3-creds
        key: ACCESS_SECRET_KEY
      region:
        name: s3-creds
        key: ACCESS_REGION
    wal:
      compression: gzip

Créer l’ObjectStore avec la commande :

kubectl apply -f ~/objectstore-demo.yaml

Configurer l’utilitaire mc pour qu’il se connecte au conteneur minio avec sa commande alias :

mc alias set minio http://172.18.0.5:9000 minioadmin minioadmin

Créer un nouveau bucket cnpg dans minio avec la commande :

mc mb minio/cnpg
Bucket created successfully `minio/cnpg`.

Créer le nouveau Cluster PostgreSQL avec la commande :

kubectl apply -f ~/postgresql-with-backup-demo.yaml

Vérifier que l’archivage se passe correctement directement dans la vue pg_stat_archiver.

Vérifier que des WAL soient bien présents dans le bucket cnpg avec la commande mc ls.

C’est un super point de départ. Mais pour le moment, il n’est pas possible de faire quelconque restauration comme il nous manque une sauvegarde complète de l’instance.

Sauvegarde complète de l’instance

Créer le fichier ~/letsbackup.yaml avec le contenu suivant :

apiVersion: postgresql.cnpg.io/v1
kind: Backup
metadata:
  name: first-backup
spec:
  cluster:
    name: postgresql-with-backup-demo
  method: plugin
  pluginConfiguration:
    name: barman-cloud.cloudnative-pg.io

Créer cette ressource avec avec kubectl.

Vérifier le statut de l’objet Backup.

Chercher dans les traces du Pod une preuve que la sauvegarde complète s’est bien déroulée.

Observer les changements dans le conteneur minio.

Générer de la donnée

Se connecter à l’instance. Créer une table et insérer quelques données.

Forcer la création d’un nouveau journal de transactions avec SELECT pg_switch_wal();.

Procéder à une restauration PITR

But : Restaurer notre instance depuis la sauvegarde PITR existante.

Les restaurations se font obligatoirement dans une nouvelle instance PostgreSQL. Le principe de restauration in-place n’est donc pas possible. Attention donc si vous souhaitez conserver le nom du Cluster vous devrez détruire le précédent Cluster qui porterait ce nom.

Maintenant qu’une instance est déployée et qu’une sauvegarde a été faite, attardons-nous sur les manières qui existent pour restaurer une instance.

Aussi, c’est l’occasion de faire un petit rappel ! N’oubliez pas de tester vos procédures de restauration fréquemment !

Simuler un crash. Détruire l’instance postgresql-with-backup-demo (Nous sommes bien évidemment ici dans un exercice de destruction maîtrisé par des professionnels).

Créer un nouveau fichier ~/postgresql-restored-demo.yaml avec le contenu suivant. L’idée est de créer une nouvelle instance postgresql-restored-demo et d’indiquer avec la section bootstrap qu’elle doit démarrer à partir d’une sauvegarde.

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgresql-restored-demo
spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:17.5-standard-bookworm
  instances: 1
  storage:
    size: 5Gi
  walStorage:
    size: 5Gi
  postgresql:
    parameters:
      shared_buffers: '256MB'
      max_connections: '10'
      work_mem: '8MB'
      archive_timeout: '20min'
  resources:
    requests:
      memory: "256Mi"
      cpu: "0.5"
    limits:
      memory: "512Mi"
      cpu: "1"
  bootstrap:
    recovery:
      source: source
  externalClusters:
  - name: source
    plugin:
      name: barman-cloud.cloudnative-pg.io
      parameters:
        barmanObjectName: objectstore-demo
        serverName: postgresql-with-backup-demo
  plugins:
  - name: barman-cloud.cloudnative-pg.io
    isWALArchiver: true
    parameters:
      barmanObjectName: objectstore-demo # réutilisation du bucket S3

Créer votre nouvelle instance avec kubectl apply -f ~/postgresql-restored-demo.yaml.

Lorsque l’instance est prête, s’y connecter et vérifier que les données s’y trouvent bien.

Supprimer les instances postgresql-demo qui ne vont plus nous servir par la suite.

Montée de version mineure de PostgreSQL

But : Effectuer une montée de version mineure de PostgreSQL.

La version de PostgreSQL est indiquée dans le nom et le tag de l’image déployée. La modification de celle-ci entraîne une montée de version de l’instance. Cette montée de version peut se faire automatiquement ou de manière supervisée (appelée “manuelle” dans la documentation).

Méthode unsupervised

Dans une autre session SSH, lancer la commande watch kubectl get pods pour voir ce qu’il se passe pendant la montée de version.

Modifier la version de PostgreSQL de 17.5 à 17.6 dans le fichier ~/postgresql-restored-demo.yaml et appliquer la modification avec kubectl apply.

Méthode supervised

Le paramètre primaryUpdateStrategy permet de définir la stratégie à suivre lors d’une mise à jour de l’instance primaire. Il est positionné par défaut à unsupervised. C’est le comportement que nous venons de voir avec l’exemple précédent.

Positionner le paramètre spec.primaryUpdateStrategy à supervised, modifier la version en la passant de 17.6 à 17.7 et tenter de faire la montée de version. Que constatez vous ?

Ajouter un secondaire à votre cluster PostgreSQL en modifiant la ligne instances du fichier postgresql-restored-demo.yaml et en appliquant la modification.

Vérifier les versions des deux instances. Que constatez-vous ?

Faite une bascule manuelle sur l’instance secondaire.

Montée de version majeure de PostgreSQL

But : Effectuer une montée de version majeure de PostgreSQL.

Là aussi, la version majeure de PostgreSQL est indiquée dans le nom et le tag de l’image déployée. La modification de celle-ci entraîne le déclenchement d’une In-Place Major Upgrade. Ce mécanisme se fait nécessairement sur des instances arrêtées. L’opérateur CloudNativePG les arrêtera pour vous.

Pour cet exercice, nous allons mettre à jour un Cluster en version 16.11 vers la version 18.1.

Créer le fichier ~/postgresql-16-to-18.yaml et ajouter le contenu suivant puis appliquer le avec kubectl.

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgresql-test
spec:
  instances: 2
  imageName: ghcr.io/cloudnative-pg/postgresql:16.11-standard-bookworm
  storage:
    size: 1Gi

Vérifier que les deux instances soient correctement déployées.

Créer une table et y insérer quelques lignes.

Ouvrir un nouveau terminal et lancer la commande kubectl get pod --watch.

Ouvrir un nouveau terminal et lancer la commande kubectl get pv --watch.

Ouvrir un nouveau terminal et lancer la commande kubectl get pvc --watch.

Modifier le tag du paramètre imageName du fichier ~/postgresql-16-to-18.yaml en le passant de 16.11 à 18.1 puis appliquer la modification avec kubectl.

Observez ce qu’il se passe dans les différents terminaux que vous avez ouvert.

Vérifier que les données soient toujours présentes.

Vérifier la version de PostgreSQL.

Montée de version de l’opérateur

But : Effectuer une montée de version de l’opérateur CNPG.

Nous avons déployé la version 1.27.1 de l’opérateur. Nous allons nous intéresser à la manière de le mettre à jour.

Lorsqu’une nouvelle version de l’opérateur est déployée, un nouveau Pod se crée. Lorsque celui-ci est prêt, l’ancien opérateur est tout simplement supprimé. Cette mise à jour déclenche également la mise à jour d’un composant présentant dans les Pods des instances PostgreSQL.

Lorsqu’un Pod PostgreSQL est déployé, un InitContainer est créé en amont et permet de récupérer du code correspondant à l’instance-manager. Il permet de contrôler l’instance, son cycle, ses redémarrages, etc. La version de ce manager est étroitement liée à la version de l’opérateur. Pour information, c’est ce processus qui va lancer PostgreSQL et qui aura le pid 1 dans le Pod.

cat /proc/1/cmdline 
/controller/manager instance run--status-port-tls--log-level=info

Attention si vous avez utilisé votre opérateur pour déployer plusieurs instances PostgreSQL, lorsque vous mettez à jour l’opérateur, tous les Pods seront, mis à jour en même temps (ou quasiment). Il y aura donc une coupure de service pour chaque instance. C’est le fonctionnement par défaut.

Dans une première console, lancer la commande watch suivante :

watch kubectl get pod

Dans une seconde console, lancer la commande watch suivante :

watch kubectl get pod -n cnpg-system

Dans une autre console, appliquer les fichiers YAML correspondant à la version 1.28.0 de l’opérateur.

kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.28/releases/cnpg-1.28.0.yaml

Regarder ce qu’il se passe au niveau des différents Pods (opérateur et PostgreSQL)

Exercices optionnels

But : Découvrir des fonctionnalités plus complexes.

Mise en place de sauvegardes programmées

Il est possible de programmer des sauvegardes régulières avec la ressource ScheduledBackup.

Voici un exemple de définition qui permet de déclencher une sauvegarde appelée backup-every-day tous les jours à 16h00 pour le cluster postgresql-restored-demo :

apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
  name: backup-every-day
spec:
  schedule: "0 42 18 * * *"
  backupOwnerReference: self
  cluster:
    name: postgresql-restored-demo
  method: plugin
  pluginConfiguration: 
    name: barman-cloud.cloudnative-pg.io

Attention, l’option schedule prend bien six paramètres (le premier étant les secondes), contrairement au CronJob dans Kubernetes ou aux lignes de /etc/crontab qui en prenne que cinq.

Créer le fichier ~/backup-every-day.yaml avec le contenu ci-dessus en modifiant l’heure d’exécution pour que la sauvegarde s’exécute dans 5 à 10 minutes.

Créer l’objet ScheduledBackup avec kubectl.

Suivez les traces de l’opérateur avec kubectl logs -n cnpg-system cnpg-controller-manager-6b9f78f594-hd5qq | grep backup | jq. Vous devriez voir le déclenchement de la sauvegarde.

Mettre en place une réplication synchrone

Il existe plusieurs méthodes pour mettre en place une réplication synchrone. Le but ici n’est pas de les évoquer ni de les comparer, mais simplement de voir le principe de la configuration.

Pour l’exemple, nous mettrons en place la méthode par Quorum.

Modifier la configuration de l’instance en rajoutant le bloc suivant à votre fichier ~/postgresql-restored-demo.yaml.

  postgresql:
    synchronous:
      method: any
      number: 1

Vérifier que la réplication est synchrone en regardant le champ sync_state de la vue pg_stat_replication de l’instance primaire.

Déploiement d’une application pgAdmin4

Pour voir comment une application peut se connecter à une instance, nous allons déployer pgAdmin dans le cluster Kubernetes.

Ouvrir une nouvelle session SSH. Passer en tant qu’utilisateur dalibo.

Créer le fichier ~/pgadmin.yaml avec le contenu suivant et le déployer dans le cluster Kubernetes.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pgadmin
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pgadmin
  template:
    metadata:
      labels:
        app: pgadmin
    spec:
      containers:
        - name: pgadmin
          image: dpage/pgadmin4
          ports:
            - containerPort: 80
          env:
            - name: PGADMIN_DEFAULT_EMAIL
              value: admin@example.com
            - name: PGADMIN_DEFAULT_PASSWORD
              value: admin

Récupérer le nom du Pod pgAdmin déployé, et lancer la commande suivante :

kubectl port-forward --address 0.0.0.0 pgadmin-*****-***** 8888:80

Accéder à l’interface de pgAdmin via votre navigateur http://adresseippublique:8888 et connectez vous à l’interface (admin@example.com / admin).

Créer une nouvelle connexion avec les informations suivantes : Créer une nouvelle connexion avec cette fois-ci postgresql-demo-ro comme paramètre Host name/address et créer une table CREATE TABLE ma_table (i int);.


Travaux pratiques (solutions)

Prise en main du cluster Kubernetes

But : Prendre en main le cluster Kubernetes.

Se connecter à la machine qui vous est dédiée.

ssh root@A.B.C.D

Trouver la version de l’utilitaire kubectl.

kubectl version
kubectl version
Client Version: v1.34.3
Kustomize Version: v5.7.1
Server Version: v1.34.0

L’utilitaire kubectl vous permet d’interagir avec le cluster Kubernetes déployé.

Lister les nœuds du cluster Kubernetes.

kubectl get nodes
NAME                 STATUS   ROLES           AGE    VERSION
kind-control-plane   Ready    control-plane   135m   v1.34.0
kind-worker          Ready    <none>          134m   v1.34.0
kind-worker2         Ready    <none>          134m   v1.34.0

Cet utilitaire sait avec quel cluster Kubernetes interagir grâce au fichier ~/.kube/config qui se trouve dans le répertoire de votre utilisateur.

Installation de l’opérateur CloudNativePG

But : Installer l’opérateur dans le cluster Kubernetes ainsi que des modules complémentaires.

Il existe plusieurs méthodes pour installer l’opérateur : soit en appliquant directement les fichiers YAML soit en utilisant le Helm Chart fourni par le projet. Pour cet atelier, nous utiliserons la première méthode, plus simple et rapide.

Installer la version 1.27.1 de l’opérateur avec la commande kubectl apply -f :

kubectl apply --server-side -f \
  https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.27/releases/cnpg-1.27.1.yaml
namespace/cnpg-system serverside-applied
customresourcedefinition.apiextensions.k8s.io/backups.postgresql.cnpg.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/clusterimagecatalogs.postgresql.cnpg.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/clusters.postgresql.cnpg.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/imagecatalogs.postgresql.cnpg.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/poolers.postgresql.cnpg.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/scheduledbackups.postgresql.cnpg.io serverside-applied
serviceaccount/cnpg-manager serverside-applied
clusterrole.rbac.authorization.k8s.io/cnpg-manager serverside-applied
clusterrolebinding.rbac.authorization.k8s.io/cnpg-manager-rolebinding serverside-applied
configmap/cnpg-default-monitoring serverside-applied
service/cnpg-webhook-service serverside-applied
deployment.apps/cnpg-controller-manager serverside-applied
mutatingwebhookconfiguration.admissionregistration.k8s.io/cnpg-mutating-webhook-configuration serverside-applied
validatingwebhookconfiguration.admissionregistration.k8s.io/cnpg-validating-webhook-configuration serverside-applied

Les fichiers seront récupérés depuis internet et appliqués sur votre cluster Kubernetes. Pour rappel, l’outil kubectl sait avec quel cluster Kubernetes interagir grâce au fichier kubeconfig.

Ces fichiers là contiennent la définition de différents ressources :

  • Un Namespace;
  • Une CustomResourceDefinition pour les différentes ressources que l’opérateur va gérer (Backup, Cluster, …)
  • Mais aussi un ServiceAccount, unClusterRoleBinding et surtout un déploiement du controller CloudNativePG.

Par défaut, le Controller, cerveau de l’opérateur, sera déployé dans le Namespace cnpg-system, créé lors de l’installation du l’opérateur. Ce controller n’est ni plus ni moins qu’une application. On peut voir le controller avec la commande kubectl get pods et en indiquant le bon Namespace :

Lister les Pods présents dans le namespace cnpg-system.

kubectl get pods -n cnpg-system
NAME                                       READY   STATUS    RESTARTS   AGE
cnpg-controller-manager-7fc549dc69-xq7gq   1/1     Running   0          11s

Retrouver la liste des nouvelles ressources Kubernetes disponibles grâce à l’opérateur CloudNativePG.

kubectl api-resources --api-group postgresql.cnpg.io
backups                                          postgresql.cnpg.io/v1             true         Backup
clusterimagecatalogs                             postgresql.cnpg.io/v1             false        ClusterImageCatalog
clusters                                         postgresql.cnpg.io/v1             true         Cluster
imagecatalogs                                    postgresql.cnpg.io/v1             true         ImageCatalog
poolers                                          postgresql.cnpg.io/v1             true         Pooler
scheduledbackups                                 postgresql.cnpg.io/v1             true         ScheduledBackup

Déploiement d’instances PostgreSQL

But : Déployer un cluster PostgreSQL mono-instance, s’y connecter et suivre les traces de l’opérateur et de l’instance.

Voici un exemple de fichier YAML très simple qui permet de déployer une instance PostgreSQL en version 17.0 avec 5 Go de volume associés au PGDATA et 5 autres aux journaux de transactions.

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgresql-demo
spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:17.5-standard-bookworm
  instances: 1
  storage:
    size: 5Gi
  walStorage:
    size: 5Gi
  affinity:
    enablePodAntiAffinity: true 
    topologyKey: kubernetes.io/hostname 
    podAntiAffinityType: preferred
  resources:
    requests:
      memory: "256Mi"
      cpu: "0.5"
    limits:
      memory: "512Mi"
      cpu: "1"

Quelques informations supplémentaires sur le contenu de ce fichier :

  • apiVersion : La version de l’API de Kubernetes est utilisée ;
  • kind : Le type d’objet créé ;
  • metadata : Des informations pour identifier l’objet ;
  • spec : La définition de l’objet en question (“l’état désiré”) ;
  • imageName : Le nom de l’image utilisée ;
  • instances : Le nombre d’instances voulues (sera toujours 1 primaire + le reste en secondaire(s) ;)
  • storage : Les informations sur le stockage souhaité pour le PGDATA ;
  • walStorage : Les informations sur le stockage souhaité pour les WALs ;
  • affinity : Indique où et comment seront déployées les instances de ce Cluster ;
  • resources : L’indication de requests et limits sur la RAM et CPU.

Créer le fichier postgresql-demo.yaml dans le home directory de dalibo et copier le contenu YAML ci-dessus :

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgresql-demo
spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:17.5-standard-bookworm
  instances: 1
  storage:
    size: 5Gi
  walStorage:
    size: 5Gi
  affinity:
    enablePodAntiAffinity: true 
    topologyKey: kubernetes.io/hostname 
    podAntiAffinityType: preferred
  resources:
    requests:
      memory: "256Mi"
      cpu: "0.5"
    limits:
      memory: "512Mi"
      cpu: "1"

Dans un autre terminal sur la VM, suivre les traces du controller avec la commande kubectl logs -f -n cnpg-system <POD> et l’utilitaire jq. Pour retrouver le nom du Pod du controlleur, vous pouvez utiliser kubectl get pod -A.

kubectl logs -f -n cnpg-system cnpg-controller-manager-7fc549dc69-8v8xw | jq

Retourner dans l’ancienne session SSH et créer l’instance PostgreSQL à partir du fichier ~/postgresql-demo.yaml avec kubectl. En parallèle regarder ce qu’il se passe dans les traces du controller :

kubectl apply -f ~/postgresql-demo.yaml
cluster.postgresql.cnpg.io/postgresql-demo created

Une instance PostgreSQL est désormais en train d’être déployée par l’opérateur. Vous avez décrit ce que vous souhaitiez avoir, l’opérateur fait le reste.

Plusieurs choses se passent lorsque vous appliquez ce fichier avec kubectl. Tout d’abord l’opérateur va déployer un premier Pod appelé <clusterName>-1-initdb-<random>. initdb devrait vous faire penser à la la commande à exécuter lorsque vous devez créer une instance manuellement par exemple.

kubectl get pods --watch
NAME                             READY   STATUS    RESTARTS   AGE   IP       NODE     NOMINATED NODE   READINESS GATES
postgresql-demo-1-initdb-5pndc   0/1     Pending   0          2s    <none>   <none>   <none>           <none>

Ce Pod là se repose sur une image qui doit être téléchargée. C’est pour cela que vous devez avoir autorisé l’accès vers internet (ou votre dépôt local d’images) à votre cluster. Lorsque celle-ci est récupérée, le Pod est “amorcé” et les conteneurs d’initialisation sont déployés, comme on peut le voir avec cette seconde remontée. Ici il existe un conteneur d’initialisation mais aucun n’est terminé.

NAME                             READY   STATUS     RESTARTS   AGE   IP       NODE       NOMINATED NODE   READINESS GATES
postgresql-demo-1-initdb-5pndc   0/1     Init:0/1   0          13s   <none>   k8s-demo   <none>           <none>

Au fur et à mesure, le Pod passe par d’autres états …

NAME                             READY   STATUS            RESTARTS   AGE   IP              NODE       NOMINATED NODE   READINESS GATES
postgresql-demo-1-initdb-5pndc   0/1     PodInitializing   0          24s   10.244.228.68   k8s-demo   <none>           <none>

… jusqu’à l’état Running. À cette étape-ci, le Pod va notamment initialiser l’instance avec la création de l’arborescence du PGDATA.

NAME                             READY   STATUS    RESTARTS   AGE   IP              NODE       NOMINATED NODE   READINESS GATES
postgresql-demo-1-initdb-5pndc   1/1     Running   0          35s   10.244.228.68   k8s-demo   <none>           <none>

Enfin, lorsque cette étape est terminée, l’opérateur CloudNativePG déploie un autre Pod qui cette fois-ci ne porte plus le mot initdb. Un numéro est ajouté à la fin du nom. Une adresse IP est attribuée à ce Pod (IP privée RFC 1918) :

kubectl get pods -o wide
NAME                READY   STATUS    RESTARTS   AGE   IP              NODE       NOMINATED NODE   READINESS GATES
postgresql-demo-1   1/1     Running   0          10m   10.244.228.69   k8s-demo   <none>           <none>

Votre Pod est prêt et donc votre instance aussi !

Se connecter à l’instance et vérifier la version de celle-ci. Vous pouvez utiliser kubectl exec […] comme ceci :

kubectl exec -it postgresql-demo-1 -c postgres -- psql

ou, via le plugin :

kubectl cnpg psql postgresql-demo
psql (17.5 (Debian 17.5-1.pgdg120+1))
Type "help" for help.

postgres=# select version()\gx
-[ RECORD 1 ]----------------------------------------------------------------------------------------------------------------
version | PostgreSQL 17.5 (Debian 17.5-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit

La commande kubectl exec -it permet d’exécuter un programme au sein du Pod. L’outil psql étant présent dans l’image, cela est possible. Essayez avec vim, qui lui n’est pas présent dans l’image, un message d’erreur apparaîtra.

Si besoin, pour quitter psql, vous pouvez utiliser control+d, \q ou exit.

postgres=# \q

Suivre les traces de l’instance avec la commande kubectl logs -f postgresql-demo-1.

kubectl logs -f postgresql-demo-1
{"level":"info","ts":"2025-12-11T14:23:56.028300944Z","logger":"postgres","msg":"record","logging_pod":"postgresql-demo-1","record":{"log_time":"2025-12-11 14:23:56.027 UTC","process_id":"30","session_id":"693ad3fb.1e","session_line_num":"5","session_start_time":"2025-12-11 14:23:55 UTC","transaction_id":"0","error_severity":"LOG","sql_state_code":"00000","message":"listening on Unix socket \"/controller/run/.s.PGSQL.5432\"","backend_type":"postmaster","query_id":"0"}}
{"level":"info","ts":"2025-12-11T14:23:56.075466089Z","logger":"postgres","msg":"record","logging_pod":"postgresql-demo-1","record":{"log_time":"2025-12-11 14:23:56.075 UTC","process_id":"36","session_id":"693ad3fc.24","session_line_num":"1","session_start_time":"2025-12-11 14:23:56 UTC","transaction_id":"0","error_severity":"LOG","sql_state_code":"00000","message":"database system was shut down at 2025-12-11 14:23:48 UTC","backend_type":"startup","query_id":"0"}}
{"level":"info","ts":"2025-12-11T14:23:56.108020409Z","logger":"postgres","msg":"record","logging_pod":"postgresql-demo-1","record":{"log_time":"2025-12-11 14:23:56.107 UTC","process_id":"30","session_id":"693ad3fb.1e","session_line_num":"6","session_start_time":"2025-12-11 14:23:55 UTC","transaction_id":"0","error_severity":"LOG","sql_state_code":"00000","message":"database system is ready to accept connections","backend_type":"postmaster","query_id":"0"}}

Les logs de l’instances sont récupérés au format JSON, et sont en l’état peu exploitables. Pour les lire plus facilement, vous pouvez utiliser l’outil jq.

kubectl logs -f postgresql-demo-1 | jq
[...]
{
  "level": "info",
  "ts": "2025-09-16T11:58:11.525416928Z",
  "logger": "postgres",
  "msg": "record",
  "logging_pod": "postgresql-demo-1",
  "record": {
    "log_time": "2025-09-16 11:58:11.525 UTC",
    "process_id": "22",
    "session_id": "68c950d3.16",
    "session_line_num": "6",
    "session_start_time": "2025-09-16 11:58:11 UTC",
    "transaction_id": "0",
    "error_severity": "LOG",
    "sql_state_code": "00000",
    "message": "database system is ready to accept connections",
    "backend_type": "postmaster",
    "query_id": "0"
  }
}

Éléments initiaux

But : Découvrir les éléments automatiquement créés par l’opérateur.

Avec quelques lignes de YAML et peu de commandes, une instance PostgreSQL est déployée et accessible. De nombreuses choses sont créées automatiquement pour nous. Voyons de quoi il s’agit.

Bases de données

Retrouver la liste des bases de données dans l’instance déployée. La meta-commande \l de psql vous permet de récupérer la liste des bases.

kubectl exec -it postgresql-demo-1 -- psql -c '\l'

ou

kubectl cnpg psql postgresql-demo -- -c '\l'
Defaulted container "postgres" out of: postgres, bootstrap-controller (init)
                                                List of databases
   Name    |  Owner   | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules |   Access privileges   
-----------+----------+----------+-----------------+---------+-------+--------+-----------+-----------------------
 app       | app      | UTF8     | libc            | C       | C     |        |           | 
 postgres  | postgres | UTF8     | libc            | C       | C     |        |           | 
 template0 | postgres | UTF8     | libc            | C       | C     |        |           | =c/postgres          +
           |          |          |                 |         |       |        |           | postgres=CTc/postgres
 template1 | postgres | UTF8     | libc            | C       | C     |        |           | =c/postgres          +
           |          |          |                 |         |       |        |           | postgres=CTc/postgres
(4 rows)

Par défaut, une base de données app est créée dans l’instance PostgreSQL.

Rôles et Secret

Retrouver la liste des rôles dans l’instance déployée. La méta-commande psql \du peut vous aider.

La méta-commande psql \du vous permet de récupérer la liste des rôles.

kubectl exec -it postgresql-demo-1 -- psql -c "\du"

ou

kubectl cnpg psql postgresql-demo -- -c '\du'
                                 List of roles
     Role name     |                         Attributes                         
-------------------+------------------------------------------------------------
 app               | 
 postgres          | Superuser, Create role, Create DB, Replication, Bypass RLS
 streaming_replica | Replication

Par défaut deux rôles sont créés : app et streaming_replica. Dans la liste des bases de données, on peut d’ailleurs voir que le rôle app est propriétaire de la base app.

Se déconnecter de l’instance.

postgres=# \q

Se connecter à la base de données app avec le rôle app. Une erreur devrait vous être retournée.

kubectl exec -it postgresql-demo-1 -- psql -U app

ou

kubectl cnpg psql postgresql-demo -- -U app
psql: error: connection to server on socket "/controller/run/.s.PGSQL.5432" failed: FATAL:  Peer authentication failed for user "app"
command terminated with exit code 2

Se connecter à la base de données app avec le rôle app et en passant par la stack TCP/IP.

L’authentification du rôle app avec la méthode peer ne peut pas se faire. Mais alors comment se connecter avec app ? En sachant que listen_addresses est positionné à * par défaut, une solution pour tester la connexion est de passer par la stack TCP/IP classique en utilisant l’option -h de psql.

kubectl exec -it postgresql-demo-1 -- psql -U app -h 127.0.0.1
Defaulted container "postgres" out of: postgres, bootstrap-controller (init)
Password for user app: 

Il faut comprendre que le 127.0.0.1 fait référence à l’adresse localhost du Pod. On demande à psql, via kubectl, de se connecter sur l’interface localhost du Pod… mais il nous faut le mot de passe de app… où le trouver ?

CloudNativePG crée automatiquement un Secret qui contient des informations de connexion, notamment le mot de passe de app.

Récupérer la liste des Secrets du cluster avec kubectl get secrets.

kubectl get secrets
NAME                          TYPE                       DATA   AGE
postgresql-demo-app           kubernetes.io/basic-auth   11     24m
postgresql-demo-ca            Opaque                     2      24m
postgresql-demo-replication   kubernetes.io/tls          2      24m
postgresql-demo-server        kubernetes.io/tls          2      24m

Récupérer le mot de passe présent dans le Secret postgresql-demo-app. N’oubliez pas qu’il est encodé en BASE64, il faut décoder le contenu obtenu.

kubectl get secret postgresql-demo-app -o json | jq '.data.password'
"VFdyejRQbmY1RWMwVjFjUHlqYkdFZnI5RG52WE5YaXN0NUhIaFZkOENwSkpKOEthVkVLUkNxUGwweTRzaGlVbw=="

Ou bien sans jq, avec une commande kubectl un peu plus poussée :

kubectl get secret postgresql-demo-app --no-headers -o custom-columns=Passwd:.data.password
VFdyejRQbmY1RWMwVjFjUHlqYkdFZnI5RG52WE5YaXN0NUhIaFZkOENwSkpKOEthVkVLUkNxUGwweTRzaGlVbw==

Le résultat est encodé en BASE64. Il faut donc le décoder avec l’une des commandes suivantes :

kubectl get secret postgresql-demo-app --no-headers -o custom-columns=Passwd:.data.password | base64 -d
TWrz4Pnf5Ec0V1cPyjbGEfr9DnvXNXist5HHhVd8CpJJJ8KaVEKRCqPl0y4shiUo

Se connecter à l’instance en utilisant le mot de passe.

kubectl exec -it postgresql-demo-1 -- psql -U app -h 127.0.0.1

Defaulted container "postgres" out of: postgres, bootstrap-controller (init)
Password for user app: 
psql (17.5 (Debian 17.5-1.pgdg120+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off, ALPN: postgresql)
Type "help" for help.

app=> 

L’astuce d’utiliser kubectl et psql avec l’option -h permet à des administrateurs de se connecter, mais cela n’est pas envisageable pour des applications. Les applications doivent passer par les objets Services.

Services

Un Service est une couche d’abstraction qui permet d’accéder à un ensemble de Pods spécifiques. L’association Service - Pods se fait via des labels. Un label est une étiquette, un tag, apposée à une ressource.

Retrouver la liste des Services dans le cluster Kubernetes.

Comme toutes les autres ressources Kubernetes, vous pouvez récupérer les objets Services avec get.

kubectl get svc
NAME                 TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
kubernetes           ClusterIP   10.96.0.1        <none>        443/TCP    6h24m
postgresql-demo-r    ClusterIP   10.105.219.134   <none>        5432/TCP   6h11m
postgresql-demo-ro   ClusterIP   10.105.155.153   <none>        5432/TCP   6h11m
postgresql-demo-rw   ClusterIP   10.105.191.44    <none>        5432/TCP   6h11m

À chaque cluster PostgreSQL déployé, trois services sont créés :

  • Un service qui permet d’accéder au primaire : postgresql-demo-rw qui est en lecture/écriture;
  • Un service qui permet d’accéder uniquement au(x) secondaire(s) : postgresql-demo-ro qui sont en lecture seule;
  • Un service qui permet d’accéder à toutes les instances : postgresql-demo-r.

Retrouver les labels définis sur le Pod de votre instance :

kubectl get pod postgresql-demo-1 --show-labels
NAME                READY   STATUS    RESTARTS   AGE   LABELS
postgresql-demo-1   1/1     Running   0          26h   cnpg.io/cluster=postgresql-demo,cnpg.io/instanceName=postgresql-demo-1,cnpg.io/instanceRole=primary,cnpg.io/podRole=instance,role=primary

Retrouver la description du Service postgresql-demo-ro et retrouver la partie Selector qui indique à quel(s) Pod(s) sera associé ce Service.

kubectl describe service postgresql-demo-ro
Name:                     postgresql-demo-ro
Namespace:                default
Labels:                   cnpg.io/cluster=postgresql-demo
Annotations:              cnpg.io/operatorVersion: 1.27.1
Selector:                 cnpg.io/cluster=postgresql-demo,cnpg.io/instanceRole=replica
Type:                     ClusterIP
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.96.123.203
IPs:                      10.96.123.203
Port:                     postgres  5432/TCP
TargetPort:               5432/TCP
Endpoints:                
Session Affinity:         None
Internal Traffic Policy:  Cluster
Events:                   <none>

Lorsqu’une bascule a lieu, les labels des Pods sont mis à jour et l’association Service - Pod est automatiquement adaptée.

De ce fait, si vos applications utilisent bien le nom du Service dans les informations de connexion, elles seront automatiquement redirigées vers la nouvelle instance primaire par exemple.

Modifications de paramètres de configuration

Installer une instance PostgreSQL ne suffit pas. Il faut en plus la configurer. Habituellement, la configuration se fait dans le fichier postgresql.conf et nécessite soit un rechargement, soit un redémarrage de l’instance selon le paramètre modifié. Nous allons voir comment le faire sur notre instance postgresql-demo-1.

Dupliquer le fichier ~/postgresql-demo.yaml pour conserver une copie de la définition initiale de l’instance.

cp ~/postgresql-demo.yaml ~/postgresql-demo.bckp

Modifier le fichier ~/postgresql-demo.yaml et ajouter la section postgresql.parameters comme dans l’exemple. Nous allons tout d’abord modifier les paramètres shared_buffers et max_connections. Positionnez-les respectivement à 256MB et 10.

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgresql-demo
spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:17.5-standard-bookworm
  instances: 1
  storage:
    size: 5Gi
  walStorage:
    size: 5Gi
  postgresql:
    parameters:
      shared_buffers: '256MB'
      max_connections: '10'
  affinity:
    enablePodAntiAffinity: true 
    topologyKey: kubernetes.io/hostname 
    podAntiAffinityType: preferred
  resources:
    requests:
      memory: "256Mi"
      cpu: "0.5"
    limits:
      memory: "512Mi"
      cpu: "1"

Suivre les traces du Pod et de l’instance avec kubectl logs -f postgresql-demo-1 | jq.

Utiliser kubectl apply -f ~/postgresql-demo.yaml pour appliquer les modifications.

Dans les traces du Pod, certains messages indiquent très clairement ce qu’il va se passer. Les champs msg et message sont les plus intéressants. Par exemple, celui-ci qui indique qu’un rechargement de la configuration est nécessaire.

{
  "level": "info",
  "ts": "2025-12-11T14:59:02.686861537Z",
  "msg": "Requesting configuration reload",
  "logger": "instance-manager",
  "logging_pod": "postgresql-demo-1",
  "controller": "instance-cluster",
  "controllerGroup": "postgresql.cnpg.io",
  "controllerKind": "Cluster",
  "Cluster": {
    "name": "postgresql-demo",
    "namespace": "default"
  },
  "namespace": "default",
  "name": "postgresql-demo",
  "reconcileID": "2fc475ca-80e9-4563-927a-f63ea3559eeb",
  "pgdata": "/var/lib/postgresql/data/pgdata",
  "pgCtlOptions": [
    "-D",
    "/var/lib/postgresql/data/pgdata",
    "reload"
  ]
}

Ou encore celui-ci, quelques lignes plus loin, qui indique que le paramètre modifié implique qu’un redémarrage de l’instance est nécessaire.

{
  "level": "info",
  "ts": "2025-12-11T14:59:02.696822747Z",
  "logger": "postgres",
  "msg": "record",
  "logging_pod": "postgresql-demo-1",
  "record": {
    "log_time": "2025-12-11 14:59:02.696 UTC",
    "process_id": "30",
    "session_id": "693ad3fb.1e",
    "session_line_num": "8",
    "session_start_time": "2025-12-11 14:23:55 UTC",
    "transaction_id": "0",
    "error_severity": "LOG",
    "sql_state_code": "55P02",
    "message": "parameter \"max_connections\" cannot be changed without restarting the server",
    "backend_type": "postmaster",
    "query_id": "0"
  }
}

À la toute fin, votre instance est de nouveau accessible comme l’indique la ligne JSON :

{
  "level": "info",
  "ts": "2025-12-11T14:59:04.359349097Z",
  "logger": "postgres",
  "msg": "record",
  "logging_pod": "postgresql-demo-1",
  "record": {
    "log_time": "2025-12-11 14:59:04.359 UTC",
    "process_id": "870",
    "session_id": "693adc38.366",
    "session_line_num": "6",
    "session_start_time": "2025-12-11 14:59:04 UTC",
    "transaction_id": "0",
    "error_severity": "LOG",
    "sql_state_code": "00000",
    "message": "database system is ready to accept connections",
    "backend_type": "postmaster",
    "query_id": "0"
  }
}

Il faut donc comprendre que dès qu’une modification est apportée à la configuration, par défaut, l’opérateur CloudNativePG va faire en sorte qu’elle soit prise immédiatement en compte.

Un rechargement de la configuration sera effectué si le paramètre ne nécessite pas le redémarrage de l’instance. Si un redémarrage est effectué, alors les connexions seront coupées et devront être refaites à l’instance PostgreSQL par les applications. Vous devez donc bien savoir ce que vous devez faire. Des précautions sont donc plus que nécessaires.

Modifier le paramètre work_mem et réappliquer la définition YAML avec kubectl apply -f ~/postgresql-demo.yaml.

[...]
  postgresql:
    parameters:
      shared_buffers: '256MB'
      max_connections: '10'
      work_mem: '8MB'
[...]

Le paramètre work_mem ne nécessite pas un redémarrage de l’instance. Voici un exemple de trace obtenue lors de la modification de ce paramètre.

{
  "level": "info",
  "ts": "2025-12-11T15:10:01.128090861Z",
  "logger": "postgres",
  "msg": "record",
  "logging_pod": "postgresql-demo-1",
  "record": {
    "log_time": "2025-12-11 15:10:01.108 UTC",
    "process_id": "870",
    "session_id": "693adc38.366",
    "session_line_num": "7",
    "session_start_time": "2025-12-11 14:59:04 UTC",
    "transaction_id": "0",
    "error_severity": "LOG",
    "sql_state_code": "00000",
    "message": "received SIGHUP, reloading configuration files",
    "backend_type": "postmaster",
    "query_id": "0"
  }

La ligne message indique que seul un rechargement de la configuration a été nécessaire.

Vérifier que la modification a bien été prise en compte. Vous pouvez le voir dans les traces ou alors directement en vous connectant à l’instance et en utilisant show work_mem dans le prompt psql;

Dans les traces, regarder le champ message.

{
  "level": "info",
  "ts": "2025-12-11T15:10:01.128178235Z",
  "logger": "postgres",
  "msg": "record",
  "logging_pod": "postgresql-demo-1",
  "record": {
    "log_time": "2025-12-11 15:10:01.110 UTC",
    "process_id": "870",
    "session_id": "693adc38.366",
    "session_line_num": "8",
    "session_start_time": "2025-12-11 14:59:04 UTC",
    "transaction_id": "0",
    "error_severity": "LOG",
    "sql_state_code": "00000",
    "message": "parameter \"work_mem\" changed to \"8MB\"",
    "backend_type": "postmaster",
    "query_id": "0"
  }
}
kubectl exec -it postgresql-demo-1 -- psql

ou, via le plugin :

kubectl cnpg psql postgresql-demo
Defaulted container "postgres" out of: postgres, bootstrap-controller (init)
psql (17.0 (Debian 17.0-1.pgdg110+1))
Type "help" for help.

postgres=# show work_mem ;
 work_mem 
----------
 8MB
(1 row)

Certains paramètres PostgreSQL ne sont pas modifiables. C’est le parti pris des développeurs de CloudNativePG. La liste se trouve dans la documentation du projet (ici).

Créer un rôle

Il existe plusieurs méthodes pour créer un rôle dans une instance. L’ordre SQL CREATE ROLE ... peut évidemment être utilisé, mais pour cet exemple, nous allons passer par la méthode déclarative et demander à l’opérateur de faire en sorte que le rôle soit présent dans l’instance.

Créer un rôle dba ayant les droits SUPERUSER dans l’instance. Cela peut se faire grâce à la spécification spec.managed.roles dans l’objet Cluster.

L’ajout d’un rôle se fait avec la section managed du fichier yaml. Par exemple dans notre fichier ~/postgresql-demo.yaml, cela donnerait :

[...]
spec:
[...]
  managed:
    roles:
    - name: dba
      ensure: present
      comment: Administrateur
      login: true
      superuser: true

Appliquer la modification avec kubectl apply -f ~/postgresql-demo.yaml.

kubectl apply -f ~/postgresql-demo.yaml
cluster.postgresql.cnpg.io/postgresql-demo configured

Vérifier que le rôle a bien été créé.

kubectl exec -it postgresql-demo-1 -- psql -c '\du'
Defaulted container "postgres" out of: postgres, bootstrap-controller (init)
                                 List of roles
     Role name     |                         Attributes                         
-------------------+------------------------------------------------------------
 app               | 
 dba               | Superuser
 postgres          | Superuser, Create role, Create DB, Replication, Bypass RLS
 streaming_replica | Replication

Le rôle est bien créé mais il n’a actuellement pas de mot de passe configuré.

Si vous souhaitez en ajouter un, vous pouvez le faire de plusieurs manières :

  • en exécutant la requête ALTER ROLE ... SET PASSWORD ... ;
  • en utilisant \password <user> (la préférer à ALTER ROLE...);
  • en demandant à CloudNativePG de le faire. Cela nécessite la création d’un objet Secret.

C’est cette dernière méthode que nous allons suivre. Pour cela, le mot de passe n’est jamais passé en clair dans le fichier YAML. Il est en fait nécessaire de créer un objet Secret qui contiendra ce mot de passe encodé en BASE64 ainsi que le nom du rôle. C’est ce Secret là qui sera utilisé dans le fichier YAML.

Encoder le nom du rôle (dba) en BASE64.

printf "dba" | base64
ZGJh

Encoder le mot de passe (ilovemydba) en BASE64.

Vous pouvez ajouter un espace avant echo pour que la commande n’apparaisse pas dans l’historique de l’utilisateur dalibo.

printf "ilovemydba" | base64   
aWxvdmVteWRiYQ==

Créer un fichier ~/secret.yaml avec le contenu suivant puis créer le Secret.

apiVersion: v1
data:
  username: ZGJh
  password: aWxvdmVteWRiYQ==
kind: Secret
metadata:
  name: secret-password-dba
  labels:
    cnpg.io/reload: "true"
type: kubernetes.io/basic-auth
kubectl apply -f ~/secret.yaml
secret/secret-password-dba created

Ajouter ce mot de passe à la définition du rôle dba dans le fichier ~/postgresql-demo.yaml, via l’information passwordSecret.

[...]
  managed:
    roles:
    - name: dba
      ensure: present
      comment: Administrateur
      login: true
      superuser: true
      passwordSecret:
        name: secret-password-dba

Appliquer les modifications.

kubectl apply -f ~/postgresql-demo.yaml
cluster.postgresql.cnpg.io/postgresql-demo configured

Se connecter avec ce nouveau rôle à la base postgres.

Le rôle dba peut désormais se connecter avec son super mot de passe. Par exemple :

kubectl exec -it postgresql-demo-1 -c postgres -- psql -d postgres -U dba -h 127.0.0.1
Defaulted container "postgres" out of: postgres, bootstrap-controller (init)
Password for user dba:
psql (17.0 (Debian 17.0-1.pgdg110+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off, ALPN: postgresql)
Type "help" for help.

postgres=# 

Créer une base de données

Créer une base de données db1 dans l’instance postgresql-demo. Le propriétaire de cette base doit être le rôle app. Pour cela, créer un fichier db1.yaml contenant la définition d’une ressource Database.

Voici la déclaration d’une telle base de données.

apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
  name: db1
spec:
  name: db1
  owner: app
  cluster:
    name: postgresql-demo

Créer la nouvelle ressource avec kubectl apply -f ~/db1.yaml.

kubectl apply -f db1.yaml
database.postgresql.cnpg.io/db1 created 

Vérifier la présence de cette base de données dans l’instance.

Plusieurs possibilités. En voici une :

kubectl cnpg psql postgresql-demo -- -c "\l"
                                                List of databases
   Name    |  Owner   | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules |   Access privileges   
-----------+----------+----------+-----------------+---------+-------+--------+-----------+-----------------------
 app       | app      | UTF8     | libc            | C       | C     |        |           | 
 db1       | app      | UTF8     | libc            | C       | C     |        |           | 
 postgres  | postgres | UTF8     | libc            | C       | C     |        |           | 
 template0 | postgres | UTF8     | libc            | C       | C     |        |           | =c/postgres          +
           |          |          |                 |         |       |        |           | postgres=CTc/postgres
 template1 | postgres | UTF8     | libc            | C       | C     |        |           | =c/postgres          +
           |          |          |                 |         |       |        |           | postgres=CTc/postgres
(5 rows)

Déploiement d’une instance secondaire

But : Déployer une instance secondaire dans le cluster postgresql-demo.

Notre instance actuellement déployée ne possède pas de secondaire. L’ajout de secondaire se fait facilement en modifiant le paramètre instances dans la section spec de notre fichier yaml.

Déployer un secondaire à votre instance en modifiant le paramètre instances à 2.

[...]
spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:17.5-standard-bookworm
  instances: 2
[...]
kubectl apply -f ~/postgresql-demo.yaml

Un second Pod va être déployé.

kubectl get pod | grep demo
postgresql-demo-1              1/1     Running    0          73m
postgresql-demo-2-join-h2vvw   0/1     Init:0/1   0          38s
kubectl get pod | grep demo
postgresql-demo-1              1/1     Running           0          73m
postgresql-demo-2-join-h2vvw   0/1     PodInitializing   0          58s
kubectl get pod | grep demo

postgresql-demo-1              1/1     Running     0          73m
postgresql-demo-2              1/1     Running     0          31s

Et voilà, un secondaire a été créé ! L’opérateur CloudNativePG s’assure de tout configurer : ajout du paramètre primary_conninfo, création du fichier standby.signal, mise à jour de pg_hba.conf, slot de réplication, etc. Le secondaire se connecte alors au primaire en utilisant la réplication physique native de PostgreSQL (Streaming Replication).

Configuration par défaut

Regardons la configuration qui est mise en place par défaut.

Se connecter avec psql au secondaire nouvellement créé.

kubectl exec -it postgresql-demo-2 -c postgres -- psql

Récupérer le contenu du paramètre primary_conninfo.

postgres=# \x
Expanded display is on.
postgres=# show primary_conninfo ;
-[ RECORD 1 ]----+---------------------
primary_conninfo | host=postgresql-demo-rw user=streaming_replica [...]

La sortie a été mise en forme pour plus de lisibilité.

host=postgresql-demo-rw
user=streaming_replica
port=5432
sslkey=/controller/certificates/streaming_replica.key
sslcert=/controller/certificates/streaming_replica.crt
sslrootcert=/controller/certificates/server-ca.crt
application_name=postgresql-demo-2
sslmode=verify-ca

Le secondaire utilise le Service postgresql-demo-rw pour accéder à l’instance primaire. Vous comprendrez qu’une résolution DNS interne au cluster Kubernetes doit se faire pour retrouver l’adresse IP associée. La réplication utilise l’utilisateur dédié streaming_replica créé par CloudNativePG lors du déploiement de la première instance. L’authentification se fait par certificat. Le paramètre application_name permet d’indiquer un nom d’application dans les informations liée la connexion.

Se connecter avec psql au primaire.

kubectl exec -it postgresql-demo-1 -c postgres -- psql

ou, via le plugin :

kubectl cnpg psql postgresql-demo

Récupérer le contenu de la table pg_stat_replication.

postgres=# select * from pg_stat_replication\gx
-[ RECORD 1 ]----+------------------------------
pid              | 16026
usesysid         | 16386
usename          | streaming_replica
application_name | postgresql-demo-2
client_addr      | 10.244.1.11
client_hostname  | 
client_port      | 54458
backend_start    | 2025-12-12 07:13:15.829844+00
backend_xmin     | 
state            | streaming
sent_lsn         | 0/B000130
write_lsn        | 0/B000130
flush_lsn        | 0/B000130
replay_lsn       | 0/B000130
write_lag        | 00:00:00.000806
flush_lag        | 00:00:00.013548
replay_lag       | 00:00:00.014192
sync_priority    | 0
sync_state       | async
reply_time       | 2025-12-12 07:17:57.892685+00

Par défaut, c’est une réplication asynchrone qui est créée.

Récupérer le contenu de la table pg_replication_slots.

postgres=# select * from pg_replication_slots \gx
postgres=# select * from pg_replication_slots \gx
-[ RECORD 1 ]-------+------------------------
slot_name           | _cnpg_postgresql_demo_2
plugin              | 
slot_type           | physical
datoid              | 
database            | 
temporary           | f
active              | t
active_pid          | 16026
xmin                | 
catalog_xmin        | 
restart_lsn         | 0/C000060
confirmed_flush_lsn | 
wal_status          | reserved
safe_wal_size       | 
two_phase           | f
inactive_since      | 
conflicting         | 
invalidation_reason | 
failover            | f
synced              | f

Par défaut, CloudNativePG crée automatiquement un slot de réplication pour sécuriser la réplication. Son nom permet de savoir facilement à quoi il correspond.

Le slot de réplication garantit au secondaire que son primaire ne recyclera pas les journaux de transactions dont il aura encore besoin. Le secondaire peut donc prendre un retard conséquent sans risque de décrochage. Attention à l’accumulation des WALs qu’il peut y avoir sur le primaire en cas de retard ou de problème (coupure réseau, crash secondaire, etc).

Sur le primaire, créer une table dans la base app.

Sur le primaire :

postgres=# \c app
app=# CREATE TABLE ma_table (i int);
CREATE TABLE

Vérifier qu’elle se trouve également sur le secondaire.

Sur le secondaire :

postgres=# \c app
You are now connected to database "app" as user "postgres".
app=# \dt
          List of relations
 Schema |   Name   | Type  |  Owner   
--------+----------+-------+----------
 public | ma_table | table | postgres
(1 row)

La réplication fonctionne !

Emplacement des instances

L’opérateur CloudNativePG veille à déployer les instances PostgreSQL sur des nœuds différents afin de garantir la disponibilité. Cela permet de réduire les risques liés à un incident en s’assurant que toutes les instances ne sont pas affectées simultanément. Cette configuration permet également la répartition de la charge entre plusieurs nœuds pour des opérations de lecture.

Trouver le nom du nœud où est déployée chaque instance.

kubectl get pod -o wide
NAME                READY   STATUS    RESTARTS   AGE     IP            NODE           NOMINATED NODE   READINESS GATES
postgresql-demo-1   1/1     Running   0          16h     10.244.2.11   kind-worker2   <none>           <none>
postgresql-demo-2   1/1     Running   0          8m34s   10.244.1.11   kind-worker    <none>           <none>

Cette répartition est possible grâce notamment à la configuration de l’affinité et anti-affinité entre Pod ajoutée au YAML du Cluster.

  affinity:
    enablePodAntiAffinity: true 
    topologyKey: kubernetes.io/hostname 
    podAntiAffinityType: preferred

Tests de bascules

But : Tester et comprendre le mécanisme de bascule entre primaire et secondaire.

Trouver quelle est l’instance primaire du cluster postgresql-demo.

Vous devriez trouver que l’instance postgresql-demo-1 est l’instance primaire. Pour le savoir, plusieurs méthodes :

  1. Utiliser la fonction pg_is_in_recovery() en étant connecté sur une instance avec psql. Si elle retourne false, l’instance est primaire, sinon, à true, elle est secondaire.
kubectl exec -it postgresql-demo-1 -c postgres -- psql -c 'SELECT pg_is_in_recovery();'
 pg_is_in_recovery 
-------------------
 f
(1 row)

kubectl exec -it postgresql-demo-2 -c postgres -- psql -c 'SELECT pg_is_in_recovery();'
 pg_is_in_recovery 
-------------------
 t
(1 row)
  1. Grâce au plugin cnpg de kubectl et de sa commande status ;
kubectl cnpg status postgresql-demo
Cluster Summary
Name                     default/postgresql-demo
System ID:               7582605931997384732
PostgreSQL Image:        ghcr.io/cloudnative-pg/postgresql:17.5-standard-bookworm
Primary instance:        postgresql-demo-1
Primary promotion time:  2025-12-11 14:23:56 +0000 UTC (17h6m44s)
Status:                  Cluster in healthy state 
Instances:               2
Ready instances:         2
Size:                    247M
Current Write LSN:       0/E000000 (Timeline: 1 - WAL File: 00000001000000000000000E)
[...]

Bascule manuelle

Promouvoir l’instance postgresql-demo-2 comme nouvelle primaire avec le plugin cnpg de kubectl.

Attention, il y a bien un espace entre le nom du cluster et l’identifiant de l’instance.

kubectl cnpg promote postgresql-demo 2

{"level":"info","ts":"2024-12-06T10:40:18.767552528Z","msg":"Cluster is not healthy"}
Node postgresql-demo-2 in cluster postgresql-demo will be promoted

Vérifier que le cluster est en bonne santé et que postgresql-demo-2 est désormais l’instance primaire.

kubectl cnpg status postgresql-demo
[...]
Primary instance:        postgresql-demo-2
Primary promotion time:  2025-12-12 07:31:26 +0000 UTC (30s)
Status:                  Cluster in healthy state 
[...]
Instances status
Name               Current LSN  Replication role  Status  QoS        Manager Version  Node
----               -----------  ----------------  ------  ---        ---------------  ----
postgresql-demo-2  0/F001078    Primary           OK      Burstable  1.27.1           kind-worker
postgresql-demo-1  0/F001078    Standby (async)   OK      Burstable  1.27.1           kind-worker2

Les applications auront évidemment une coupure réseau, comme il y a une bascule.

Bascule automatique

Lorsqu’une erreur survient sur le primaire le mécanisme de failover va être déclenché. Ce mécanisme sera démarré après une certaine durée modifiable via le paramètre .spec.failoverDelay (par défaut à 0) dans la définition du cluster PostgreSQL.

L’erreur peut être, par exemple, un problème sur le volume associé, le Pod primaire qui serait supprimé, le conteneur PostgreSQL qui serait KO, etc… (voir la documentation sur les probes).

Ajouter le paramètre .spec.failoverDelay à votre instance et le postionner à 10 secondes.

[...]
spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:17.0
  instances: 2
  failoverDelay: 10
[...]

Le prendre en compte avec kubectl apply -f ~/postgresql-demo.yaml.

kubectl apply -f ~/postgresql-demo.yaml
cluster.postgresql.cnpg.io/postgresql-demo configured

Dans une autre session, lancer la commande watch kubectl get pod.

watch kubectl get pod

Détruire le Pod correspondant à l’instance primaire.

kubectl delete pod postgresql-demo-2

pod "postgresql-demo-2" deleted

Regarder comment réagit le Cluster.

Une bascule sur le seul Pod disponible est faite après 10 secondes. L’instance postgresql-demo-1 est automatiquement promue primaire. Dans la foulée, un Pod est recréé pour retrouver la situation initiale.

Instances status
Name               Current LSN  Replication role  Status  QoS        Manager Version  Node
----               -----------  ----------------  ------  ---        ---------------  ----
postgresql-demo-1  0/11002BF8   Primary           OK      Burstable  1.27.1           kind-worker2
postgresql-demo-2  0/11002BF8   Standby (async)   OK      Burstable  1.27.1           kind-worker

Voici un exemple avec un délai .spec.failoverDelay à 20 secondes.

Mise en place d’une sauvegarde PITR

But : Mettre en place une sauvegarde PITR sur un stockage S3 (archivage et sauvegarde complète).

Comme vous le savez certainement, il existe le concept de sauvegarde physique PITR comme mécanisme de sauvegarde d’une instance. Pour mettre en place cela, il est d’abord nécessaire de faire une sauvegarde physique de l’arborescence de l’instance. Ceci peut être fait à chaud. Le second élément essentiel est l’archivage des journaux de transactions (WAL) qui seront rejoués après une restauration pour rétablir un état cohérent.

En déployant une instance avec CloudNativePG, la seule solution de sauvegarde PITR utilisable est Barman Cloud. Très connu dans l’éco-système PostgreSQL, cet outil nous permet de faire la sauvegarde physique et l’archivage des WALs. Les commandes passées pour la mettre en place le seront de manière automatique mais une configuration doit être rajoutée dans le fichier YAML de définition.

Installation

La mise en place d’une solution de sauvegarde nécessite, depuis la version 1.26, l’installation d’un plugin dédié aux sauvegardes. Le projet CloudNativePG met à disposition le plugin Barman Cloud CNPG-I.

Nous allons donc l’installer sur notre cluster Kubernetes. Aussi, l’outil cert-manager doit être présent dans le cluster Kubernetes. Il est utilisé pour la génération de certificats pour la communication entre l’opérateur et le plugin de sauvegarde.

Installer cert-manager avec la commande suivante.

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.19.2/cert-manager.yaml

Patienter quelques minutes, le temps que cert-manager s’installe, puis installer le plugin avec la commande suivante.

kubectl apply -f https://github.com/cloudnative-pg/plugin-barman-cloud/releases/download/v0.11.0/manifest.yaml

Vérifier que le plugin est bien installé dans le Namespace cnpg-system.

kubectl get pod -n cnpg-system
NAME                                       READY   STATUS    RESTARTS   AGE
barman-cloud-6858cdc47f-gr2bh              1/1     Running   0          3m32s
cnpg-controller-manager-7b7fcf5cf6-8rmj9   1/1     Running   0          9m18s

Configuration

Créer le fichier ~/s3-creds.yaml avec le contenu suivant.

Les paramètres ACCESS_* doivent contenir l’information encodée en BASE64. Si vous utilisez la commande echo, n’oubliez l’option -n qui empêche la prise en compte du saut de ligne. Les informations vous seront données par le formateur.

---
apiVersion: v1
kind: Secret
metadata:
  name: s3-creds
type: Opaque
data:
  ACCESS_KEY_ID: bWluaW9hZG1pbg==
  ACCESS_REGION: ZnItcGFy
  ACCESS_SECRET_KEY: bWluaW9hZG1pbg==

Créer le Secret dans votre cluster Kubernetes avec la commande kubectl apply -f ~/s3-creds.yaml.

kubectl apply -f s3-creds.yaml 
secret/s3-creds createdkubectl delete -f postgresql-demo.yaml

Ce Secret contient les informations de la clé API qui permettra de s’authentifier au Bucket S3 et de déposer les WAL et les sauvegardes. Ici il s’agit de minioadmin.

Pour cette partie du TP, nous allons créer une autre instance PostgreSQL (postgresql-with-backup-demo), donc un objet de type Cluster et un nouveau nom.

Créer le fichier ~/postgresql-with-backup-demo.yaml avec le contenu suivant.

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgresql-with-backup-demo
spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:17.5-standard-bookworm
  instances: 1
  storage:
    size: 5Gi
  walStorage:
    size: 5Gi
  postgresql:
    parameters:
      shared_buffers: '256MB'
      max_connections: '10'
      work_mem: "8MB"
      archive_timeout: "20min"
  resources:
    requests:
      memory: "256Mi"
      cpu: "0.5"
    limits:
      memory: "512Mi"
      cpu: "1"
  plugins:
  - name: barman-cloud.cloudnative-pg.io
    isWALArchiver: true
    parameters:
      barmanObjectName: objectstore-demo   

La partie backup indique quel plugin de sauvegarde utilisé, ici Barman Cloud. Il est également indiqué que c’est ce plugin qui sera en charge de l’archivage des journaux de transactions grâce à le paramètre isWALArchiver à true.

Créer le fichier ~/objectstore-demo.yaml pour créer l’ ObjectStore qui sera utilisé par le nouveau Cluster. N’oubliez pas de modifier le paramètre endpointURL avec l’adresse IP du conteneur minio.

apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
  name: objectstore-demo
spec:
  configuration:
    destinationPath: "s3://cnpg/"
    endpointURL: "http://172.18.0.5:9000"
    s3Credentials:
      accessKeyId:
        name: s3-creds
        key: ACCESS_KEY_ID
      secretAccessKey:
        name: s3-creds
        key: ACCESS_SECRET_KEY
      region:
        name: s3-creds
        key: ACCESS_REGION
    wal:
      compression: gzip

La configuration des paramètres endpointURL et destinationPath devra être adaptée selon votre fournisseur de stockage S3. Les paramètres ci-dessus fonctionnent bien avec minio en local. Faites vraiment attention, vous risquerez de perdre beaucoup de temps … vraiment :).

Créer l’ObjectStore avec la commande :

kubectl apply -f ~/objectstore-demo.yaml

Configurer l’utilitaire mc pour qu’il se connecte au conteneur minio avec sa commande alias :

mc alias set minio http://172.18.0.5:9000 minioadmin minioadmin

Créer un nouveau bucket cnpg dans minio avec la commande :

mc mb minio/cnpg
Bucket created successfully `minio/cnpg`.

Créer le nouveau Cluster PostgreSQL avec la commande :

kubectl apply -f ~/postgresql-with-backup-demo.yaml

Vérifier que l’archivage se passe correctement directement dans la vue pg_stat_archiver.

kubectl cnpg psql postgresql-with-backup-demo

postgres=# \x
Expanded display is on.
postgres=# select * from pg_stat_archiver ;
-[ RECORD 1 ]------+------------------------------
archived_count     | 2
last_archived_wal  | 000000010000000000000002
last_archived_time | 2025-12-12 10:22:11.071834+00
failed_count       | 0
last_failed_wal    | 
last_failed_time   | 
stats_reset        | 2025-12-12 10:20:09.349145+00

Vérifier que des WAL soient bien présents dans le bucket cnpg avec la commande mc ls.

Regardons dossier par dossier ce qui est créé dans le bucket. Tout d’abord, le dossier postgresql-with-backup-demo/, qui reprend le nom du Cluster est bien créé dans cnpg.

mc ls minio/cnpg/
[2026-04-02 16:12:42 CEST]     0B postgresql-with-backup-demo/

… qui contient lui-même un dossier wals.

mc ls minio/cnpg/postgresql-with-backup-demo/
[2026-04-02 16:15:23 CEST]     0B wals/

Les journaux (WAL) sont enregistrés dans des dossiers qui reprennent la timeline de l’instance.

mc ls minio/cnpg/postgresql-with-backup-demo/wals/
[2026-04-02 16:15:55 CEST]     0B 0000000100000000/

Et enfin, dans ce dernier dossier, se trouvent les journaux de transaction compressés.

mc ls minio/cnpg/postgresql-with-backup-demo/wals/0000000100000000/
[2026-04-02 16:10:15 CEST] 2.3MiB STANDARD 000000010000000000000001.gz

C’est un super point de départ. Mais pour le moment, il n’est pas possible de faire quelconque restauration comme il nous manque une sauvegarde complète de l’instance.

Sauvegarde complète de l’instance

Créer le fichier ~/letsbackup.yaml avec le contenu suivant :

apiVersion: postgresql.cnpg.io/v1
kind: Backup
metadata:
  name: first-backup
spec:
  cluster:
    name: postgresql-with-backup-demo
  method: plugin
  pluginConfiguration:
    name: barman-cloud.cloudnative-pg.io

Il faut donner un nom à cet objet Backup et le nom du cluster PostgreSQL que l’on souhaite sauvegarder ainsi qu’avec quelle méthode de sauvegarde cela va être fait, ici via le plugin Barman Cloud.

Créer cette ressource avec avec kubectl.

kubectl apply -f ~/letsbackup.yaml 
backup.postgresql.cnpg.io/first-backup created

Vérifier le statut de l’objet Backup :

kubectl get backup
NAME           AGE   CLUSTER                       METHOD   PHASE     ERROR
first-backup   14s   postgresql-with-backup-demo   plugin   started

Chercher dans les traces du Pod une preuve que la sauvegarde complète s’est bien déroulée.

kubectl logs postgresql-with-backup-demo-1 | grep completed | jq
{
  "level": "info",
  "ts": "2025-12-12T10:29:49.602829931Z",
  "msg": "Backup completed",
  "pluginConfiguration": {
    "name": "barman-cloud.cloudnative-pg.io"
  },
  "backupName": "first-backup",
  "backupNamespace": "default",
  "logging_pod": "postgresql-with-backup-demo-1"
}

Observer les changements dans le conteneur minio.

Au niveau du conteneur, un nouveau dossier base est apparu à côté de wals.

mc ls minio/cnpg/postgresql-with-backup-demo/
[2026-04-02 16:18:09 CEST]     0B base/
[2026-04-02 16:18:09 CEST]     0B wals/

Il contient toutes les sauvegardes faites jusqu’à présent.

mc ls minio/cnpg/postgresql-with-backup-demo/base/
[2026-04-02 16:18:33 CEST]     0B 20260402T141657/

La sauvegarde physique se trouve dans ce dossier et comporte un fichier d’informations et une archive tar.

mc ls minio/cnpg/postgresql-with-backup-demo/base/20260402T141657/
[2026-04-02 16:17:03 CEST] 1.4KiB STANDARD backup.info
[2026-04-02 16:17:03 CEST]  32MiB STANDARD data.tar

Incroyable ! Nous avons une sauvegarde et un archivage des WALs qui semblent se dérouler correctement. Mais, qu’est ce qu’il se cache derrière cela ?

La première chose que nous pouvons chercher à savoir par exemple, est quel outil est utilisé pour archiver les journaux. Le paramètre archive_command nous donne un début de réponse.

kubectl exec -it postgresql-with-backup-demo-1 -- psql -c "SHOW archive_command"
                                  archive_command                                   
------------------------------------------------------------------------------------
 /controller/manager wal-archive --log-destination /controller/log/postgres.json %p
(1 row)

Un outil appelé manager présent dans le conteneur est utilisé avec l’option wal-archive suivie de plusieurs paramètres. %p est un placeholders qui permet d’indiquer le WAL courant.

Générer de la donnée

Se connecter à l’instance. Créer une table et insérer quelques données.

kubectl exec -it postgresql-with-backup-demo-1 -- psql

ou, via le plugin :

kubectl cnpg psql postgresql-with-backup-demo
create table t1 (i int);
insert into t1 select generate_series(1, 100);
checkpoint ;

Ne pas oublier d’exécuter l’ordre CHECKPOINT qui permettra de forcer la synchronisation des données sur disque et la création d’un point de cohérence sans attendre l’expiration de checkpoint_timeout.

Forcer la création d’un nouveau journal de transactions avec SELECT pg_switch_wal();.

postgres=# postgres=# select pg_switch_wal();
 pg_switch_wal 
---------------
 1/FFCFCAA8
(1 row)

Il devrait apparaitre dans votre Bucket S3.

Procéder à une restauration PITR

But : Restaurer notre instance depuis la sauvegarde PITR existante.

Les restaurations se font obligatoirement dans une nouvelle instance PostgreSQL. Le principe de restauration in-place n’est donc pas possible. Attention donc si vous souhaitez conserver le nom du Cluster vous devrez détruire le précédent Cluster qui porterait ce nom.

Maintenant qu’une instance est déployée et qu’une sauvegarde a été faite, attardons-nous sur les manières qui existent pour restaurer une instance.

Aussi, c’est l’occasion de faire un petit rappel ! N’oubliez pas de tester vos procédures de restauration fréquemment !

Aussi, c’est l’occasion de faire un petit rappel ! N’oubliez pas de tester vos procédures de restauration fréquemment !

Simuler un crash. Détruire l’instance postgresql-with-backup-demo (Nous sommes bien évidemment ici dans un exercice de destruction maîtrisé par des professionnels).

kubectl delete -f ~/postgresql-with-backup-demo.yaml

Créer un nouveau fichier ~/postgresql-restored-demo.yaml avec le contenu suivant. L’idée est de créer une nouvelle instance postgresql-restored-demo et d’indiquer avec la section bootstrap qu’elle doit démarrer à partir d’une sauvegarde.

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgresql-restored-demo
spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:17.5-standard-bookworm
  instances: 1
  storage:
    size: 5Gi
  walStorage:
    size: 5Gi
  postgresql:
    parameters:
      shared_buffers: '256MB'
      max_connections: '10'
      work_mem: '8MB'
      archive_timeout: '20min'
  resources:
    requests:
      memory: "256Mi"
      cpu: "0.5"
    limits:
      memory: "512Mi"
      cpu: "1"
  bootstrap:
    recovery:
      source: source
  externalClusters:
  - name: source
    plugin:
      name: barman-cloud.cloudnative-pg.io
      parameters:
        barmanObjectName: objectstore-demo
        serverName: postgresql-with-backup-demo
  plugins:
  - name: barman-cloud.cloudnative-pg.io
    isWALArchiver: true
    parameters:
      barmanObjectName: objectstore-demo # réutilisation du bucket S3

L’emplacement de la sauvegarde est indiqué dans l’attribut source de la section bootstrap.recovery. Il fait référence à un externalClusters qui contient les informations de l’ObjectStore utilisé et présent dans le cluster Kuberetes.

Créer votre nouvelle instance avec kubectl apply -f ~/postgresql-restored-demo.yaml.

kubectl apply -f  ~/postgresql-restored-demo.yaml

Lorsque l’instance est prête, s’y connecter et vérifier que les données s’y trouvent bien.

kubectl exec -it postgresql-restored-demo-1 -c postgres -- psql -c "select count(*) from t1;"
 count 
-------
   100
(1 row)

La méthode que nous venons de suivre, suppose que vous ayez accès à l’objet ObjectStore créé dans le cluster Kubernetes. Mais qu’en est-il si c’est tout le cluster Kubernetes qui est en panne et doit être recréé ?

Dans ce cas-là, l’objet ObjectStore n’existe plus. Soit vous le recréez, soit vous renseigner directement les informations de connexion au stockage S3.

Du côté du Bucket S3, un nouveau dossier est automatiquement créé avec le nom du nouvel objet Cluster. Cela est du à la partie plugins dans laquelle nous avons indiquer de réutiliser l’ObjectStore précédent.

mc ls minio/cnpg
[2026-04-02 16:22:02 CEST]     0B postgresql-restored-demo/
[2026-04-02 16:22:02 CEST]     0B postgresql-with-backup-demo/

Dans cet exemple, la restauration s’est faite sur le même cluster Kubernetes. Dans le cas où vous devez la faire ailleurs, n’oubliez pas de recréer le Secret qui contient les informations de l’API Key nécessaire à l’accès au stockage S3.

Supprimer les instances postgresql-demo qui ne vont plus nous servir par la suite.

kubectl delete -f postgresql-demo.yaml

Il ne doit rester que le cluster PostgreSQL postgresql-restored-demo.

kubectl get pod
NAME                         READY   STATUS    RESTARTS      AGE
postgresql-restored-demo-1   1/1     Running   2 (26m ago)   2d15h

Montée de version mineure de PostgreSQL

But : Effectuer une montée de version mineure de PostgreSQL.

La version de PostgreSQL est indiquée dans le nom et le tag de l’image déployée. La modification de celle-ci entraîne une montée de version de l’instance. Cette montée de version peut se faire automatiquement ou de manière supervisée (appelée “manuelle” dans la documentation).

Méthode unsupervised

Dans une autre session SSH, lancer la commande watch kubectl get pods pour voir ce qu’il se passe pendant la montée de version.

Vous devriez voir la chose suivante avec un rafraichissement toutes les 2 secondes.

Every 2.0s: kubectl get pods         scw-boring-keller: Mon Nov 25 08:40:03 2024

NAME                         READY   STATUS    RESTARTS      AGE
postgresql-restored-demo-1   1/1     Running   2 (46m ago)   2d16h

Modifier la version de PostgreSQL de 17.5 à 17.6 dans le fichier ~/postgresql-restored-demo.yaml et appliquer la modification avec kubectl apply.

spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:17.6-standard-bookworm
kubectl apply -f ~/postgresql-restored-demo.yaml
cluster.postgresql.cnpg.io/postgresql-restored-demo configured

Le Pod de l’instance en version 17.5 est supprimé.

Un nouveau Pod est créé.

Le temps de télécharger l’image et de le redémarré, il passe à l’état Running.

Un select version() indique que nous sommes bien passés en version 17.6.

Par défaut, une instance PostgreSQL est déployée de telle sorte que la montée de version se fasse automatiquement, c’est-à-dire que l’opérateur arrête puis redémarre l’instance tout seul. Voyons maintenant le cas d’une montée de version en mode supervised.

Méthode supervised

Le paramètre primaryUpdateStrategy permet de définir la stratégie à suivre lors d’une mise à jour de l’instance primaire. Il est positionné par défaut à unsupervised. C’est le comportement que nous venons de voir avec l’exemple précédent.

Positionner le paramètre spec.primaryUpdateStrategy à supervised, modifier la version en la passant de 17.6 à 17.7 et tenter de faire la montée de version. Que constatez vous ?

spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:17.7-standard-bookworm
  instances: 1
  primaryUpdateStrategy: supervised
kubectl apply -f ~/postgresql-restored-demo.yaml 

Aïe …

The Cluster "postgresql-restored-demo" is invalid: spec.primaryUpdateStrategy:
Invalid value: "supervised": supervised update strategy is not allowed for clusters 
with a single instance

Nous venons de découvrir une première subtilité. Ce paramètre n’est en réalité pas utilisable avec une seule instance. En effet ce paramètre permet de contrôler la manière dont est redémarrée l’instance primaire après que toutes les instances secondaires aient été mis à jour. Ici, nous n’avons qu’une primaire de déployée.

Ceci nous permet donc de voir redéployer un secondaire. Rien de plus simple.

Ajouter un secondaire à votre cluster PostgreSQL en modifiant la ligne instances du fichier postgresql-restored-demo.yaml et en appliquant la modification.

[...]
spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:17.7-standard-bookworm
  instances: 2
[...]

Un premier Pod join va être créé puis le Pod de l’instance secondaire sera finalement déployé. Nous nous retrouvons donc avec deux Pods reprenant le nom du cluster, incrémentés de 1.

kubectl get pod
NAME                                    READY   STATUS    RESTARTS   AGE
postgresql-restored-demo-1              2/2     Running   0          6m16s
postgresql-restored-demo-2-join-7ckvl   0/1     Pending   0          5s
kubectl get pod
NAME                                    READY   STATUS      RESTARTS   AGE
postgresql-restored-demo-1              1/1     Running     0          61m
postgresql-restored-demo-2              1/1     Running     0          20s

Vérifier les versions des deux instances. Que constatez-vous ?

Comme nous avons modifié la version de l’image en 17.7, l’instance secondaire qui vient d’être déployée est bien dans cette version. La version du primaire est quant à elle restée en 17.6, ce qui est normal comme nous sommes dans une montée de version supervisée.

# primaire
kubectl exec -it postgresql-restored-demo-1 -c postgres -- psql -c 'show server_version;'
        server_version         
-------------------------------
 17.6 (Debian 17.6-2.pgdg12+1)
(1 row)
# secondaire
kubectl exec -it postgresql-restored-demo-2 -c postgres -- psql -c 'show server_version;'
        server_version         
-------------------------------
 17.7 (Debian 17.7-3.pgdg12+1)
(1 row)

Il nous reste donc à signifier à CloudNativePG que le primaire peut être mis à jour à son tour. Pour cela, il faut passer par le plugin CloudNativePG de kubectl et procéder à une bascule sur le secondaire qui deviendra le nouveau primaire avec la ligne de commande : kubectl cnpg promote [cluster] [new_primary].

Faite une bascule manuelle sur l’instance secondaire.

kubectl cnpg promote postgresql-restored-demo postgresql-restored-demo-2

L’ancien secondaire est désormais primaire comme on peut le voir dans la sortie de kubectl cnpg status postgresql-restored-demo qui donne l’état du cluster PostgreSQL, en particulier, avec la ligne Primary instance: postgresql-restored-demo-2, ou encore dans le tableau.

Instances status
Name                        Current LSN  Replication role  Status  QoS        Manager Version  Node
----                        -----------  ----------------  ------  ---        ---------------  ----
postgresql-restored-demo-2  0/D001108    Primary           OK      Burstable  1.27.1           kind-worker
postgresql-restored-demo-1  0/D001108    Standby (async)   OK      Burstable  1.27.1           kind-worker2

Les deux instances sont bien en version 17.7.

kubectl exec -it postgresql-restored-demo-1 -c postgres -- psql -c 'show server_version;'
        server_version         
-------------------------------
 17.7 (Debian 17.7-3.pgdg12+1)
(1 row)

Il existe d’autres cas où le redémarrage des instances est nécessaire. Par exemple, si un paramètre comme max_connections ou shared_buffers a été modifié. Dans ce cas-là, si vous faites toujours une montée de version supervisée, il vous est possible de ne pas faire de bascule mais simplement de redémarrer l’instance primaire avec la ligne de commande kubectl cnpg restart [cluster] [current_primary];

Montée de version majeure de PostgreSQL

But : Effectuer une montée de version majeure de PostgreSQL.

Là aussi, la version majeure de PostgreSQL est indiquée dans le nom et le tag de l’image déployée. La modification de celle-ci entraîne le déclenchement d’une In-Place Major Upgrade. Ce mécanisme se fait nécessairement sur des instances arrêtées. L’opérateur CloudNativePG les arrêtera pour vous.

Pour cet exercice, nous allons mettre à jour un Cluster en version 16.11 vers la version 18.1.

Créer le fichier ~/postgresql-16-to-18.yaml et ajouter le contenu suivant puis appliquer le avec kubectl.

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgresql-test
spec:
  instances: 2
  imageName: ghcr.io/cloudnative-pg/postgresql:16.11-standard-bookworm
  storage:
    size: 1Gi
kubectl apply -f ~/postgresql-16-to-18.yaml

Vérifier que les deux instances soient correctement déployées.

kubectl get pod
NAME                READY   STATUS    RESTARTS   AGE
postgresql-test-1   1/1     Running   0          38s
postgresql-test-2   1/1     Running   0          20s

Créer une table et y insérer quelques lignes.

kubectl cnpg psql postgresql-test -- -c "CREATE TABLE t1 (c1 INTEGER PRIMARY KEY, c2 text)";
CREATE TABLE
kubectl cnpg psql postgresql-test -- -c "INSERT INTO t1 SELECT generate_series(1,100), generate_series(1,100)::text";
INSERT 0 100

Ouvrir un nouveau terminal et lancer la commande kubectl get pod --watch.

Ouvrir un nouveau terminal et lancer la commande kubectl get pv --watch.

Ouvrir un nouveau terminal et lancer la commande kubectl get pvc --watch.

Modifier le tag du paramètre imageName du fichier ~/postgresql-16-to-18.yaml en le passant de 16.11 à 18.1 puis appliquer la modification avec kubectl.

kubectl apply -f ~/postgresql-16-to-18.yaml
cluster.postgresql.cnpg.io/postgresql-test configured

Observez ce qu’il se passe dans les différents terminaux que vous avez ouvert.

Les deux instances passent en Terminating

postgresql-test-1   1/1     Terminating   0          4m26s
postgresql-test-2   1/1     Terminating   0          4m8s

La montée de version est déclenchée et un nouveau Pod apparait :

postgresql-test-1-major-upgrade-7hl8w   0/1     Init:0/2          0          0s
postgresql-test-1-major-upgrade-7hl8w   0/1     Init:1/2          0          1s
postgresql-test-1-major-upgrade-7hl8w   0/1     PodInitializing   0          2s
postgresql-test-1-major-upgrade-7hl8w   1/1     Running           0          18s
postgresql-test-1-major-upgrade-7hl8w   0/1     Completed         0          45s

Lorsque qu’il a terminé de s’exécuté, le Pod correspondant à l’instance primaire est relancé :

postgresql-test-1                       0/1     Pending           0          0s
postgresql-test-1                       0/1     Init:0/1          0          0s
postgresql-test-1                       0/1     PodInitializing   0          0s
postgresql-test-1                       0/1     Running           0          10s
postgresql-test-1                       1/1     Running           0          10s

À ce moment là, le volume (PV) qui existait et qui était rattaché à l’ancienne instance secondaire postgresql-test-2 sont supprimés.

pvc-23b021b2-163b-41e3-9791-721b8635bee5   1Gi        RWO            Delete           Released   default/postgresql-test-2   standard       <unset>                          5m3s
pvc-23b021b2-163b-41e3-9791-721b8635bee5   1Gi        RWO            Delete           Terminating   default/postgresql-test-2   standard       <unset>                          5m6s

Même chose pour le PVC :

postgresql-test-2   Terminating   pvc-23b021b2-163b-41e3-9791-721b8635bee5   1Gi        RWO            standard       <unset>                 5m5s

Pour respecter ce qui est demandé dans la définition YAML, une instance secondaire postgresql-test-3 est créée et va se joindre à la nouvelle instance primaire postgresql-test-1. Cela se fait par le déploiement d’un Pod intermédiaire suffixé par -join-XXXXX. Il sera détruit à la fin de l’opération.

postgresql-test-3-join-tcrbs            0/1     Pending           0          0s
postgresql-test-3-join-tcrbs            0/1     Init:0/1          0          5s
postgresql-test-3-join-tcrbs            0/1     PodInitializing   0          6s
postgresql-test-3-join-tcrbs            1/1     Running           0          7s
postgresql-test-3-join-tcrbs            0/1     Completed         0          10s
postgresql-test-3                       0/1     Pending           0          0s
postgresql-test-3                       0/1     Init:0/1          0          0s
postgresql-test-3                       0/1     PodInitializing   0          0s
postgresql-test-3                       0/1     Running           0          10s
postgresql-test-3                       1/1     Running           0          10s
postgresql-test-3-join-tcrbs            0/1     Terminating       0          22s

Vérifier que les données soient toujours présentes.

kubectl cnpg psql postgresql-test -- -c "SELECT count(*) FROM t1;"
 count 
-------
   100
(1 row)

Vérifier la version de PostgreSQL.

kubectl cnpg psql postgresql-test -- -c "show server_version;"        
        server_version         
-------------------------------
 18.1 (Debian 18.1-1.pgdg12+2)
(1 row)

Montée de version de l’opérateur

But : Effectuer une montée de version de l’opérateur CNPG.

Nous avons déployé la version 1.27.1 de l’opérateur. Nous allons nous intéresser à la manière de le mettre à jour.

Lorsqu’une nouvelle version de l’opérateur est déployée, un nouveau Pod se crée. Lorsque celui-ci est prêt, l’ancien opérateur est tout simplement supprimé. Cette mise à jour déclenche également la mise à jour d’un composant présentant dans les Pods des instances PostgreSQL.

Lorsqu’un Pod PostgreSQL est déployé, un InitContainer est créé en amont et permet de récupérer du code correspondant à l’instance-manager. Il permet de contrôler l’instance, son cycle, ses redémarrages, etc. La version de ce manager est étroitement liée à la version de l’opérateur. Pour information, c’est ce processus qui va lancer PostgreSQL et qui aura le pid 1 dans le Pod.

cat /proc/1/cmdline 
/controller/manager instance run--status-port-tls--log-level=info

Attention si vous avez utilisé votre opérateur pour déployer plusieurs instances PostgreSQL, lorsque vous mettez à jour l’opérateur, tous les Pods seront, mis à jour en même temps (ou quasiment). Il y aura donc une coupure de service pour chaque instance. C’est le fonctionnement par défaut.

Dans une première console, lancer la commande watch suivante :

watch kubectl get pod

Dans une seconde console, lancer la commande watch suivante :

watch kubectl get pod -n cnpg-system

Dans une autre console, appliquer les fichiers YAML correspondant à la version 1.28.0 de l’opérateur.

kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.28/releases/cnpg-1.28.0.yaml

Regarder ce qu’il se passe au niveau des différents Pods (opérateur et PostgreSQL)

Les instances PostgreSQL déployées par l’opérateur sont redémarrées lors d’une montée de version de l’opérateur. Selon la configuration des instances, une opération manuelle sera nécessaire pour mettre à jour le primaire.

Il existe une méthode pour éviter ce comportement. Cependant elle ne garantit pas le critère immuable que devrait suivre un Pod.

À titre d’information, voici la méthode à suivre pour y parvenir. Pour cela il faut modifier la configuration de l’opérateur en passant le paramètre ENABLE_INSTANCE_MANAGER_INPLACE_UPDATES à true. Cela permettra de mettre à jour le manager sans pour autant redémarrer le Pod complet. Cette configuration doit être faite dans un objet ConfigMap.

Exercices optionnels

But : Découvrir des fonctionnalités plus complexes.

Mise en place de sauvegardes programmées

Il est possible de programmer des sauvegardes régulières avec la ressource ScheduledBackup.

Voici un exemple de définition qui permet de déclencher une sauvegarde appelée backup-every-day tous les jours à 16h00 pour le cluster postgresql-restored-demo :

apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
  name: backup-every-day
spec:
  schedule: "0 42 18 * * *"
  backupOwnerReference: self
  cluster:
    name: postgresql-restored-demo
  method: plugin
  pluginConfiguration: 
    name: barman-cloud.cloudnative-pg.io

Attention, l’option schedule prend bien six paramètres (le premier étant les secondes), contrairement au CronJob dans Kubernetes ou aux lignes de /etc/crontab qui en prenne que cinq.

Créer le fichier ~/backup-every-day.yaml avec le contenu ci-dessus en modifiant l’heure d’exécution pour que la sauvegarde s’exécute dans 5 à 10 minutes.

Créer l’objet ScheduledBackup avec kubectl.

kubectl apply -f ~/backup-every-day.yaml 
scheduledbackup.postgresql.cnpg.io/backup-every-day created

Suivez les traces de l’opérateur avec kubectl logs -n cnpg-system cnpg-controller-manager-6b9f78f594-hd5qq | grep backup | jq. Vous devriez voir le déclenchement de la sauvegarde.

kubectl logs -n cnpg-system cnpg-controller-manager-6b9f78f594-hd5qq | grep backup | jq
{
  "level": "info",
  "ts": "2025-12-15T18:42:00.095050921Z",
  "msg": "Starting backup",
  "controller": "backup",
  "controllerGroup": "postgresql.cnpg.io",
  "controllerKind": "Backup",
  "Backup": {
    "name": "backup-every-day-20251215184200",
    "namespace": "default"
  },
  "namespace": "default",
  "name": "backup-every-day-20251215184200",
  "reconcileID": "65490b4b-415b-41d2-be86-b1bab33de9b4",
  "cluster": "postgresql-restored-demo",
  "pod": "postgresql-restored-demo-1"
}

Vous verrez alors la sauvegarde sur votre emplacement de stockage S3.

Mettre en place une réplication synchrone

Il existe plusieurs méthodes pour mettre en place une réplication synchrone. Le but ici n’est pas de les évoquer ni de les comparer, mais simplement de voir le principe de la configuration.

Pour l’exemple, nous mettrons en place la méthode par Quorum.

Modifier la configuration de l’instance en rajoutant le bloc suivant à votre fichier ~/postgresql-restored-demo.yaml.

  postgresql:
    synchronous:
      method: any
      number: 1
kubectl apply -f postgresql.yaml 
cluster.postgresql.cnpg.io/postgresql-demo configured

Vérifier que la réplication est synchrone en regardant le champ sync_state de la vue pg_stat_replication de l’instance primaire.

postgres=# select * from pg_stat_replication\gx 
-[ RECORD 1 ]----+------------------------------
pid              | 1210
usesysid         | 16386
usename          | streaming_replica
application_name | postgresql-restored-demo-1
client_addr      | 10.244.3.29
client_hostname  | 
client_port      | 51518
backend_start    | 2025-12-15 18:29:04.335147+00
backend_xmin     | 
state            | streaming
sent_lsn         | 0/D000000
write_lsn        | 0/D000000
flush_lsn        | 0/D000000
replay_lsn       | 0/D000000
write_lag        | 
flush_lag        | 
replay_lag       | 
sync_priority    | 1
sync_state       | quorum
reply_time       | 2025-12-15 18:52:30.849357+00

sync_state est bien à quorum.

Plus d’informations sur la documentation.

Déploiement d’une application pgAdmin4

Pour voir comment une application peut se connecter à une instance, nous allons déployer pgAdmin dans le cluster Kubernetes.

Ouvrir une nouvelle session SSH. Passer en tant qu’utilisateur dalibo.

Créer le fichier ~/pgadmin.yaml avec le contenu suivant et le déployer dans le cluster Kubernetes.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pgadmin
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pgadmin
  template:
    metadata:
      labels:
        app: pgadmin
    spec:
      containers:
        - name: pgadmin
          image: dpage/pgadmin4
          ports:
            - containerPort: 80
          env:
            - name: PGADMIN_DEFAULT_EMAIL
              value: admin@example.com
            - name: PGADMIN_DEFAULT_PASSWORD
              value: admin
kubectl apply -f ~/pgadmin.yaml 
deployment.apps/pgadmin created

Récupérer le nom du Pod pgAdmin déployé, et lancer la commande suivante :

kubectl port-forward --address 0.0.0.0 pgadmin-*****-***** 8888:80

Cette commande permet de forwarder le trafic entrant sur le port TCP 8888 de la machine vers le port 80 du Pod pgAdmin, rendant ainsi accessible l’application. Cette méthode reste valide pour des démonstrations, n’allez pas mettre ça en production :).

Accéder à l’interface de pgAdmin via votre navigateur http://adresseippublique:8888 et connectez vous à l’interface (admin@example.com / admin).

L’adresse IP publique de la machine peut être retrouvée avec la commande suivante :

ip -f inet addr show ens2
2: ens2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    altname enp0s2
    inet 51.158.67.253/32 metric 100 scope global dynamic ens2
       valid_lft 842sec preferred_lft 842sec

Créer une nouvelle connexion avec les informations suivantes :

  • Name : postgresql-demo (Onglet General) ;
  • Host name/address : postgresql-demo-rw (Onglet Connection) ;
  • Port : 5432 ;
  • Username : app;
  • Password : celui récupéré dans le Secret;
  • Cliquer sur Save.

Créer une nouvelle connexion avec cette fois-ci postgresql-demo-ro comme paramètre Host name/address et créer une table CREATE TABLE ma_table (i int);.

Cette commande ne pourra pas être exécutée comme le Service renvoie sur une instance secondaire qui est nécessairement en lecture seule. Le message d’erreur sera :

CREATE TABLE ma_table (i int);
ERROR:  cannot execute CREATE TABLE in a read-only transaction 

SQL state: 25006

Vous avez maintenant l’application pgAdmin déployée dans Kubernetes qui a accès à l’instance postgresql-demo. L’exemple ci-dessus montre comment une application peut accéder à une base de données en utilisant la ressource Service prévue à cet effet. Application et base se trouvent toutes deux dans Kubernetes. Accéder à l’instance PostgreSQL depuis une application externe (i.e déployée ailleurs) est plus complexe, demande le déploiement d’autres ressources … mais ne sera pas traité dans ce workshop.