xpeditis2.0/docs/mise-en-prod/09-deploiement-application.md
David b22f4e0b74 docs: procedure de mise en production pas a pas
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
2026-09-07 21:40:50 +02:00

13 KiB

09 — Déploiement de l'application

Durée : environ 2 h. Premier déploiement, réalisé à la main. L'automatisation vient à l'étape suivante — on ne débogue pas un premier déploiement à travers une chaîne CI.


Le premier administrateur

La migration 1730000000007-SeedTestUsers créait trois comptes dont le mot de passe est écrit en clair dans le dépôt — dont admin@xpeditis.com, rôle ADMIN, mot de passe Password123!. Sur une base de production neuve, appliquer les migrations créait donc un administrateur aux identifiants publics.

Trois mécanismes remplacent cela, et fonctionnent sans intervention :

Migration Effet
1730000000007-SeedTestUsers (modifiée) Ne s'exécute plus quand NODE_ENV=production. Les comptes ne sont jamais créés.
1756000000000-NeutralizeSeedAccountsInProduction Filet de sécurité : si ces comptes existent malgré tout (base migrée avant la garde, restauration ancienne), ils sont renommés, rendus inauthentifiables et désactivés. La migration échoue si le nettoyage est incomplet.
1756000000001-BootstrapAdminFromEnv Crée votre administrateur à partir de BOOTSTRAP_ADMIN_EMAIL.

Comment votre mot de passe est défini

Dans le cas nominal — BOOTSTRAP_ADMIN_EMAIL renseigné dans le ConfigMap, BOOTSTRAP_ADMIN_PASSWORD_HASH laissé vide — le compte est créé avec le hash Argon2id d'un secret aléatoire immédiatement perdu. Le compte existe, il est actif, mais aucun mot de passe ne peut y correspondre. Vous définissez le vôtre via « mot de passe oublié », qui envoie un jeton à usage unique, valable une heure, stocké haché en base.

Conséquence : aucun secret n'existe nulle part — ni dans Git, ni dans le Secret Kubernetes, ni dans l'historique du shell, ni dans les journaux de migration. Il n'y a rien à faire fuiter. Effet de bord utile : la réception du courriel prouve que la chaîne SMTP fonctionne.

Variante, si SMTP n'est pas encore opérationnel. Générez un hash hors ligne (node apps/backend/scripts/setup/generate-admin-hash.js) et placez-le dans BOOTSTRAP_ADMIN_PASSWORD_HASH, côté Secret et jamais ConfigMap. Un hash reste attaquable hors ligne : changez le mot de passe dès la première connexion, puis retirez la variable et réappliquez le Secret. Un mot de passe en clair placé dans cette variable fait échouer la migration.

Garde-fous : la migration ne fait rien s'il existe déjà un administrateur actif — elle ne peut donc pas en créer un second lors d'un déploiement ultérieur. Et si un compte porte déjà cette adresse, il est promu ADMIN sans que son mot de passe ne soit touché.


1. Construire les premières images

La chaîne de preprod produit les images preprod-<sha>. Pour le premier déploiement, on les fabrique à la main.

export SHA=$(git rev-parse --short=7 HEAD)
export REGISTRY=rg.fr-par.scw.cloud/weworkstudio

docker login "$REGISTRY" -u nologin -p '<REGISTRY_TOKEN>'

# Backend — aucune variable de build : tout est lu au démarrage
docker buildx build --platform linux/amd64 \
  -t "$REGISTRY/xpeditis-backend:prod-$SHA" \
  -f apps/backend/Dockerfile apps/backend --push

# Frontend — les URLs sont FIGÉES ici, pas à l'exécution
docker buildx build --platform linux/amd64 \
  --build-arg NEXT_PUBLIC_API_URL=https://api.xpeditis.com \
  --build-arg NEXT_PUBLIC_APP_URL=https://app.xpeditis.com \
  -t "$REGISTRY/xpeditis-frontend:prod-$SHA" \
  -f apps/frontend/Dockerfile apps/frontend --push

# Log exporter
docker buildx build --platform linux/amd64 \
  -t "$REGISTRY/xpeditis-log-exporter:prod-$SHA" \
  -f apps/log-exporter/Dockerfile apps/log-exporter --push

next.config.js fige NEXT_PUBLIC_API_URL au moment du build. Une image construite avec l'URL de preprod appellera api.preprod.xpeditis.com en production, quelles que soient les variables injectées dans le pod. C'est pour cette raison que le frontend est reconstruit pour la production et jamais promu depuis la preprod.

Vérifiez que l'URL de preprod n'a pas fuité dans le bundle :

CID=$(docker create "$REGISTRY/xpeditis-frontend:prod-$SHA")
docker cp "$CID:/app/.next" /tmp/next-check && docker rm "$CID"
grep -rl "api.preprod.xpeditis.com" /tmp/next-check && echo "PROBLEME" || echo "OK"
rm -rf /tmp/next-check

2. Appliquer la configuration

export KUBECONFIG=~/.kube/xpeditis-prod.yaml
cd infra/prod

kubectl apply -f k8s/base/00-namespaces.yaml
kubectl apply -f k8s/base/01-limits.yaml
kubectl apply -f k8s/base/02-configmap-backend.yaml
kubectl apply -f k8s/base/08-traefik-middlewares.yaml
kubectl apply -f k8s/base/10-network-policies.yaml
kubectl apply -f k8s/base/11-certificate.yaml

Relisez la configuration avant d'aller plus loin :

kubectl -n xpeditis-prod get cm xpeditis-backend-config -o yaml | grep -E 'DATABASE_HOST|DATABASE_SSL|COOKIE_DOMAIN|CORS_ORIGIN|AWS_S3'

Cinq valeurs à ne pas rater :

  • DATABASE_SSL: "true" — pg_hba n'accepte que hostssl ;
  • COOKIE_DOMAIN: ".xpeditis.com" — avec le point initial ;
  • CORS_ORIGIN — doit contenir exactement les origines du frontend (credentials: true interdit le joker *) ;
  • NODE_ENV: "production" — c'est ce qui empêche SeedTestUsers de s'exécuter ;
  • BOOTSTRAP_ADMIN_EMAIL — une adresse que vous relevez réellement, c'est par elle que passera le lien de définition du mot de passe.

3. Migrations, puis démarrage

3.1 Migrations

sed "s|__IMAGE_TAG__|prod-$SHA|g" k8s/base/07-migration-job.yaml | kubectl apply -f -
kubectl -n xpeditis-prod wait --for=condition=complete "job/xpeditis-migrate-prod-$SHA" --timeout=900s
kubectl -n xpeditis-prod logs "job/xpeditis-migrate-prod-$SHA"

40 migrations doivent s'appliquer. En cas d'échec, les journaux du Job donnent la requête SQL fautive.

Trois lignes à repérer dans la sortie :

SeedTestUsers ignore : NODE_ENV=production. ...
[neutralisation] Aucun compte de démonstration présent.
[amorçage admin] Administrateur ops@xpeditis.com créé (aucun mot de passe — à définir via « mot de passe oublié »).

Si vous lisez Seeded test users successfully, arrêtez-vous : NODE_ENV n'est pas positionné à production dans le ConfigMap. Corrigez, puis exécutez infra/prod/scripts/harden-seed-data.sh sur db-01 avant de continuer.

Pourquoi un Job. L'image lance déjà les migrations à chaque démarrage de pod (scripts/setup/startup.js). Avec deux replicas, deux processus migrent simultanément ; TypeORM ne sérialise pas entre processus et l'un des deux part en CrashLoopBackOff. Le Job (parallélisme 1) applique tout d'abord ; startup.js ne fait ensuite que constater qu'il n'y a rien à migrer.

3.2 Vérifier l'état des comptes avant d'ouvrir quoi que ce soit

ssh deploy@<db_public_ipv4> 'cd /opt/xpeditis/data-node && \
  sudo docker compose exec -T -u postgres postgres psql -d xpeditis_prod -c \
  "SELECT email, role, is_active FROM users ORDER BY role, email;"'

Attendu : une seule ligne, votre administrateur, is_active = t. Aucune adresse @xpeditis.com de démonstration ne doit apparaître.

3.3 Démarrer l'application

kubectl apply -f k8s/base/04-backend.yaml
kubectl apply -f k8s/base/05-frontend.yaml
kubectl apply -f k8s/base/06-log-exporter.yaml

kubectl -n xpeditis-prod set image deploy/xpeditis-backend  "backend=$REGISTRY/xpeditis-backend:prod-$SHA"
kubectl -n xpeditis-prod set image deploy/xpeditis-frontend "frontend=$REGISTRY/xpeditis-frontend:prod-$SHA"
kubectl -n xpeditis-prod set image deploy/xpeditis-log-exporter "log-exporter=$REGISTRY/xpeditis-log-exporter:prod-$SHA"

kubectl -n xpeditis-prod rollout status deploy/xpeditis-backend  --timeout=300s
kubectl -n xpeditis-prod rollout status deploy/xpeditis-frontend --timeout=300s

3.4 Exposer

kubectl apply -f k8s/base/09-ingress.yaml
kubectl -n xpeditis-prod get ingress

4. Vérifications

kubectl -n xpeditis-prod get pods -o wide
kubectl -n xpeditis-prod logs -l app.kubernetes.io/name=xpeditis-backend --tail=50

Le backend doit afficher PostgreSQL is ready, No pending migrations, puis la bannière de démarrage.

# De l'extérieur
curl -s https://api.xpeditis.com/api/v1/health | jq
curl -sI https://app.xpeditis.com/ | head -1

# Les comptes de démonstration sont bien morts
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://api.xpeditis.com/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@xpeditis.com","password":"Password123!"}'
# Attendu : 401

# Swagger désactivé
curl -s -o /dev/null -w '%{http_code}\n' https://api.xpeditis.com/api/docs
# Attendu : 404 (ou 401 si vous avez choisi de le protéger)

bash scripts/smoke-test.sh

Symptômes fréquents

Symptôme Cause probable Vérification
CrashLoopBackOff, password authentication failed DATABASE_PASSWORD ≠ POSTGRES_PASSWORD de db-01 Comparer les deux
CrashLoopBackOff, no pg_hba.conf entry ... SSL off DATABASE_SSL absent ou à false ConfigMap
CrashLoopBackOff, config validation error Une variable requise par Joi manque Les journaux la nomment
ImagePullBackOff regcred absent ou périmé kubectl -n xpeditis-prod get secret regcred
Connexion OK mais retour sur /login COOKIE_DOMAIN sans point initial ConfigMap
Erreurs CORS dans la console navigateur CORS_ORIGIN incomplet ConfigMap
L'app appelle api.preprod… Image frontend promue au lieu d'être reconstruite Reconstruire

5. Définir le mot de passe de votre administrateur

Le compte a été créé par la migration, sans mot de passe utilisable. Vous définissez le vôtre par le flux de réinitialisation — le même que celui de vos utilisateurs, ce qui le valide au passage.

# 1. Demander le lien
curl -sS -X POST https://api.xpeditis.com/api/v1/auth/forgot-password \
  -H 'Content-Type: application/json' \
  -d '{"email":"ops@xpeditis.com"}'
# Réponse toujours 200, même pour une adresse inconnue (anti-énumération).

Ou simplement depuis https://app.xpeditis.com/fr/forgot-password.

2. Ouvrir le lien reçu par courriel (valable 1 heure, à usage unique)
3. Définir un mot de passe long et aléatoire, issu de votre gestionnaire
4. Se connecter sur https://app.xpeditis.com/fr/login

Contrôles

# Un seul ADMIN actif, le vôtre
ssh deploy@<db_public_ipv4> 'cd /opt/xpeditis/data-node && \
  sudo docker compose exec -T -u postgres postgres psql -d xpeditis_prod -c \
  "SELECT email, role, is_active FROM users WHERE role = '"'"'ADMIN'"'"';"'

Si vous avez utilisé la variante BOOTSTRAP_ADMIN_PASSWORD_HASH : connectez-vous, changez le mot de passe depuis l'interface, puis retirez la variable du Secret et réappliquez-le. Un hash qui reste dans un coffre-fort est une cible d'attaque hors ligne sans aucune contrepartie une fois le compte opérationnel.

Si le courriel n'arrive pas

C'est la chaîne SMTP qui est en cause :

kubectl -n xpeditis-prod logs -l app.kubernetes.io/name=xpeditis-backend | grep -i smtp

Causes habituelles : clé SMTP Brevo invalide (celle de preprod est compromise et doit avoir été remplacée), domaine expéditeur non vérifié chez Brevo, SPF ou DKIM absents (08).


6. Données de référence

Les migrations installent déjà les ports (SeedMajorPorts), les transporteurs (SeedCarriersAndOrganizations) et les abonnements gratuits (SeedFreeSubscriptions).

ssh deploy@<db_public_ipv4> 'cd /opt/xpeditis/data-node && \
  sudo docker compose exec -T -u postgres postgres psql -d xpeditis_prod -c "
    SELECT '\''ports'\'' AS t, count(*) FROM ports
    UNION ALL SELECT '\''carriers'\'', count(*) FROM carriers
    UNION ALL SELECT '\''organizations'\'', count(*) FROM organizations
    UNION ALL SELECT '\''users'\'', count(*) FROM users;"'

Trois organisations de démonstration subsistent (Test Freight Forwarder Inc., Demo Shipping Company, Sample Shipper Ltd.). Elles ne sont pas supprimées automatiquement : les comptes désactivés y sont rattachés et une suppression en cascade toucherait audit_logs. Renommez-les ou masquez-les depuis l'interface d'administration si elles gênent.

Grilles tarifaires

Les grilles se chargent par l'interface d'administration (import CSV 33 colonnes). Chargez au moins une grille export avant l'ouverture : sans grille, la recherche de tarifs ne renvoie rien et la plateforme paraît cassée.


7. Contrôle

[ ] Images prod-<sha> construites et poussées (frontend reconstruit)
[ ] Aucune URL de preprod dans le bundle frontend
[ ] ConfigMap vérifié : NODE_ENV, DATABASE_SSL, COOKIE_DOMAIN, CORS_ORIGIN,
    BOOTSTRAP_ADMIN_EMAIL
[ ] Job de migration terminé, 40 migrations appliquées
[ ] Journal du Job : « SeedTestUsers ignore : NODE_ENV=production »
[ ] Journal du Job : « [amorçage admin] Administrateur ... créé »
[ ] La table users ne contient QUE votre administrateur
[ ] Connexion admin@xpeditis.com / Password123! → 401
[ ] Backend et frontend : 2 pods Running chacun
[ ] Ingress créés, https://api.xpeditis.com/api/v1/health → 200
[ ] Swagger inaccessible
[ ] smoke-test.sh au vert
[ ] Lien « mot de passe oublié » reçu, mot de passe défini, connexion réussie
[ ] Un seul ADMIN actif en base
[ ] Au moins une grille tarifaire chargée

→ Suite : 10 — CI/CD