Retour au cours

infra / github-actions

Déploiement continu : build, push Docker, déploiement serveur

Leçon 121 exercice

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 rollout sous 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.

ÉtapeRôle
Build de l'image DockerEmpaqueter 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 commitIdentifier 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

yaml
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
yaml
  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"
yaml
# 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/api

Résumé

  • docker/build-push-action + docker/metadata-action couvrent build, tag et push en quelques steps.
  • GITHUB_TOKEN suffit pour publier sur ghcr.io (registre intégré à GitHub), sans secret externe.
  • Le déploiement final peut être du SSH direct (docker compose pull && up -d --wait) ou kubectl.
  • Toujours gater le job deploy derrière un environment: protégé en production.

Exercices pratiques

1 disponible
1

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.

Résoudre l’exercice →