Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
261 lines
8.6 KiB
Markdown
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)**
|