0

PGO : opérateurs kubernetes pour PostgreSQL, la suite !

twitterlinkedinmail

Salut à toutes et tous ! Cette semaine la suite de notre petit tour des opérateurs Kubernetes pour PostgreSQL, et après kubegres, c’est au tour de PGO de CrunchyData.

Quelques infos générales sur l’opérateur PGO

Comparé à Kubegres, PGO semble plus complet dans le sens où il intègre de base un réplica par défaut, mais aussi la possibilité de backuper directement avec pgBackRest dans des repositories locaux ou cloud, un pod pgBouncer, et un exporter pour Prometheus.


(source : https://access.crunchydata.com/documentation/postgres-operator/v5/architecture/overview/)

Comme pour kubegres, l’operateur PGO encapsule à l’intérieur de ses deployments des objets de base Kubernetes tels que des StatefulSets pour les pods primaire et replicas, des Services, des PV et PVC pour le stockage, etc… comme nous allons le voir lors du deploiement de notre premier cluster.

Installation de l’opérateur PGO

Première chose à faire avant de créer notre premier cluster, déployer l’opérateur PGO. Il est possible de le faire au choix soit via Kustomize soit via Helm. CrunchyData propose un repo git à cloner et qui contient déjà les fichiers de configuration de base, que nous pourrons modifier au besoin pour customiser notre déploiement. Une fois le git cloné sur notre github Capdata, nous pouvons commencer à récupérer les fichiers en local et regarder le contenu des fichiers de définition. Nous utiliserons Kustomize pour l’exemple :

$ git clone --depth=1 "https://github.com/Capdata/postgres-operator-examples.git"
Cloning into 'postgres-operator-examples'...
remote: Enumerating objects: 140, done.
remote: Counting objects: 100% (140/140), done.
remote: Compressing objects: 100% (105/105), done.
remote: Total 140 (delta 33), reused 81 (delta 26), pack-reused 0
Receiving objects: 100% (140/140), 150.57 KiB | 3.01 MiB/s, done.
Resolving deltas: 100% (33/33), done.

$ cd postgres-operator-examples/kustomize

$ tree -a install/namespace/
install/namespace/
├── kustomization.yaml
└── namespace.yaml

$ tree -a install/default/
install/default/
├── kustomization.yaml
└── selectors.yaml

L’apply de ~kustomize/install/namespace/namespace.yaml va créer un namespace dédié postgres-operator:

apiVersion: v1
kind: Namespace
metadata:
  name: postgres-operator

Puis ~kustomize/install/default va créer le reste de l’opérateur:

$ kubectl apply --kustomize=kustomize/install/namespace
namespace/postgres-operator created

$ kubectl apply --server-side --kustomize=kustomize/install/default
customresourcedefinition.apiextensions.k8s.io/pgupgrades.postgres-operator.crunchydata.com serverside-applied
customresourcedefinition.apiextensions.k8s.io/postgresclusters.postgres-operator.crunchydata.com serverside-applied
serviceaccount/pgo serverside-applied
serviceaccount/postgres-operator-upgrade serverside-applied
clusterrole.rbac.authorization.k8s.io/postgres-operator serverside-applied
clusterrole.rbac.authorization.k8s.io/postgres-operator-upgrade serverside-applied
clusterrolebinding.rbac.authorization.k8s.io/postgres-operator serverside-applied
clusterrolebinding.rbac.authorization.k8s.io/postgres-operator-upgrade serverside-applied
deployment.apps/pgo serverside-applied
deployment.apps/pgo-upgrade serverside-applied

$ kubectl get all --namespace=postgres-operator
NAME                               READY   STATUS    RESTARTS   AGE
pod/pgo-774db98dbc-htm5d           1/1     Running   0          74m
pod/pgo-upgrade-785dd6dc4c-cw2ld   1/1     Running   0          74m

NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/pgo           1/1     1            1           74m
deployment.apps/pgo-upgrade   1/1     1            1           74m

NAME                                     DESIRED   CURRENT   READY   AGE
replicaset.apps/pgo-774db98dbc           1         1         1       74m
replicaset.apps/pgo-upgrade-785dd6dc4c   1         1         1       74m

Création d’un premier cluster PGO

Maintenant que notre opérateur est installé, c’est le moment de s’intéresser au paramétrage du futur cluster. Tout se trouve dans ~kustomize/postgres:

$ tree -a postgres/
postgres/
├── kustomization.yaml
└── postgres.yaml

Le coeur de notre cluster se trouve dans postgresl.yaml :

apiVersion: postgres-operator.crunchydata.com/v1beta1
kind: PostgresCluster
metadata:
  name: hippo
spec:
  image: registry.developers.crunchydata.com/crunchydata/crunchy-postgres:ubi8-15.2-0
  postgresVersion: 15
  instances:
    - name: instance1
      dataVolumeClaimSpec:
        accessModes:
        - "ReadWriteOnce"
        resources:
          requests:
            storage: 1Gi
  backups:
    pgbackrest:
      image: registry.developers.crunchydata.com/crunchydata/crunchy-pgbackrest:ubi8-2.41-4
      repos:
      - name: repo1
        volume:
          volumeClaimSpec:
            accessModes:
            - "ReadWriteOnce"
            resources:
              requests:
                storage: 1Gi

Comme pour Kubegres, l’opérateur PGO nous permet de créer un nouveau type d’objet dans Kubernetes :

kind: PostgresCluster

Le nom du cluster par défaut est ‘hippo‘ mais nous pourrons le changer sans problème. Pour les pods (primaire, réplicas, pgBackRest), les images sont précisées ainsi que les volumes qui sont rattachés via des abstractions de PVC appelées soit “dataVolumeClaimSpec” pour les pods PostgreSQL soit “VolumeClaimSpec” pour la partie sauvegarde.

Nous pouvons compléter le fichier de définition par défaut avec quelques customisations:
– Ajouter des quotas de ressources CPU et mémoire via instances.resources.limits
– Ajouter un réplica
– Renommer notre cluster ‘pgcluster1
– Et enfin ajouter un nodePort pour exposer notre cluster au monde extérieur :

apiVersion: postgres-operator.crunchydata.com/v1beta1
kind: PostgresCluster
metadata:
  name: pgcluster1
spec:
  image: registry.developers.crunchydata.com/crunchydata/crunchy-postgres:ubi8-15.2-0
  postgresVersion: 15
  instances:
    - name: postgresdb1
      replicas: 2
      resources:
        limits:
          cpu: "0.5"
          memory: 1Gi
      dataVolumeClaimSpec:
        accessModes:
        - "ReadWriteOnce"
        resources:
          requests:
            storage: 1Gi
  backups:
    pgbackrest:
      image: registry.developers.crunchydata.com/crunchydata/crunchy-pgbackrest:ubi8-2.41-4
      repos:
      - name: repo1
        volume:
          volumeClaimSpec:
            accessModes:
            - "ReadWriteOnce"
            resources:
              requests:
                storage: 1Gi
  service:
    metadata:
      annotations:
        annotation1: "mdnodeport1"
      labels:
        label1: "32000"
    type: NodePort
    nodePort: 32000

Pour la configuration des sauvegardes, nous verrons un peu plus tard. Dans l’immédiat, créons notre cluster:

$ kubectl apply -k kustomize/postgres/
postgrescluster.postgres-operator.crunchydata.com/pgcluster1 created

$ kubectl get all --namespace=postgres-operator
NAME                                READY   STATUS    RESTARTS   AGE
pod/pgcluster1-postgresdb1-55pl-0   4/4     Running   0          11s
pod/pgcluster1-postgresdb1-9w2w-0   4/4     Running   0          11s
pod/pgcluster1-repo-host-0          2/2     Running   0          11s
pod/pgo-774db98dbc-tshp6            1/1     Running   0          68s
pod/pgo-upgrade-785dd6dc4c-ntwkd    1/1     Running   0          68s

NAME                           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/pgcluster1-ha          NodePort    10.102.232.74   <none>        5432:32000/TCP   51m
service/pgcluster1-ha-config   ClusterIP   None            <none>        <none>           51m
service/pgcluster1-pods        ClusterIP   None            <none>        <none>           51m
service/pgcluster1-primary     ClusterIP   None            <none>        5432/TCP         51m
service/pgcluster1-replicas    ClusterIP   10.106.148.50   <none>        5432/TCP         51m

NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/pgo           1/1     1            1           69s
deployment.apps/pgo-upgrade   1/1     1            1           68s

NAME                                     DESIRED   CURRENT   READY   AGE
replicaset.apps/pgo-774db98dbc           1         1         1       68s
replicaset.apps/pgo-upgrade-785dd6dc4c   1         1         1       68s

NAME                                           READY   AGE
statefulset.apps/pgcluster1-postgresdb1-55pl   1/1     11s
statefulset.apps/pgcluster1-postgresdb1-9w2w   1/1     11s
statefulset.apps/pgcluster1-repo-host          1/1     11s

Il se peut qu’il y ait des problèmes de quota mémoire / CPU disponible, les pods vont rester en Pending, et la suppression / recréation des objets ne suffit pas. La suppression du namespace se bloque en Terminating il a fallu que je déroule la procédure de suppression du namespace à la main pour repartir de zéro.

Bref notre déploiement nous a donc créé 3 nouveaux pods et 3 StatefulSets (primaire, réplica et pgBackRest), 4 services ClusterIP et notre nodePort.

Pour nous y connecter, nous avons besoin de récupérer le secret qui a été créé à l’initialisation du cluster. Jetons un coup d’oeil au secret dans sa globalité pour voir ce qu’il contient:

$ kubectl get secret --namespace=postgres-operator pgcluster1-pguser-pgcluster1 -o json
{
    "apiVersion": "v1",
    "data": {
        "dbname": "cGdjbHVzdGVyMQ==",
        "host": "cGdjbHVzdGVyMS1wcmltYXJ5LnBvc3RncmVzLW9wZXJhdG9yLnN2Yw==",
        "jdbc-uri": "amRiYzpwb3N0Z3Jlc3FsOi8vcGdjbHVzdGVyMS1wcmltYXJ5LnBvc3RncmVzLW9wZXJhdG9yLnN2Yzo1NDMyL3BnY2x1c3RlcjE/cGFzc3dvcmQ9UHBybiUzQnZ1WDlrSiU1RE1WQnZwd3QzTk5wJTJBJnVzZXI9cGdjbHVzdGVyMQ==",
        "password": "UHBybjt2dVg5a0pdTVZCdnB3dDNOTnAq",
        "port": "NTQzMg==",
        "uri": "cG9zdGdyZXNxbDovL3BnY2x1c3RlcjE6UHBybjt2dVg5a0olNURNVkJ2cHd0M05OcCUyQUBwZ2NsdXN0ZXIxLXByaW1hcnkucG9zdGdyZXMtb3BlcmF0b3Iuc3ZjOjU0MzIvcGdjbHVzdGVyMQ==",
        "user": "cGdjbHVzdGVyMQ==",
        "verifier": "U0NSQU0tU0hBLTI1NiQ0MDk2Olo3OTNBUVIwU0xZUVBDY3BXNkRaSXc9PSRWUWdlc0VlSGVvVnpnakc4emkyRGJJNmlpemo1ZnJGWmN2K3c3NzZScVhVPTpDT1JDVStoQU1IeDBkRzBKaGU3dllwUTdFWTB4QzZ5RzJUUE5NWFV5MTlRPQ=="
    },
    "kind": "Secret",
    "metadata": {
        "creationTimestamp": "2023-06-05T11:39:32Z",
        "labels": {
            "postgres-operator.crunchydata.com/cluster": "pgcluster1",
            "postgres-operator.crunchydata.com/pguser": "pgcluster1",
            "postgres-operator.crunchydata.com/role": "pguser"
        },
        "name": "pgcluster1-pguser-pgcluster1",
        "namespace": "postgres-operator",
        "ownerReferences": [
            {
                "apiVersion": "postgres-operator.crunchydata.com/v1beta1",
                "blockOwnerDeletion": true,
                "controller": true,
                "kind": "PostgresCluster",
                "name": "pgcluster1",
                "uid": "80bbee62-0602-4012-9c06-dcd23ca7723b"
            }
        ],
        "resourceVersion": "169451",
        "uid": "fe833c97-0585-4910-95ca-fb1c7774d5b2"
    },
    "type": "Opaque"
}

On a donc la possibilité de récupérer le user et le mot de passe :

$ export PGUSER=$(kubectl get secret --namespace=postgres-operator pgcluster1-pguser-pgcluster1 -o jsonpath={.data.user} | base64 -d)
$ export PGPASSWORD=$(kubectl get secret --namespace=postgres-operator pgcluster1-pguser-pgcluster1 -o jsonpath={.data.password} | base64 -d)

Et tester la connexion (noter que l’adresse IP est celle du node, ie kubectl describe nodes):

$ psql -h 192.168.59.101 -p 32000 -c "select version();"
                                                 version
---------------------------------------------------------------------------------------------------------
 PostgreSQL 15.2 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 8.5.0 20210514 (Red Hat 8.5.0-16), 64-bit
(1 row)

Bascules et haute disponibilité

Lançons une connexion en boucle sur le nodePort pour récupérer l’IP de l’instance primaire et voir ce qui se passe en cas de bascule.

$ while(true); do psql -h 192.168.59.101 -p 32000 -c "select inet_server_addr();"; sleep 1; done
 inet_server_addr
------------------
 172.17.0.7
(1 row)

 inet_server_addr
------------------
 172.17.0.7
(1 row)

(...)

Pour tester la bascule, nous allons carrément supprimer le StatefulSet de l’instance primaire, il faut commencer par récupérer son nom, puis on supprime :

$ kubectl -n postgres-operator get pods \
  --selector=postgres-operator.crunchydata.com/role=master \
  -o jsonpath='{.items[*].metadata.labels.postgres-operator\.crunchydata\.com/instance}'
pgcluster1-postgresdb1-9w2w

$ kubectl delete statefulset --namespace=postgres-operator pgcluster1-postgresdb1-9w2w
statefulset.apps "pgcluster1-postgresdb1-9w2w" deleted

La connexion en boucle indique que l’on a bien changé d’IP:

(...)
 inet_server_addr
------------------
 172.17.0.7
(1 row)

 inet_server_addr
------------------
 172.17.0.7
(1 row)

 inet_server_addr
------------------
 172.17.0.6
(1 row)

 inet_server_addr
------------------
 172.17.0.6
(1 row)

… et minikube a détecté la perte du StatefulSet pgcluster1-postgresdb1-9w2w et l’a recréé en arrière plan:

$ kubectl get statefulset --namespace=postgres-operator
NAME                          READY   AGE
pgcluster1-postgresdb1-55pl   1/1     96m
pgcluster1-postgresdb1-9w2w   1/1     11s
pgcluster1-repo-host          1/1     96m

Il existe un certain nombre d’options complémentaires notamment de l’anti-affinité pour éviter que les pods ne tournent sur les mêmes nodes, voir la documentation pour plus de détails.

Mise en place des backups via pgbackrest

Bien qu’il soit possible de sauvegarder directement sur AWS S3, Azure ou GCP, pour l’exemple nous avons déployé un volume Kubernetes simple.
Pour ajouter une planification et une rétention il faut rajouter quelques propriétés à spec.backups.pgbackrest :

  backups:
    pgbackrest:
      image: registry.developers.crunchydata.com/crunchydata/crunchy-pgbackrest:ubi8-2.41-4
        global:
	  repo1-retention-full: "14"
          repo1-retention-full-type: time
      repos:
      - name: repo1
        volume:
          volumeClaimSpec:
            accessModes:
            - "ReadWriteOnce"
            resources:
              requests:
                storage: 1Gi
	schedules:
	  full: "50 15 * * *"
$ kubectl apply -k kustomize/postgres/
postgrescluster.postgres-operator.crunchydata.com/pgcluster1 configured

$ kubectl get cronjobs --namespace=postgres-operator
NAME                    SCHEDULE       SUSPEND   ACTIVE   LAST SCHEDULE   AGE
pgcluster1-repo1-full   50 15 * * *    False     0        <none>          106s

Kubernetes a créé un cronjob associé. Sachant que les backups peuvent être aussi différentiels ou incrémentaux selon la stratégie de sauvegarde envisagée.
La rétention quant à elle peut être indiquée en jours (time) ou en nombre de sauvegardes (count). Grâce à pgBackRest, PGO permettra ensuite d’utiliser les backups pour soit cloner les bases vers une autre cluster, soit le restaurer à un point dans le temps vers un nouveau cluster (pour comparer les données ou récupérer des lignes supprimées par erreur par exemple), ou restaurer in-place. Le sujet est assez long et cela pourra faire l’objet d’un futur épisode, mais globalement la puissance de feu de pgBackRest au service de la restaurabilité donne un atout supplémentaire à PGO par rapport à ses concurrents.

Conclusion

Dans cette première approche de PGO, nous n’avons fait qu’effleurer la surface des possibilités de cet opérateur, qui semble aller plus loin que ses concurrents avec:
– La sauvegarde et restauration via pgBackRest intégré, et la possibilité de sauvegarder directement dans le cloud.
– Intégration avec Promotheus.
– Intégration avec pgBouncer.
– Déployable directement via des standards tels que Kustomize ou Helm.
– Gestion des secrets intégrée.
etc… Sûrement que d’autres articles pour approfondir PGO viendront compléter celui-ci, en attendant bonne lecture et à bientôt sur le blog Cap Data !

Continuez votre lecture sur le blog :

twitterlinkedinmail

David Baffaleuf

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.