No alternative text provided

🐘 PostgreSQL sur Kubernetes avec ArgoCD et l’opérateur Zalando (avec SSL et backups S3 bien sûr)

J'avais besoin d'une base de données PostgreSQL pour ce site web (oui, celui que vous lisez), car j’utilise Strapi comme CMS headless. Je voulais une solution :

  • Facile à déployer ✅
  • Assez robuste pour la prod ✅
  • Sécurisée correctement (SSL !) ✅
  • Avec des sauvegardes automatiques quotidiennes vers S3 ✅

Comme d’habitude, “facile” est un terme relatif. Voici donc exactement comment j’ai fait — avec des extraits de code, des astuces kubectl, et quelques pièges à éviter. Vous êtes les bienvenus 😎


⚙️ Vue d’ensemble de l’architecture

  • Cluster Kubernetes hébergé sur DigitalOcean (service managé).
  • ArgoCD pour le déploiement GitOps.
  • Zalando Postgres Operator pour gérer les clusters PostgreSQL.
  • Strapi comme consommateur de la base.
  • Chiffrement TLS entre l’app et la DB, avec un certificat auto-signé.
  • Backups vers DigitalOcean Spaces (compatible S3).

📦 Déploiement de l’opérateur PostgreSQL de Zalando

L’opérateur tourne dans le namespace main. Voici la définition de l’application ArgoCD :

1apiVersion: argoproj.io/v1alpha1
2kind: Application
3metadata:
4  name: postgres-operator
5  namespace: argocd
6  finalizers:
7    - resources-finalizer.argocd.argoproj.io
8spec:
9  project: default
10  source:
11    repoURL: 'git@github.com:bv86/k8s-infra.git'
12    targetRevision: HEAD
13    path: kustomize/postgres-operator/base
14  destination:
15    server: "https://kubernetes.default.svc"
16    namespace: main
17  syncPolicy:
18    automated:
19      prune: true
20      selfHeal: true
21    syncOptions:
22      - CreateNamespace=true

Et le fichier kustomization.yaml pour s’assurer que tout tourne dans le bon namespace :

1apiVersion: kustomize.config.k8s.io/v1beta1
2kind: Kustomization
3
4resources:
5  - github.com/zalando/postgres-operator/manifests?ref=v1.14.0
6
7patches:
8  # by default, service account is created in the default namespace, so add another one in the proper namespace
9  - target:
10      version: v1
11      kind: ServiceAccount
12      name: postgres-operator
13    patch: |-
14      - op: replace
15        path: /metadata/namespace
16        value: main

🔒 Générer un certificat SSL auto-signé

Node.js est très pointilleux avec les certificats SSL. Voici comment j’ai généré un certificat signé par ma propre autorité :

1. Générer la clé et le certificat de l’AC

1openssl req -x509 -newkey rsa:4096 -days 9000 \
2  -nodes -keyout ca-key.pem -out ca-cert.pem \
3  -subj "/CN=MyPGSQL_CA"

2. Générer la clé serveur et la requête de signature

1openssl req -newkey rsa:4096 -nodes \
2  -keyout server-key.pem -out server-req.pem \
3  -subj "/CN=acid-strapi.main.svc.cluster.local"

☝️ Important : le CN doit correspondre exactement au nom du service interne.

3. Signer le certificat serveur

1openssl x509 -req -in server-req.pem -CA ca-cert.pem \
2  -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -days 3650

4. Le stocker dans Kubernetes

1kubectl create secret generic pg-tls \
2  --from-file=tls.crt=server-cert.pem \
3  --from-file=tls.key=server-key.pem \
4  --from-file=ca.crt=ca-cert.pem \
5  -n main

🛠 Déployer le cluster PostgreSQL

🔐 Secret pour les identifiants Spaces

On commence par créer un secret contenant les credentials d’accès à mon espace S3 chez DigitalOcean :

1kubectl create secret generic app-secrets \
2  --from-literal=DO_SPACE_ACCESS_KEY="your-access-key" \
3  --from-literal=DO_SPACE_SECRET_KEY="your-secret-key" \
4  -n main

L’instance PostgreSQL

Fait partie de mon app my-strapi dans ArgoCD. Voici le CRD :

1apiVersion: "acid.zalan.do/v1"
2kind: postgresql
3metadata:
4  name: acid-strapi
5  namespace: main
6spec:
7  teamId: "acid"
8  spiloFSGroup: 103
9  volume:
10    size: 1Gi
11  numberOfInstances: 2
12  tls:
13    secretName: "pg-tls"
14    caFile: "ca.crt"
15  users:
16    benoit:
17      - superuser
18      - createdb
19  preparedDatabases:
20    strapi:
21      defaultUsers: true
22  postgresql:
23    version: "17"
24  env:
25    - name: WAL_S3_BUCKET
26      value: strapi-bucket-ben
27    - name: AWS_ACCESS_KEY_ID
28      valueFrom:
29        secretKeyRef:
30          name: app-secrets
31          key: DO_SPACE_ACCESS_KEY
32    - name: AWS_SECRET_ACCESS_KEY
33      valueFrom:
34        secretKeyRef:
35          name: app-secrets
36          key: DO_SPACE_SECRET_KEY
37    - name: AWS_ENDPOINT
38      value: "https://lon1.digitaloceanspaces.com"
39    - name: BACKUP_SCHEDULE
40      value: "0 1 * * *"  # Daily at 1AM

📦 Configuration de Strapi

Strapi se connecte à la DB avec SSL activé et certificats personnalisés. Voici le setup :

Fichier de configuration (ConfigMap)

1apiVersion: v1
2kind: ConfigMap
3metadata:
4  name: strapi-database-conf
5data:
6  DATABASE_HOST: acid-strapi.main.svc.cluster.local
7  DATABASE_PORT: "5432"
8  DATABASE_NAME: strapi
9  DATABASE_USERNAME: strapi_owner_user
10  DATABASE_SSL: "true"
11  DATABASE_CLIENT: postgres

Définition du déploiement Strapi

1apiVersion: apps/v1
2kind: Deployment
3metadata:
4  name: strapi-app
5spec:
6  replicas: 1
7  selector:
8    matchLabels:
9      app: strapi-app
10  template:
11    metadata:
12      labels:
13        app: strapi-app
14    spec:
15      imagePullSecrets:
16        - name: ghcr-credentials
17      volumes:
18        - name: certs
19          secret:
20            secretName: pg-tls
21      containers:
22        - name: strapi
23          image: ghcr.io/bv86/my-strapi:main
24          ports:
25            - containerPort: 1337
26              name: http
27          envFrom:
28            - configMapRef:
29                name: strapi-app-conf
30            - configMapRef:
31                name: strapi-database-conf
32            - configMapRef:
33                name: strapi-s3-conf
34            - secretRef:
35                name: app-secrets
36          env:
37            - name: DATABASE_PASSWORD
38              valueFrom:
39                secretKeyRef:
40                  name: strapi-owner-user.acid-strapi.credentials.postgresql.acid.zalan.do
41                  key: password
42            - name: NODE_EXTRA_CA_CERTS
43              value: /etc/certs/ca.crt
44          volumeMounts:
45            - name: certs
46              mountPath: /etc/certs
47              readOnly: true

Comme tu peux le voir, je charge les valeurs depuis mes ConfigMap en tant que variables d’environnement.

Le secret strapi-owner-user.acid-strapi.credentials.postgresql.acid.zalan.do est géré automatiquement par l’opérateur PostgreSQL et contient le mot de passe.

Un point important : je monte le certificat ca.crt depuis le secret pg-tls dans /etc/certs, afin que Node.js sache qu’il peut lui faire confiance.

Enfin, dans Strapi, la config base de données ressemble à :

1postgres: {
2      connection: {
3        connectionString: env('DATABASE_URL'),
4        host: env('DATABASE_HOST', 'localhost'),
5        port: env.int('DATABASE_PORT', 5432),
6        database: env('DATABASE_NAME', 'strapi'),
7        user: env('DATABASE_USERNAME', 'strapi'),
8        password: env('DATABASE_PASSWORD', 'strapi'),
9        ssl: env.bool('DATABASE_SSL', false) && {
10          key: env('DATABASE_SSL_KEY', undefined),
11          cert: env('DATABASE_SSL_CERT', undefined),
12          ca: env('DATABASE_SSL_CA', undefined),
13          capath: env('DATABASE_SSL_CAPATH', undefined),
14          cipher: env('DATABASE_SSL_CIPHER', undefined),
15          rejectUnauthorized: env.bool(
16            'DATABASE_SSL_REJECT_UNAUTHORIZED',
17            true
18          ),
19        },
20        schema: env('DATABASE_SCHEMA', 'public'),
21      },
22      pool: {
23        min: env.int('DATABASE_POOL_MIN', 2),
24        max: env.int('DATABASE_POOL_MAX', 10),
25      },
26    }

🧪 Astuces de debug

  • Utilise psql depuis un autre pod pour tester le SSL :
1psql "sslmode=verify-full host=acid-strapi.main.svc.cluster.local dbname=strapi user=strapi_owner_user sslrootcert=/etc/certs/ca.crt"
  • Vérifie les logs de l’opérateur Zalando :
1kubectl logs -l name=postgres-operator -n main

✅ Conclusion

Avec cette configuration, j’ai maintenant :

  • Une base PostgreSQL prête pour la production
  • Un chiffrement SSL/TLS entre l’app et la DB
  • Des sauvegardes automatiques tous les jours
  • De la magie GitOps avec ArgoCD ✨

Prochaine étape ? Probablement automatiser la création des certificats avec Let’s Encrypt.