Blog | WeScale

PCA multi-région avec pgEdge : un POC actif-actif sur Kubernetes

Rédigé par Benjamin Rabiller | 16/09/2026

Résumé : Construire un Plan de Continuité d'Activité (PCA) multi-région avec pgEdge Distributed Postgres et son chart Helm officiel : un POC complet sur deux clusters kind, avec application de test, observabilité de la réplication Spock et un regard critique sur les forces et les faiblesses de l'approche active-active.

L'intégralité des fichiers (configs kind, values Helm, manifestes, application de test, scripts) est disponible dans le repo wescale/pgedge-pca-poc.

pgEdge, la rolls du PCA pgSQL ?

Avoir un cluster PostgreSQL hautement disponible repose sur un schéma tout à fait classique primaire/standby : une instance active, d'autres instances passives alimentées par réplication physique (streaming ou restauration de WAL (Write-Ahead Logging) depuis un object store). Ce modèle fonctionne bien et est bien outillé. Sur Kubernetes, CloudNativePG le gère nativement avec ses replicaset clusters.

Mais lorsque l'on souhaite adresser un PCA multi région, les choses deviennent plus complexes. Le setup précédent actif/passif possède un inconvénient majeur. Comment gérer la bascule vers la seconde région ? Comment éviter le split brain ?

Une approche élégante est le passage en actif-actif : chaque région possède un node Postgres accessible en lecture et en écriture, et les écritures sont répliquées logiquement vers les autres nodes. En cas de perte d'une région, il n'y a aucune instance à promouvoir : l'autre région fonctionne déjà. Le Recovery Time Objective (RTO) se réduit au temps de re-routage du trafic (DNS, anycast, etc), et le Recovery Point Objective (RPO) correspond au lag de réplication logique au moment de l'incident. Ce lag est lié à la réplication qui reste asynchrone.

C'est exactement ce que propose pgEdge depuis fin 2025. Son chart Helm s'appuie sur CloudNativePG (CNPG). L'opérateur est devenu standard pour Postgres sur Kubernetes.

pgEdge en deux mots

pgEdge Distributed Postgres est un PostgreSQL standard (16, 17 ou 18) enrichi d'extensions. Il est désormais entièrement open source sous licence PostgreSQL :

  • Spock v5 : Réplication logique multi-master (active-active), gestion de conflits, replication sets, failover slots
  • Snowflake : Génération de séquences uniques distribuées pour prévenir les collisions SERIAL en multi-master (à intégrer dès la conception d'un cluster actif/actif).
  • Chart Helm pgedge : Orchestre N nodes pgEdge, chacun étant un cluster CloudNativePG (primaire + standbys), et initialise la topologie Spock

Le point d'architecture clé : chaque node pgEdge est un cluster CNPG complet. La haute disponibilité intra-région est assurée par la réplication physique de CNPG (failover automatique en secondes), configurée ici en mode synchrone. La réplication gérée par CNPG est asynchrone par défaut, c'est un choix explicite. La réplication inter-régions, elle, est logique et asynchrone via Spock.

Spock v5 apporte les failover slots et le delayed feedback : si le primaire d'un node CNPG bascule sur un standby, les slots de réplication logique survivent et la topologie active-active se rétablit sans intervention.

Architecture du POC

On simule deux régions avec deux clusters kind. Les deux clusters partagent le réseau Docker kind, ce qui permet de simuler la connectivité inter-régions avec MetalLB (IPs stables) et un patch CoreDNS (résolution croisée). En production, ces deux rôles seraient tenus par un réseau inter-régions (VPC peering, Cilium ClusterMesh, Submariner) et un vrai DNS.

Ci-dessous le cheminement d'une écriture qui illustre la combinaison des deux niveaux de réplication :

Et le scénario PCA proprement dit :

Mise en place

Prérequis

Le chart pgEdge nécessite deux opérateurs préinstallés dans chaque cluster : CloudNativePG (qui gère les clusters Postgres) et cert-manager (qui émet l'autorité de certification et les certificats clients des utilisateurs managés). Pour le contexte kind, deux briques complémentaires sont nécessaires : MetalLB, avec un pool d'IPs différents par cluster pour exposer les primaires, et un patch CoreDNS qui résout les hostnames inter-clusters (n1.pgedge.local, n2.pgedge.local) vers les IPs MetalLB dans les deux clusters.

Déploiement en deux temps

Le chart s'installe depuis le repo officiel :

helm repo add pgedge https://pgedge.github.io/charts

Le déploiement doit suivre un ordre précis en deux phases. D'abord la région A, sans initialiser Spock (initSpock: false) puisque n2 n'existe pas encore. Puis la région B, avec initSpock: true : c'est à ce moment que le job init-spock crée la topologie de réplication croisée sur les deux nodes.

Quatre options du chart portent l'ensemble de la logique multi-cluster :

  • initSpock : Active le job d'initialisation Spock (région B uniquement)
  • provisionCerts : Émet la CA cliente via cert-manager (région A uniquement)
  • externalNodes : Déclare les nodes distants et leurs endpoints de réplication
  • internalHostname : Service cluster-local utilisé pour les health-checks internes

L'extrait de values suivant illustre la configuration de la région A :

pgEdge:
  appName: pgedge
  initSpock: false            # n2 n'existe pas encore
  provisionCerts: true        # la CA cliente est émise ici, une seule fois

  nodes:
    - name: n1
      hostname: n1.pgedge.local         # endpoint vu par n2 pour la réplication
      internalHostname: pgedge-n1-rw    # service local pour les health-checks
      clusterSpec:
        instances: 3
        postgresql:
          synchronous: { method: any, number: 1, dataDurability: required }

  externalNodes:
    - name: n2
      hostname: n2.pgedge.local

  extraResources:
    - apiVersion: v1
      kind: Service
      metadata:
        name: pgedge-n1-external
        annotations:
          metallb.universe.tf/loadBalancerIPs: 172.18.255.200
      spec:
        type: LoadBalancer
        selector: { cnpg.io/cluster: pgedge-n1, cnpg.io/instanceRole: primary }
        ports: [{ name: postgres, port: 5432, targetPort: 5432 }]

  clusterSpec:
    certificates:
      serverAltDNSNames: [n1.pgedge.local]
    monitoring:
      enablePodMonitor: true

À noter que le Service LoadBalancer sera déclaré dans les extraResources à la place d'être appliqué séparément après l'installation.

Partage des certificats entre clusters

Les utilisateurs managés (app, admin, pgedge pour la réplication, streaming_replica pour la réplication physique intra-CNPG) s'authentifient par certificat client. Les deux régions doivent donc partager la même CA cliente et les mêmes certificats, émis une seule fois par la région A.

Cinq secrets sont à copier de la région A vers la région B : client-ca-key-pair, pgedge-client-cert, admin-client-cert, app-client-cert et streaming-replica-client-cert. Une fois la copie effectuée, on vérifie par différence que seuls les secrets légitimes divergent entre les deux namespaces (les secrets serveur par node, propres à chaque cluster CNPG).

À noter pour un setup en production : Un simple kubectl get secret | kubectl apply convient pour un POC. En production, cette commande fait transiter des clés privées entre clusters : idéalement on pensera à utiliser un outil dédié (External Secrets Operator + Vault par exemple).

Initialisation du schéma applicatif

La gestion globale du DDL et du DCL par pgEdge

Avec les versions récentes de pgEdge, la réplication automatique de la structure et de la sécurité (Auto-DDL / DDX) est nativement active. Contrairement aux limitations historiques de la réplication logique "standard", pgEdge ne se contente pas de propager la structure des tables : il capture et réplique l'intégralité du cycle de vie des objets, incluant la gestion des rôles (CREATE ROLE), des propriétaires (ALTER OWNER) et des permissions (GRANT/REVOKE).

Cela signifie qu'un script d'initialisation exécuté sur un nœud unique (le nœud principal de la région A) est automatiquement et entièrement rejoué sur les autres nœuds du cluster. Les tables créées sont également ajoutées au replication set sans intervention manuelle.

-- À EXÉCUTER UNIQUEMENT SUR LE NŒUD 1 (La propagation est automatique)

-- 1. Le rôle applicatif est créé et répliqué partout
CREATE ROLE app WITH LOGIN;

-- 2. La table se propage automatiquement via l'Auto-DDL
CREATE TABLE public.messages (
  id         uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  region     text NOT NULL,
  content    text NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);

-- 3. Le changement de propriétaire est également répliqué
ALTER TABLE public.messages OWNER TO app;

L'attribution du propriétaire (ALTER TABLE ... OWNER TO app) est indispensable pour que l'application puisse interagir avec la base. Grâce au moteur de réplication de pgEdge, cette commande (ainsi que les éventuels GRANT) est interceptée comme une modification et est appliquée sur le nœud 2.

Et la gestion des clés primaires ?

Ça ne vous aura pas échappé mais multi master rime souvent avec casse-tête de l'auto-incrément pour les clés primaires. Celle de la table est un uuid DEFAULT gen_random_uuid().

Pourquoi ce choix ? En actif-actif, les écritures peuvent se faire sur les deux régions, donc un BIGSERIAL classique produirait des collisions entre régions.

Une autre alternative pgEdge est l'extension Snowflake (IDs 64 bits uniques par nœud). C'est préférable si vous tenez aux clés triables (ce qui peut être le cas pour certains CMS par exemple).

L'application de test

Grâce à mon ami Claude, j'ai généré une petite app Flask qui est déployée dans chaque région, connectée à son node local en mTLS (certificat client de l'utilisateur app). Elle expose une page HTML avec un formulaire d'injection et la liste des derniers messages, plus une API JSON (GET/POST /api/messages, GET /healthz).

Chaque ligne porte sa région d'origine, et le bandeau indique le node Spock auquel l'app est connectée. On peut donc voir la réplication se faire en instantané.

Scénario 1 : Perte de la région A

On éteint toute la "région" A en stoppant ses conteneurs. La région B continue de fonctionner sans interruption ni reconfiguration : Le RTO applicatif est de 0 pour les utilisateurs. Pendant la panne, le slot de réplication vers n1 retient le WAL sur n2. C'est la métrique retained_wal_bytes qu'il faut surveiller.

Au retour à la vie de la région A, CNPG reconstruit son quorum, Spock rejoue le backlog, et les messages écrits pendant la panne apparaissent en région A. RPO sur ce scénario : zéro ligne perdue, car la région défaillante était la destination du backlog.

Le cas défavorable est l'inverse : des transactions commitées sur A mais pas encore appliquées sur B au moment de la perte définitive de A sont perdues. Le RPO réel est le lag de réplication, d'où l'importance de le mesurer en continu.

Scénario 2 : La gestion d'un conflit

En mettant à jour la même ligne simultanément dans les deux régions, les deux UPDATE sont appliqués localement. Spock détecte le conflit et le résout par défaut en last-update-wins (timestamp de commit) : la version au commit le plus récent l'emporte sur les deux nodes. A condition d'avoir activé spock.save_resolutions (désactivé par défaut), chaque résolution est tracée dans spock.resolutions. Aucune erreur n'est remontée à l'application. Cela reste cohérent et déterministe, MAIS c'est une sémantique que les équipes de dev doivent connaître et accepter. A noter : track_commit_timestamp doit être à on pour que la résolution par timestamp fonctionne.

Au passage, cela répond à la question du split-brain de l'introduction, en la déplaçant plutôt qu'en la supprimant. L'actif-passif fait porter la partition réseau sur la disponibilité alors que l'actif-actif la fait porter sur la cohérence. Les divergences étant réconciliées après coup par le last-update-wins. Un problème de disponibilité qui se transforme en un problème de cohérence de données.

Observabilité de la stack

Un PCA, il faut le superviser au risque de décevoir le jour J… CNPG fournit déjà une base solide qu'il faut simplement compléter par des métriques spécifiques à Spock :

Les métriques CNPG natives

Chaque instance Postgres expose un exporter sur le port 9187. clusterSpec.monitoring.enablePodMonitor: true dans les values crée les PodMonitors. On y trouve l'état des clusters, le lag de streaming intra-région, les WAL, les checkpoints. Le dashboard Grafana officiel CNPG (ID 20417) les couvre.

Les métriques Spock

Via les custom queries de l'exporter. C'est le cœur de la supervision du PCA. Une ConfigMap référencée par customQueriesConfigMap dans les values ajoute quatre nouvelles métriques :

Métrique Utilité
cnpg_spock_subscriptions_enabled La réplication active-active est-elle en marche ?
cnpg_spock_replication_slots_active La région distante consomme-t-elle son slot ?
cnpg_spock_replication_slots_retained_wal_bytes Combien de WAL s'accumule si la région distante est sourde ?
cnpg_spock_apply_lag_last_commit_age_seconds Quel est mon RPO effectif, en secondes ?

Les alertes

Une PrometheusRule décline ces métriques en alertes orientées PCA : souscription désactivée (critique - le RPO se dégrade en continu), slot inactif (région distante injoignable), WAL retenu au-delà d'1 Gio (risque de saturation du volume), et lag d'application supérieur à 60 s.

Point important : Le lag de réplication EST votre RPO. Dans une architecture active-active asynchrone, l'engagement de RPO que vous pouvez contractualiser est le lag Spock observé en P99 (percentiles 99). C'est la métrique à mettre en SLO, à grapher, et à tester sous une charge réaliste.

Forces et faiblesses

Six faiblesses qui méritent d'être développées, parce qu'elles conditionnent l'utilisation de pgEdge :

1. Plan de Continuité d'Activité (PCA)

  • Forces : RTO quasi nul : Il n'y a rien à promouvoir manuellement en cas de panne, éliminant ainsi le besoin d'une procédure complexe de bascule (failover) de la base de données.
  • Faiblesses : RPO non nul : Par design, le RPO reste dépendant du décalage (lag) de la réplication asynchrone entre les clusters. Il faudra donc prendre soin de monitorer et alerter sur cette métrique (replicationLag)

2. Cohérence des Données

  • Forces : La résolution des conflits est déterministe et entièrement journalisée.
  • Faiblesses : Fonctionnement en Last-update-wins (la dernière écriture l'emporte) de manière silencieuse : Absence de transactions distribuées et de cohérence forte globale.

3. Opérations & Maintenance

  • Forces : Excellente robustesse opérationnelle grâce aux fondations de CNPG (failover intra-région éprouvé, gestion native des sauvegardes et des mises à niveau) : Intégration des failover slots avec Spock v5.
  • Faiblesses : La résolution des conflits d'écriture reste un point noir. Si deux utilisateurs modifient la même ligne en même temps dans deux régions différentes, c'est la règle du "dernier qui parle a raison" (par timestamp) qui s'applique par défaut. Les données écrasées dans la région "perdante" sont envoyées dans une table d'antécédents (spock.resolutions), ce qui oblige les équipes de production à surveiller et gérer manuellement ces écarts pour éviter les potentielles incohérences de données.

4. Écosystème & Maturité

  • Forces : Solution 100 % open source sous licence PostgreSQL. S'appuie sur du Postgres "vanilla" (versions 16, 17 ou 18).
  • Faiblesses : Le Chart Helm est encore très jeune (v0.2), ce qui implique que l'API des values est susceptible de changer et que les retours d'expérience en production sont encore rares.

5. Gestion Multi-cluster

  • Forces : La mécanique globale (initSpock, provisionCerts, externalNodes) est simple, claire et explicite.
  • Faiblesses : La gestion de la communication (routage réseau cross-cluster et distribution sécurisée des certificats) reste entièrement à votre charge.

6. Impact Applicatif

  • Forces : Excellentes performances d'accès avec une latence locale en lecture comme en écriture dans chaque région.
  • Faiblesses : Quelques contraintes de design pour le développement : obligation d'utiliser des Clés Primaires de type UUID ou Snowflake, exclusion du type SERIAL, et nécessité d'auditer les séquences ainsi que les triggers.

Conclusion

Ce POC démontre avec le chart Helm pgEdge et CloudNativePG qu'il est facile de mettre en place une topologie Postgres active-active multi-région. Le scénario PCA fonctionne. On éteint une région, l'autre continue, le backlog se rejoue au retour et l'observabilité nécessaire se construit proprement avec les briques CNPG existantes.

La vraie décision n'est pas technique mais plutôt architecturale : accepter un RPO égal au lag de réplication et une sémantique de résolution de conflits last-update-wins, en échange d'un RTO quasi nul et de deux régions réellement actives. Pour beaucoup de workloads, c'est un excellent choix. Pour les autres, ce POC aura je pense le mérite d'enrichir les discussions.

Mon conseil

Si votre workload le permet, dirigez toutes les écritures vers une seule région et réservez la seconde à la lecture et au rôle de secours. Vous supprimez à la source le point qui fait le "défaut" de l'actif-actif, les conflits et leur résolution silencieuse en last-update-wins. On bénéficie de l'essentiel : une cible avec des données chaudes et synchronisées qui peut permettre de prendre le relais sans délai sur incident.

L'intégralité des fichiers (configs kind, values Helm, manifestes, application de test, scripts) est disponible dans le repo wescale/pgedge-pca-poc.