Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
338 lines
13 KiB
Markdown
338 lines
13 KiB
Markdown
# 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.
|
|
|
|
```bash
|
|
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 :
|
|
|
|
```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@<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
|
|
|
|
```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@<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 :
|
|
|
|
```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@<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](./10-cicd-github-actions.md)**
|