# 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-`. Pour le premier déploiement, on les fabrique à la main. ```bash export SHA=$(git rev-parse --short=7 HEAD) export REGISTRY=rg.fr-par.scw.cloud/weworkstudio docker login "$REGISTRY" -u nologin -p '' # 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 : ```bash 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 ```bash 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 : ```bash 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 ```bash 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 ```bash ssh deploy@ '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 ```bash 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 ```bash kubectl apply -f k8s/base/09-ingress.yaml kubectl -n xpeditis-prod get ingress ``` --- ## 4. Vérifications ```bash 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. ```bash # 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. ```bash # 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 ```bash # Un seul ADMIN actif, le vôtre ssh deploy@ '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 : ```bash 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](./08-dns-tls-cloudflare.md)). --- ## 6. Données de référence Les migrations installent déjà les ports (`SeedMajorPorts`), les transporteurs (`SeedCarriersAndOrganizations`) et les abonnements gratuits (`SeedFreeSubscriptions`). ```bash ssh deploy@ '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- 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](./10-cicd-github-actions.md)**