Exploitation · Socle GitOps & bootstrap

Socle GitOps & bootstrap

Comment le cluster se construit et se réconcilie : un dépôt Git décrit l'état désiré, Argo CD l'applique dans l'ordre. Deux exceptions, bootstrappées à la main une fois — expliquées ici.

Trois cycles de vie, un seul dépôt

Toute l'infra tient dans fga-infrastructure, en dossiers séparés. Argo CD ne surveille que gitops/.

fga-infrastructure/
├─ provisioning/   # Day 0 : bootstrap k3s, Argo, operators, secrets — HORS Argo (runbook)
├─ dev/            # docker-compose local
└─ gitops/         # état désiré du cluster — SEUL chemin surveillé par Argo
   ├─ charts/         quarkus-service · spa-front
   ├─ apps/           values par app (svc-reference-data, fga-portail, …)
   ├─ envs/staging/   Applications plateforme + CR + ApplicationSets (par sync-wave)
   └─ bootstrap/      app-of-apps racine (root-staging.yaml)
Le piège œuf/poule. Le provisionnement (Day 0) n'est jamais sous Argo : il crée le cluster, or Argo vit dans ce cluster. Provisioning = lancé une fois (runbook) ; GitOps = réconcilié en continu.

App-of-apps & sync-waves

Une Application racine (app-of-apps) surveille envs/staging/. Argo applique son contenu dans l'ordre des sync-waves : il attend qu'une vague soit Healthy avant la suivante. Les dépendances sont ainsi garanties (le ClusterIssuer attend cert-manager, l'instance Keycloak attend sa base, etc.).

wave −3
AppProjectNamespaces
wave −2
cert-managersealed-secrets
wave −1
ClusterIssuer (Let's Encrypt)
wave 0
Ingress ArgoPostgreSQL sigfga-pgPostgreSQL keycloak-pgMinIOmonitoringApplicationSet servicesApplicationSet fronts
wave 1
Instance KeycloakCluster Kafka
wave 2
Realm agents (import)
Ordre de réconciliation Argo. Les ApplicationSet génèrent une Application par service / par front à partir d'une liste.

Flux applicatif : CI publie l'image → write-back du tag dans apps/<app>/values-staging.yaml → Argo synchronise. Ajouter un service = un fichier de values + une ligne dans l'ApplicationSet.

Les operators bootstrappés hors GitOps

Quatre operators publient des CRD énormes (ex. clusters, keycloaks, kafkas, prometheuses) dont les annotations dépassent la limite du client-side apply (262144 octets). Argo ne bascule pas ces CRD en server-side apply de façon fiable → la synchro échoue en boucle. On installe donc l'operator en direct (kubectl apply --server-side), une fois, exactement comme Argo CD lui-même. Leurs CR (bases, instance Keycloak, cluster Kafka…) restent, eux, en GitOps.

Bootstrap manuel
kubectl --server-side, une fois (runbook)
CloudNativePG
operator PostgreSQL
Keycloak
operator
Strimzi
operator Kafka
Prometheus
CRD (stack via GitOps)
fournissent les types (CRD)
GitOps (Argo)
applique les CR
Cluster / Base / Instance
sigfga-pg, keycloak-pg, sigfga-keycloak, sigfga-kafka…
Operators (CRD géantes) = bootstrap manuel ; leurs CR = GitOps. Même principe que pour Argo CD, k3s, cert-manager.
OperatorRôleSes CR (en GitOps)
CloudNativePGPostgreSQL opéréCluster sigfga-pg, keycloak-pg (secrets d'identifiants auto-générés)
KeycloakauthentificationKeycloak + KeycloakRealmImport (realm agents)
StrimziKafka KRaft (sans ZooKeeper)Kafka + KafkaNodePool
Prometheus operatorobservabilitéstack kube-prometheus-stack via Helm (crds.enabled=false)

Bases de données — une base par service

Chaque service a sa propre base PostgreSQL, déclarée par un Database CR de CloudNativePG (fichier gitops/envs/staging/databases.yaml). Le service s'y connecte et Liquibase migre dans le schéma public (qui existe toujours dans une base neuve) — rien à pré-créer, aucun Job, aucune commande manuelle.

apiVersion: postgresql.cnpg.io/v1
kind: Database              # déclaratif, en GitOps — un bloc par service
metadata: { name: claims-db }
spec:
  cluster: { name: sigfga-pg }
  name: claims
  owner: sigfga
Pourquoi une base par service et pas un schéma par service ? Avec un schéma dédié (currentSchema=X), Liquibase crée sa table de suivi databasechangelog dans ce schéma avant le moindre changeset — or PostgreSQL ne crée pas le schéma tout seul, et un changeset ne peut pas créer le schéma dont Liquibase a déjà besoin (limite connue : Quarkus #12287). Il aurait fallu un Job central de création de schémas, ou un contournement (suivi Liquibase dans public). La base par service supprime le problème : CNPG crée la base déclarativement, public existe, Liquibase tourne normalement. Chaque service reste auto-suffisant.

Recette d'un service (tout déclaratif) : un bloc Database + un apps/<svc>/values-staging.yaml (base, client Keycloak, Kafka si besoin, ingress.path) + une ligne dans l'ApplicationSet + le build de l'image. Ingress sur un certificat TLS partagé api-tls (émis une fois pour api.fga.antah.dev) — pas un certif par service.

Secrets

Identifiants BDgénérés par CloudNativePG (secret <cluster>-app) → consommés via secretEnv. Zéro saisie.
Sealed Secretspour les secrets à versionner : kubeseal chiffre avec le certif public du contrôleur ; seul le cluster déchiffre. Sûr dans Git.
Bootstrapquelques secrets posés une fois à la main (accès GHCR, MinIO, Grafana) — non versionnés, comme les credentials Argo↔dépôt.

Pièges rencontrés (documentés dans le repo)

  • CRD trop grosse pour Argo (annotations: Too long) → bootstrapper l'operator hors GitOps (server-side). Voir tableau ci-dessus.
  • no matches for kind … alors que la CRD existe → soit la version d'API diffère (réflexe : kubectl api-resources | grep <type> ; ex. Strimzi sert v1, plus v1beta2), soit le cache d'Argo est périmé après un bootstrap de CRD (rollout restart statefulset argocd-application-controller).
  • CR d'operator en OutOfSync perpétuel → un champ posé au mauvais endroit est pruné par le schéma de la CRD (ex. les ressources du broker Kafka vont sur le KafkaNodePool, pas sur Kafka.spec.kafka).
  • Fronts (SPA) — binaire natif rollup manquant (bug npm des deps optionnelles, npm/cli#4828) → l'image se build sans le lock (résolution fraîche pour la plateforme du runner). Le ci.yml des fronts doit aussi checkout le submodule design-system (token).
  • Services (jib) — push GHCR en échec sous concurrence (BLOB_UPLOAD_UNKNOWN) → GHCR perd des sessions d'upload quand trop de couches/images partent en parallèle. Correctif : -Djib.serialize=true (pushs en série) + max-parallel: 2 sur le matrix CI.
  • Schéma par service + Liquibase → la table de suivi ne peut pas s'auto-créer son schéma (cf. section Bases de données). Choix retenu : une base par service (CNPG Database CR), déclaratif, sans Job.
Le runbook complet (du serveur nu au cluster opérationnel, pas à pas) vit dans fga-infrastructure/provisioning/README.md, et l'état désiré dans gitops/README.md. Objectif : qu'un nouveau serveur (qualification, prod) se monte en rejouant la procédure, sans re-découvrir les pièges.