# 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- existe-t-elle ? │ Si non → BLOCAGE. Ce commit n'est pas passé par la preprod. │ ├─ Promotion du backend preprod- → prod- (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- » → 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 ``` > 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@ ' 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@ ' cat >> ~/.ssh/authorized_keys <` (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@:/opt/xpeditis/infra-prod/ ssh -i ~/.ssh/xpeditis_prod deploy@ \ 'chmod +x /opt/xpeditis/infra-prod/scripts/*.sh' ``` ### 4.4 Tester la clé restreinte ```bash # Autorisé ssh -i ~/.ssh/xpeditis_ci deploy@ "status" # Refusé — c'est le résultat attendu ssh -i ~/.ssh/xpeditis_ci deploy@ "cat /opt/xpeditis/data-node/.env.data" ssh -i ~/.ssh/xpeditis_ci deploy@ # pas de shell ssh -i ~/.ssh/xpeditis_ci deploy@ "deploy ; rm -rf /" ``` Les refus sont tracés : ```bash ssh deploy@ '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@ "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)**