Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
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 dansBOOTSTRAP_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.jsfigeNEXT_PUBLIC_API_URLau moment du build. Une image construite avec l'URL de preprod appelleraapi.preprod.xpeditis.comen 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_hban'accepte quehostssl;COOKIE_DOMAIN: ".xpeditis.com"— avec le point initial ;CORS_ORIGIN— doit contenir exactement les origines du frontend (credentials: trueinterdit le joker*) ;NODE_ENV: "production"— c'est ce qui empêcheSeedTestUsersde 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 enCrashLoopBackOff. Le Job (parallélisme 1) applique tout d'abord ;startup.jsne 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