infra / github-actions
Déploiement continu : build, push Docker, déploiement serveur
Explication
Ce que vous allez apprendre
- Assembler un pipeline complet : build d'image Docker, push vers un registre, déploiement
- Comprendre pourquoi empaqueter l'application en image Docker garantit un environnement identique
- Étiqueter chaque image poussée pour identifier précisément quelle version tourne où
- Comparer deux approches de déploiement final : SSH classique et
kubectl rolloutsous Kubernetes - Protéger un déploiement de production avec un
environment, vu à la leçon 8
Dans quel contexte ?
Une application tourne différemment sur la machine du développeur, sur le serveur de CI et sur le serveur de production : une version de Node légèrement différente, une dépendance système manquante, et un bug qui "marchait chez moi" apparaît en production sans qu'on comprenne pourquoi. Empaqueter l'application dans une image Docker construite une seule fois, testée en CI, puis déployée telle quelle, élimine cette catégorie entière de problèmes : ce qui tourne en production est exactement ce qui a été testé, au bit près.
Assembler ce qui a été vu jusqu'ici
D'abord, cette leçon relie plusieurs briques déjà étudiées séparément, cache, secrets, environments, artifacts, dans un scénario réel et complet.
Le scénario complet
Construire une image Docker, la publier dans un registre, puis déployer cette image sur un serveur. Comprendre le déploiement continu, c'est comprendre comment ces briques s'enchaînent dans un ordre logique.
| Étape | Rôle |
|---|---|
| Build de l'image Docker | Empaqueter l'application avec un environnement identique à celui testé |
Push vers un registre (ghcr.io) | Rendre l'image disponible pour le serveur ou le cluster cible |
| Tag par hash de commit | Identifier précisément quelle version du code tourne en production |
Déploiement (SSH ou kubectl rollout) | Récupérer et lancer l'image déjà construite, sans jamais recompiler |
Pourquoi construire une image plutôt que déployer le code brut
Empaqueter l'application dans une image Docker garantit que l'environnement d'exécution en production est identique à celui testé en CI, mêmes dépendances, même version du runtime.
Ce que devient cette image ensuite
Cette image est ensuite poussée vers un "registre", un espace de stockage d'images, ici ghcr.io, le registre intégré à GitHub, pour être ensuite téléchargée et lancée sur le serveur cible.
Prérequis
Cette leçon assemble des briques déjà vues séparément (cache, secrets, environments, artifacts). Si l'une d'elles n'est pas claire, mieux vaut y revenir avant d'aborder ce scénario complet.
Le tag d'image : retrouver exactement quelle version tourne
Une fois l'image construite, il reste un point important : chaque image poussée est étiquetée, par exemple avec le hash court du commit, pour pouvoir identifier précisément quelle version du code correspond à quelle image en production.
Pourquoi ce détail compte en cas de problème
C'est indispensable pour diagnostiquer un problème ou revenir à une version antérieure en cas de souci.
Deux façons courantes d'appliquer le déploiement final
Une fois l'image disponible dans le registre, le déploiement effectif peut prendre deux formes : se connecter en SSH au serveur pour lui dire de récupérer la nouvelle image et redémarrer le service.
L'autre forme, en contexte Kubernetes
Dans un contexte Kubernetes, demander au cluster de basculer progressivement vers la nouvelle version, avec kubectl rollout.
Le point commun entre ces deux approches
Les deux approches partagent la même idée : le serveur ou le cluster récupère une image déjà construite et validée, il ne recompile jamais le code lui-même.
Le rappel de sécurité qui ferme la boucle
Ce déploiement final devrait toujours être protégé par un environment:, vu en leçon 8, pour garder un contrôle humain avant qu'un changement n'atteigne réellement les utilisateurs.
Piège fréquent
Déployer directement sans passer par un environment: protégé revient à annuler tout le contrôle mis en place à la leçon 8 : même avec un pipeline parfaitement construit, une image bugguée peut alors atteindre la production sans qu'aucun humain n'ait eu l'occasion de l'arrêter.
La dernière leçon de ce cours change de point de vue : celui d'un attaquant, pour sécuriser un pipeline qui a souvent accès à des secrets puissants.
Commandes & code
Déploiement continu
name: Build, Push and Deploy
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # nécessaire pour pousser vers GitHub Container Registry
outputs:
image_tag: ${{ steps.meta.outputs.tags }}
steps:
- uses: actions/checkout@v4
- name: Login au registre
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Métadonnées d'image (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,format=short
type=raw,value=latest,enable={{is_default_branch}}
- uses: docker/setup-buildx-action@v3
- name: Build et push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max deploy:
needs: build-and-push
runs-on: ubuntu-latest
environment: production
steps:
- name: Déployer sur le serveur via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_SSH_USER }}
key: ${{ secrets.PROD_SSH_PRIVATE_KEY }}
script: |
cd /srv/app
docker compose pull api
docker compose up -d --no-deps --wait api
docker image prune -f --filter "until=72h"# Alternative : déploiement Kubernetes via kubectl, en s'authentifiant avec un kubeconfig secret
deploy-k8s:
needs: build-and-push
runs-on: ubuntu-latest
environment: production
steps:
- uses: azure/setup-kubectl@v4
- name: Configurer l'accès au cluster
run: echo "${{ secrets.KUBE_CONFIG_BASE64 }}" | base64 -d > kubeconfig.yaml
- name: Rolling update
run: |
kubectl --kubeconfig=kubeconfig.yaml set image deployment/api \
api=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
kubectl --kubeconfig=kubeconfig.yaml rollout status deployment/apiRésumé
docker/build-push-action+docker/metadata-actioncouvrent build, tag et push en quelques steps.GITHUB_TOKENsuffit pour publier surghcr.io(registre intégré à GitHub), sans secret externe.- Le déploiement final peut être du SSH direct (
docker compose pull && up -d --wait) oukubectl. - Toujours gater le job
deployderrière unenvironment:protégé en production.
Exercices pratiques
Mission : une image 'latest' impossible à tracer en production
Objectif : Corriger un pipeline de déploiement Docker qui ne trace pas ses versions et qui déploie sans contrôle humain.
Contexte
Un pipeline construit une image Docker et la pousse vers ghcr.io, mais ne la tague qu'avec latest, sans référence au commit source. Le job deploy enchaîne directement un docker compose pull && up -d sur chaque push vers main, sans aucun environment:. Un incident de production survient, et personne ne parvient à identifier précisément quelle version du code tourne réellement, ni qui a laissé partir ce déploiement.