Bases par environnement (la barrière est dans PostgreSQL)
Contexte : ADR 029 Décision 3, issue #137. Cette page explique comment les trois databases sont créées et pourquoi ce mécanisme-là. Le quoi (1 RDS, 3 databases, users scopés) est tranché dans l'ADR.
Le problème que ça résout
Les trois environnements partagent une seule instance RDS — compromis de coût assumé. Elle est managée, hors cluster, et tous les envs joignent le même endpoint : une NetworkPolicy ne peut donc pas distinguer dev de prod au niveau réseau, ils parlent tous à la même adresse.
La seule barrière possible est dans le moteur : une database par env, un user par env, et aucun droit croisé. C'est PostgreSQL qui refuse, pas le réseau.
Pourquoi ce n'est pas Terraform qui crée les bases
C'est le réflexe naturel — il existe un provider cyrilgdn/postgresql — et il ne
marche pas ici, pour deux raisons cumulées :
- Le RDS n'est joignable que depuis le VPC. Il est en subnet privé
(
publicly_accessible = false) et son security group n'ouvre le 5432 qu'au CIDR10.0.0.0/16. Terraform tourne sur le runner self-hosted, chez Youssef, hors VPC : le provider n'a aucune route vers l'instance tant qu'il n'y a pas de bastion. - Le RDS est éphémère. Il vit dans
terraform/ephemeral/, donc il est détruit et recréé vide à chaque cycleinfra-stop/infra-start. Une création faite à la main une fois pour toutes ne survivrait pas à la première nuit.
D'où le choix : le SQL s'exécute depuis le cluster, qui lui est dans le VPC, et il est rejoué à chaque montée.
Le mécanisme
terraform/persistent ──> 3 secrets ASM fastapi-eks/{dev,staging,prod}
(survit au teardown) DB_PASSWORD généré par Terraform
│
│ ESO (IRSA)
▼
ns fastapi (plateforme) Secret K8s db-bootstrap
│ master + les 3 mots de passe
▼
Job db-bootstrap ──psql──> RDS
(hook Sync ArgoCD)
Les fichiers : k8s/platform/db-bootstrap/ (SecretStore, ExternalSecret, Job) et
l'Application ArgoCD k8s/platform/argocd-apps/db-bootstrap.yaml.
Le Job vit dans le namespace plateforme fastapi, pas dans les overlays d'env.
C'est le point qui compte : seul ce Job a besoin du mot de passe master RDS,
le seul habilité à CREATE DATABASE et CREATE ROLE. Le descendre dans
fastapi-dev / -staging / -prod reviendrait à donner à chaque env le pouvoir
de supprimer les bases des autres — la barrière n'aurait plus aucune valeur.
sync-wave: -1 : les bases doivent exister avant que les pods applicatifs ne
démarrent, puisque leur initContainer lance alembic upgrade head.
Convergence plutôt que création
Le hook Sync rejoue le Job à chaque synchronisation. Le SQL est donc écrit pour
converger, pas pour créer :
CREATE ROLE/CREATE DATABASEseulement si absents (via\gexec, seul moyen propre de conditionner ces deux commandes, qui ne supportent pasIF NOT EXISTSni les blocsDO) ;- puis un
ALTER ROLE ... PASSWORDà chaque passage.
Ce ALTER n'est pas cosmétique : les mots de passe vivent dans le stack
persistent alors que le RDS renaît vide à chaque montée. C'est lui qui fait
se rejoindre les deux cycles de vie après un infra-start.
Ce qui produit réellement l'isolation
Deux gestes, et le second est le moins intuitif :
CREATE DATABASE app_dev OWNER app_dev;
REVOKE CONNECT ON DATABASE app_dev FROM PUBLIC;
GRANT CONNECT ON DATABASE app_dev TO app_dev;
OWNER: depuis PostgreSQL 15, le schémapublicappartient àpg_database_owner. Faire du user l'owner de sa base lui donne le droit d'y créer ses tables — ce dont alembic a besoin — sans lui accorder quoi que ce soit ailleurs. Pas besoin deGRANT ALL ON SCHEMA public.REVOKE CONNECT ... FROM PUBLIC: c'est la ligne qui isole. Par défaut PostgreSQL accordeCONNECTàPUBLICsur toute base créée. Sans ceREVOKE, les trois users existent bien séparément mais peuvent tous se connecter aux trois bases. Créer des users distincts ne suffit pas ; il faut retirer le droit implicite.
Le même REVOKE est appliqué à fastapi_db, la base historique qui porte
encore la prod tant que #138 n'a pas donné à chaque env son propre
ExternalSecret. Sans lui, les nouveaux users pouvaient y ouvrir une session
(sans lire ses tables, qui appartiennent au master, ni en créer, PostgreSQL 15+
retirant CREATE sur public à PUBLIC — mais une session de dev sur la base de
prod n'a pas lieu d'être).
L'application n'est pas affectée : elle se connecte avec le user master,
propriétaire des bases, dont le CONNECT est inscrit dans l'ACL de la base
(fastapi=CTc/fastapi) et non hérité de PUBLIC. Vérifié sur un owner
non-superuser, le cas d'un master RDS.
Ce que #137 ne faisait PAS — et que #138 a livré
À la livraison de #137, les applications n'étaient pas branchées sur ces bases :
les trois envs lisaient encore fastapi_db avec le user master, ConfigMap et
ExternalSecret communs. Autrement dit, dev et staging écrivaient toujours dans la
base de prod.
Ce que #137 livrait, c'était la barrière : les bases et les users existent, et PostgreSQL refuse tout accès croisé.
#138 s'y est branché — un ConfigMap et un ExternalSecret par env, un rôle IRSA
scopé par env, et les hosts DNS correspondants. La prod a basculé au passage de
fastapi_db vers app_prod, quittant le user master.
Un point mérite d'être relevé, parce qu'il n'était pas visible en écrivant #137 :
le cloisonnement PostgreSQL décrit ci-dessus supposait que chaque pod n'obtienne
que le mot de passe de son environnement. Rien ne le garantissait, un
ExternalSecret pouvant nommer le chemin ASM d'un autre env. C'est ce que le rôle
IRSA par environnement ferme.
Détail du mécanisme : Identités et secrets par environnement.
Vérifier
Depuis un pod du cluster (le RDS n'est pas joignable depuis le poste) :
# Le Job a-t-il convergé ?
kubectl -n fastapi get job db-bootstrap # absent = succès, ArgoCD l'a ramassé
kubectl -n fastapi logs job/db-bootstrap # s'il est encore là, c'est qu'il a échoué
# Les trois bases existent, avec le bon owner
kubectl -n fastapi run psql --rm -it --restart=Never \
--image=postgres:16-alpine --env=PGPASSWORD=<master> -- \
psql -h <endpoint> -U fastapi -d fastapi_db \
-c "\l app_*"
# LA vérification qui compte : dev ne joint pas prod
kubectl -n fastapi run psql --rm -it --restart=Never \
--image=postgres:16-alpine --env=PGPASSWORD=<mot de passe dev> -- \
psql -h <endpoint> -U app_dev -d app_prod -c "select 1"
# attendu : FATAL: permission denied for database "app_prod"
# DETAIL: User does not have CONNECT privilege.
Les mots de passe se lisent dans le Secret db-bootstrap du namespace fastapi.