Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
8.6 KiB
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, toutpushsurmaindé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.
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.
# 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
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
# 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 :
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 :
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
# 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 :
ssh deploy@<app_public_ipv4> 'sudo journalctl -t xpeditis-ssh-deploy -n 20'
5. Premier déploiement automatique
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é
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() :
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 :
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.
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: 0suffit à é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) ;
- 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é