xpeditis2.0/docs/mise-en-prod/10-cicd-github-actions.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

261 lines
8.6 KiB
Markdown

# 10 — CI/CD GitHub Actions
**Durée : environ 1 h 30.**
Une fois le premier déploiement manuel réussi, on automatise. Le workflow
`.github/workflows/cd-main.yml` a été réécrit pour cette infrastructure.
---
## 1. Enchaînement
```
push sur main
│
├─ Lint + type-check + tests unitaires (backend, frontend)
│
├─ Vérification : l'image preprod-<sha> existe-t-elle ?
│ Si non → BLOCAGE. Ce commit n'est pas passé par la preprod.
│
├─ Promotion du backend preprod-<sha> → prod-<sha> (aucun rebuild)
├─ Reconstruction du frontend avec les URLs de production
│ + contrôle : aucune URL de preprod dans le bundle
│
├─ Déploiement (environnement protégé « production »)
│ 1. ouverture du port 22 pour l'IP du runner (firewall Hetzner dédié)
│ 2. rsync de infra/prod vers app-01
│ 3. ssh « deploy prod-<sha> » → migrations, images, attente
│ 4. tests de fumée depuis l'extérieur
│ 5. retour arrière automatique en cas d'échec
│ 6. fermeture du firewall — TOUJOURS, même sur échec ou annulation
│
└─ Notification Discord
```
---
## 2. Les trois décisions structurantes
### 2.1 Le backend est promu, le frontend est reconstruit
Promouvoir garantit que le binaire déployé est **exactement** celui qui a passé
la chaîne de preprod, au condensat près. Un rebuild casserait cette garantie.
Mais `next.config.js` fige `NEXT_PUBLIC_API_URL` au moment du build. L'ancien
workflow re-taguait l'image frontend de preprod vers la production : le résultat
aurait été une application appelant `api.preprod.xpeditis.com` en production.
Le frontend est donc **reconstruit** depuis le commit exact déjà vérifié, avec
les URLs de production, et une étape de contrôle échoue si la chaîne
`api.preprod.xpeditis.com` se retrouve malgré tout dans le bundle.
### 2.2 Le déploiement passe par SSH, pas par l'API Kubernetes
L'API k3s (6443) n'est ouverte qu'à vos IP d'administration. Les runners GitHub
n'ont pas d'IP fixe, et leurs rangs publiés sont trop vastes et trop mouvants
pour une liste blanche.
Le job ouvre donc le port 22 pour **la seule IP du runner en cours**, via un
firewall Hetzner dédié (`xpeditis-prod-fw-cicd`), puis le referme dans une
étape `if: always()` — donc y compris si le déploiement échoue, si le job est
annulé ou s'il expire.
### 2.3 Le kubeconfig et la clé SOPS ne sont pas dans GitHub
- Le **kubeconfig** donne les pleins pouvoirs sur le cluster. Un dépôt compromis
ne doit pas les offrir. La CI utilise le kubeconfig local du serveur, à
travers une clé SSH restreinte à un script.
- La **clé age** déchiffre tous les secrets de production. Les secrets sont
appliqués depuis votre poste (`make secrets-apply`), jamais par la CI.
---
## 3. Configurer GitHub
### 3.1 Environnement protégé
**Settings → Environments → New environment → `production`**
| Réglage | Valeur |
|---|---|
| Required reviewers | **vous** (au moins une personne) |
| Deployment branches | `main` uniquement |
| Wait timer | 0 |
> Sans `Required reviewers`, tout `push` sur `main` déploie en production sans
> validation humaine. Avec, chaque déploiement demande une confirmation
> explicite — deux secondes qui ont déjà sauvé beaucoup de vendredis soir.
### 3.2 Secrets et variables
La liste complète, avec la façon d'obtenir chaque valeur, est dans
[`infra/prod/env/github-secrets.md`](../../infra/prod/env/github-secrets.md).
Résumé :
**Secrets** : `REGISTRY_TOKEN`, `HCLOUD_TOKEN_CICD`, `PROD_SSH_HOST`,
`PROD_SSH_USER`, `PROD_SSH_KEY`, `PROD_SSH_KNOWN_HOSTS`,
`NEXT_PUBLIC_API_URL_PROD`, `NEXT_PUBLIC_APP_URL_PROD`, `DISCORD_WEBHOOK_URL`.
**Variables** : `PROD_API_URL`, `PROD_APP_URL`, `HCLOUD_CICD_FIREWALL`.
```bash
# Empreinte du serveur, pour PROD_SSH_KNOWN_HOSTS
ssh-keyscan -H <app_public_ipv4>
```
> L'empreinte épinglée n'est pas une formalité : sans elle, un détournement DNS
> ou BGP pourrait rediriger le déploiement — clé SSH comprise — vers une
> machine tierce.
---
## 4. Préparer le serveur
### 4.1 Répertoire de travail
```bash
ssh -i ~/.ssh/xpeditis_prod deploy@<app_public_ipv4> '
sudo mkdir -p /opt/xpeditis/infra-prod
sudo chown -R deploy:deploy /opt/xpeditis
'
```
### 4.2 Clé de déploiement restreinte
```bash
# Clé publique de la CI (générée en 01-prerequis.md)
cat ~/.ssh/xpeditis_ci.pub
```
Sur app-01, ajoutez-la **avec des restrictions** :
```bash
ssh -i ~/.ssh/xpeditis_prod deploy@<app_public_ipv4> '
cat >> ~/.ssh/authorized_keys <<EOF
restrict,pty,command="/opt/xpeditis/infra-prod/scripts/ssh-deploy-wrapper.sh" ssh-ed25519 AAAA... github-actions-prod
EOF
chmod 600 ~/.ssh/authorized_keys
'
```
`restrict` désactive le transfert de ports, d'agent et X11.
`command=` force l'exécution du script quelle que soit la commande demandée :
**une clé volée ne donne pas un shell**.
Le wrapper (`infra/prod/scripts/ssh-deploy-wrapper.sh`) n'accepte que quatre
choses : le `rsync` de `infra/prod`, `deploy prod-<sha>` (avec validation
stricte du tag), `rollback` et `status`. Tout le reste est refusé et journalisé.
### 4.3 Premier envoi manuel du wrapper
Le wrapper doit exister avant que la clé restreinte ne puisse servir :
```bash
rsync -az -e "ssh -i ~/.ssh/xpeditis_prod" \
infra/prod/ deploy@<app_public_ipv4>:/opt/xpeditis/infra-prod/
ssh -i ~/.ssh/xpeditis_prod deploy@<app_public_ipv4> \
'chmod +x /opt/xpeditis/infra-prod/scripts/*.sh'
```
### 4.4 Tester la clé restreinte
```bash
# Autorisé
ssh -i ~/.ssh/xpeditis_ci deploy@<app_public_ipv4> "status"
# Refusé — c'est le résultat attendu
ssh -i ~/.ssh/xpeditis_ci deploy@<app_public_ipv4> "cat /opt/xpeditis/data-node/.env.data"
ssh -i ~/.ssh/xpeditis_ci deploy@<app_public_ipv4> # pas de shell
ssh -i ~/.ssh/xpeditis_ci deploy@<app_public_ipv4> "deploy ; rm -rf /"
```
Les refus sont tracés :
```bash
ssh deploy@<app_public_ipv4> 'sudo journalctl -t xpeditis-ssh-deploy -n 20'
```
---
## 5. Premier déploiement automatique
```bash
git checkout preprod && git merge main && git push # chaîne preprod
# ... attendre que cd-preprod.yml passe au vert ...
git checkout main && git merge preprod && git push
```
Suivez l'exécution dans l'onglet Actions. Le job `deploy` attendra votre
approbation.
### Vérifier que le firewall s'est bien refermé
```bash
hcloud firewall describe xpeditis-prod-fw-cicd
```
Attendu : **aucune règle**. S'il en reste une, une exécution a été interrompue
d'une façon qui a contourné le `if: always()` :
```bash
echo '[]' > /tmp/empty.json
hcloud firewall replace-rules xpeditis-prod-fw-cicd --rules-file /tmp/empty.json
```
Ajoutez ce contrôle à votre routine hebdomadaire.
---
## 6. Le workflow de retour arrière
`.github/workflows/rollback.yml` existe déjà. Vérifiez qu'il cible bien la
nouvelle infrastructure ; à défaut, le retour arrière manuel reste disponible :
```bash
ssh -i ~/.ssh/xpeditis_ci deploy@<app_public_ipv4> "rollback"
# ou
make -C infra/prod rollback
```
> **Le retour arrière ne défait pas les migrations.** Si la version retirée
> contenait une migration destructrice (colonne supprimée, type modifié),
> l'ancienne version applicative peut ne plus fonctionner contre le schéma
> courant. Voir
> [15 § Retour arrière avec migration](./15-exploitation-incidents.md#retour-arriere-avec-migration).
---
## 7. Ce que la chaîne ne fait pas
Elle est volontairement conservatrice. Elle **ne fait pas** :
- de tests end-to-end contre la production (Playwright tourne sur la preprod) ;
- de déploiement bleu-vert ou canari — inutile à cette échelle, le
`maxUnavailable: 0` suffit à éviter toute coupure ;
- de sauvegarde avant déploiement — la sauvegarde quotidienne et l'archivage
continu couvrent le besoin. Avant une migration risquée, prenez une
sauvegarde manuelle ([12](./12-sauvegardes-restauration.md)) ;
- d'analyse de vulnérabilité des images. À ajouter (Trivy) quand vous en aurez
le temps ; ce n'est pas bloquant pour le lancement.
---
## 8. Contrôle
```
[ ] Environnement GitHub « production » créé, Required reviewers actif
[ ] Branches de déploiement limitées à main
[ ] 9 secrets renseignés
[ ] 3 variables renseignées
[ ] /opt/xpeditis/infra-prod créé et appartenant à deploy
[ ] Clé CI ajoutée avec restrict + command=
[ ] « status » fonctionne, tout le reste est refusé
[ ] Refus visibles dans journalctl -t xpeditis-ssh-deploy
[ ] Premier déploiement automatique réussi
[ ] Firewall CI vide après exécution
[ ] Notification Discord reçue
```
→ **Suite : [11 — Observabilité](./11-observabilite.md)**