Cet article est également disponible en anglais.
Read in English

GitHub Actions pour le CI/CD : Un Guide Complet

20 min de lecture
GitHub Actions pour le CI/CD : Un Guide Complet
Un guide complet pour construire des pipelines d'intégration et de déploiement continus avec GitHub Actions. Apprenez à configurer des workflows, à créer des images de conteneurs et à automatiser en toute sécurité les déploiements sur Kubernetes.

L'Intégration Continue et la Livraison Continue (CI/CD) sont au cœur de la philosophie DevOps. L'objectif est simple : au lieu de construire, pousser et déployer manuellement une application, nous définissons un pipeline qui effectue ces étapes automatiquement.

Pipeline CI/CD avec GitHub Actions

Les principaux avantages par rapport à un déploiement manuel en ligne de commande ou à un script de déploiement local sont :

  • Répétabilité : les mêmes étapes de déploiement s'exécutent à chaque fois, réduisant les erreurs humaines.
  • Suivi des versions : chaque déploiement est lié à un commit Git, ce qui facilite la connaissance des changements et de leur date. Si quelque chose casse, nous pouvons redéployer une image ou un commit précédent plus facilement.
  • Automatisation : les déploiements peuvent se faire automatiquement après un push, une fusion ou une release.
  • Meilleure sécurité : les processus CI/CD peuvent être isolés de votre environnement de production, permettant uniquement au runner de posséder les informations d'identification pour le déploiement.
  • Auditabilité : les journaux CI/CD fournissent une trace claire de qui a déclenché un déploiement et des modifications apportées.
  • Collaboration facilitée : tout le monde dans l'équipe suit le même processus de déploiement. Une fois le pipeline défini, l'ajout de nouvelles applications ou de nouveaux environnements devient plus facile.

Dans cet article, nous allons explorer en profondeur les GitHub Actions, ce qu'elles sont, comment elles fonctionnent et comment mettre en place un système CI/CD adéquat.

Anatomie d'un workflow

GitHub Actions est un système CI/CD intégré à GitHub. Il nous permet de définir des workflows automatisés qui se déclenchent lors d'événements sur le dépôt (par exemple, push, pull request, release).

Les workflows sont définis sous forme de fichiers YAML dans le répertoire .github/workflows/. Ils sont composés de jobs et de steps (étapes) :

  • Jobs : Un groupe d'étapes qui s'exécutent sur le même conteneur.
  • Steps : Des tâches individuelles (exécutant des commandes shell ou utilisant des actions préconstruites).

Nous nous concentrerons sur la création d'un workflow qui teste, construit et déploie notre application conteneurisée.

Un workflow CI/CD de base suit généralement cette structure :

Configuration d'un workflow GitHub Actions

En-tête / Configuration du workflow

D'abord, nous avons le haut du workflow où nous définissons un ensemble de paramètres globaux. Un en-tête de base ressemblerait à ceci :

name: Build and Deploy to Kubernetes
on:
  push:
    branches:
      - main
      - develop
  workflow_dispatch:

Le champ name définit le nom d'affichage du workflow dans l'interface utilisateur de GitHub Actions. C'est utile car un dépôt peut avoir plusieurs workflows.

La section on définit quand le workflow doit s'exécuter. Les workflows GitHub Actions sont déclenchés par des événements tels que push, pull_request, schedules, releases, ou une exécution manuelle. Il s'applique dès que le fichier de workflow est présent dans le dépôt Git sur GitHub.

Dans cet exemple, le workflow s'exécute dans deux situations :

  • lorsque du code est poussé vers les branches main ou develop
  • lorsque nous démarrons manuellement le workflow depuis l'interface GitHub Actions en utilisant workflow_dispatch. GitHub requiert ce déclencheur pour afficher le bouton "Run workflow" dans l'onglet Actions.

L'option workflow_dispatch vous permet également de définir des entrées si vous souhaitez contrôler plus précisément l'exécution manuelle. Par exemple, ici, nous définissons une variable environment :

on:
  workflow_dispatch:
    inputs:
      environment:
        description: "Environment to deploy to"
        required: true
        default: "staging"
        type: choice
        options:
          - staging
          - production

De nombreux autres déclencheurs existent, tels que :

  • pull_request : S'exécute lorsqu'une pull request a une activité quelconque (création, mise à jour, ...).
  • pull_request_target : Similaire à pull_request, il exécute un workflow dans le contexte du dépôt de base, lui permettant de gérer des PR avec des permissions plus élevées (plus risqué mais parfois utile).

Pour push, pull_request, et pull_request_target, vous pouvez spécifier des sous-paramètres pour affiner le moment où le workflow s'exécute :

  • branches / branches-ignore : Filtre par noms de branches (supporte les jokers).
  • tags / tags-ignore : Filtre par tags Git (supporte les jokers).
  • paths / paths-ignore : S'exécute uniquement si au moins un fichier modifié correspond au modèle.
on:
  push:
    branches:
      - main
      - "releases/**"
    tags:
      - "v*"
    paths:
      - "src/**"
      - "Dockerfile"

Vous pouvez déclencher des workflows en fonction de presque tous les événements qui se produisent sur GitHub. Beaucoup de ces événements acceptent un paramètre types pour spécifier quels types d'activité déclenchent l'exécution (par exemple, created, edited, deleted). Voici les événements les plus courants :

ParamètreDescription du déclencheur
workflow_callRend le workflow réutilisable pour qu'il puisse être appelé par d'autres workflows.
workflow_runDéclenché lorsqu'un autre workflow est demandé ou terminé.
scheduleExécute périodiquement le workflow à des moments précis en utilisant la syntaxe cron POSIX.
repository_dispatchDéclenché par une requête HTTP à l'API GitHub depuis un système externe.
issuesDéclenché lorsqu'une issue est ouverte, modifiée, fermée, etc.
issue_commentDéclenché lorsqu'un commentaire est créé, modifié ou supprimé sur une issue ou une PR.
releaseDéclenché lorsqu'une release est créée, publiée ou modifiée.
createDéclenché lorsqu'une branche ou un tag est créé.
deleteDéclenché lorsqu'une branche ou un tag est supprimé.
discussionDéclenché lorsqu'une Discussion GitHub est créée ou modifiée.
discussion_commentDéclenché lorsqu'un commentaire sur une discussion est créé ou modifié.
forkDéclenché lorsque quelqu'un forke le dépôt.
gollumDéclenché lorsqu'une page wiki est créée ou mise à jour.
labelDéclenché lorsqu'une étiquette est créée, modifiée ou supprimée.
milestoneDéclenché lorsqu'un jalon est créé, ouvert, terminé ou modifié.
projectDéclenché lorsqu'un tableau de projet classique est modifié.
registry_packageDéclenché lorsqu'un paquet est publié ou mis à jour (par exemple, GHCR).
statusDéclenché lorsque le statut d'un commit Git change.
watchDéclenché lorsqu'un dépôt est mis en favori (starred).

Vous pouvez combiner n'importe lequel de ces paramètres dans un seul workflow. Le workflow s'exécutera si l'un des événements définis se produit.

on:
  push:
    branches: [main]
  schedule:
    - cron: "0 12 * * *"
  issues:
    types: [opened, edited]
  workflow_dispatch:

jobs

Pipeline CI/CD avec GitHub Actions

Un workflow contient un ou plusieurs jobs. Un job est un groupe d'étapes qui s'exécutent sur le même runner. Dans les workflows simples, nous commençons souvent avec un seul job. Voici un job de base :

jobs:
  build-and-deploy:
    name: Build and deploy
    runs-on: ubuntu-latest
    env:
      IMAGE_NAME: my-app

Ici, build-and-deploy est l'identifiant interne du job. name est le nom lisible par l'homme affiché dans l'interface de GitHub. Enfin, le champ runs-on définit sur quelle machine le job s'exécutera. Actuellement, le runner est défini sur ubuntu-latest, ce qui signifie que le job s'exécute sur un runner Linux hébergé par GitHub.

La section env définit des variables d'environnement qui peuvent être réutilisées à l'intérieur du job.

Un job GitHub Actions peut être configuré avec plusieurs paramètres optionnels qui contrôlent où il s'exécute, combien de temps il peut s'exécuter, quelles permissions il a, et comment il se comporte en cas d'échec.

Par exemple, un job peut s'exécuter à l'intérieur d'une image de conteneur spécifique :

jobs:
  build:
    runs-on: ubuntu-latest
    container:
      image: node:20-alpine

Par défaut, un job s'exécute directement sur le runner hébergé par GitHub, comme ubuntu-latest. Cependant, l'utilisation de container vous permet d'exécuter le job à l'intérieur d'une image Docker prédéfinie.

C'est utile lorsque votre workflow a besoin d'outils ou de dépendances spécifiques. Vous pourriez installer ces outils pendant le workflow, mais cela ralentit chaque exécution. Une meilleure option est souvent d'utiliser une image personnalisée où les outils requis sont déjà installés.

Un job peut également définir de nombreux autres paramètres :

jobs:
  build:
    runs-on: ubuntu-latest
 
    # Exécute ce job uniquement sur la branche main
    if: github.ref == 'refs/heads/main'
 
    # Arrête le job s'il dure plus de 15 minutes
    timeout-minutes: 15
 
    # N'échoue pas tout le workflow si ce job échoue
    continue-on-error: true
 
    # Limite ce que le job est autorisé à faire
    permissions:
      contents: read # Lire depuis le dépôt
      packages: write # Publier des paquets
 
    # Attache ce job à un environnement GitHub
    environment: production
 
    # Exécute le même job avec plusieurs valeurs
    strategy:
      matrix:
        node-version: [18, 20, 22]
      fail-fast: true
      max-parallel: 2
 
    # Démarre des conteneurs supplémentaires nécessaires pendant le job, comme des bases de données ou des caches
    services:
      postgres:
        image: postgres:15
        ports:
          - 5432:5432
    steps:
      - uses: actions/checkout@v4
      - name: Use Node.js
        run: echo "Running with Node ${{ matrix.node-version }}"

Les jobs peuvent également produire des sorties (outputs) que d'autres jobs peuvent réutiliser :

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.get_version.outputs.version }}
    steps:
      - id: get_version
        run: echo "version=1.0.0" >> "$GITHUB_OUTPUT"

$GITHUB_OUTPUT est un chemin de fichier temporaire fourni par GitHub Actions où une étape peut écrire des valeurs de sortie que les étapes ou jobs ultérieurs peuvent réutiliser. Cela permet à un autre job d'accéder à la valeur :

jobs:
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying version ${{ needs.build.outputs.version }}"

Vous pouvez également définir des paramètres par défaut pour toutes les étapes run à l'intérieur d'un job :

jobs:
  build:
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash
        working-directory: ./frontend

Enfin, concurrency est utile lorsqu'une seule version d'un job doit s'exécuter à la fois, en particulier pour les déploiements :

jobs:
  deploy:
    runs-on: ubuntu-latest
    concurrency:
      group: production-deploy
      cancel-in-progress: true

Avec cancel-in-progress: true, si une nouvelle instance du workflow est déclenchée alors qu'une autre est encore en cours, GitHub annule l'ancienne et ne conserve que le déploiement le plus récent.

steps

Un job est composé d'une ou plusieurs étapes (steps).

Job de déploiement avec plusieurs étapes

Chaque étape représente une tâche unique dans le job. Une étape peut soit :

  1. Exécuter une commande shell en utilisant run.
  2. Utiliser une GitHub Action préconstruite en utilisant uses.

Par exemple :

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Install dependencies & Test
        run: |
          npm install
          npm test

Dans cet exemple, la première étape utilise une action existante, tandis que l'autre étape exécute des commandes shell. Spécifiquement, la première étape utilise l'action officielle checkout pour télécharger le code du dépôt dans le runner ; en gros, cela exécute un git clone. Il existe de nombreuses actions prédéfinies comme github/super-linter@v7 pour la vérification du code. Mais vous pouvez aussi créer votre propre action personnalisée à partager entre les workflows et les projets.

Note : le symbole | dans la commande run nous permet d'écrire plusieurs commandes shell en une seule étape.

Une étape peut également définir plusieurs paramètres optionnels similaires à ceux des jobs :

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Install dependencies
        if: github.ref == 'refs/heads/main'
        timeout-minutes: 10
        continue-on-error: true
        working-directory: ./frontend
        shell: bash
        env:
          NODE_ENV: production
        run: npm install

Certaines actions acceptent également des paramètres d'entrée en utilisant with :

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

Ici, nous utilisons setup-node qui configure la version demandée de Node.js sur le runner et l'ajoute au PATH, afin que les commandes comme node, npm, et npx fonctionnent dans les étapes ultérieures. Le paramètre with passe des valeurs de configuration à l'action. Dans ce cas, il indique à setup-node quelle version de Node.js installer et active la mise en cache de npm.

Les étapes peuvent aussi avoir un id. C'est utile lorsqu'une autre étape doit réutiliser sa sortie :

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Get version
        id: get_version
        run: echo "version=1.0.0" >> "$GITHUB_OUTPUT"
      - name: Print version
        run: echo "Version is ${{ steps.get_version.outputs.version }}"

La première étape écrit une valeur dans $GITHUB_OUTPUT. Comme l'étape a un id, les étapes ultérieures peuvent accéder à cette valeur en utilisant {{ steps.get_version.outputs.version }}.

En bref : Les jobs définissent l'environnement et les règles d'exécution. Les étapes définissent le travail réel à effectuer.

Création du Workflow

Maintenant que nous comprenons l'anatomie d'un workflow, assemblons un pipeline CI/CD complet. Cet exemple de workflow automatisera trois étapes critiques : tester votre code, construire une image de conteneur et la déployer sur votre cluster Kubernetes.

Le Workflow Complet

Créez un dépôt de test et enregistrez le YAML ci-dessous dans .github/workflows/deploy.yml à la racine de votre dépôt. Ensuite, commitez et poussez ce fichier.

name: CI/CD
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  workflow_dispatch:
 
concurrency:
  group: production-deploy
  cancel-in-progress: true
 
permissions:
  contents: read
 
jobs:
  test:
    name: Tester l'application
    runs-on: ubuntu-latest
    steps:
      - name: Récupérer le dépôt
        uses: actions/checkout@v4
      - name: Configurer Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - name: Installer les dépendances
        run: npm ci
      - name: Lancer les tests
        run: npm test
 
  build:
    name: Créer et pousser l'image
    runs-on: ubuntu-latest
    needs: test
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    permissions:
      contents: read
      packages: write
    outputs:
      image: ${{ steps.image.outputs.image }}
    steps:
      - name: Récupérer le dépôt
        uses: actions/checkout@v4
      - name: Définir le nom de l'image
        id: image
        run: |
          IMAGE="ghcr.io/${{ github.repository }}:${{ github.sha }}"
          echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
      - name: Connexion à GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Configurer Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Créer et pousser
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.image.outputs.image }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
 
  deploy:
    name: Déployer sur Kubernetes
    runs-on: ubuntu-latest
    needs: build
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    environment: production
    permissions:
      contents: read
    steps:
      - name: Installer kubectl
        uses: azure/setup-kubectl@v4
      - name: Configurer kubeconfig
        run: |
          mkdir -p ~/.kube
          echo "${{ secrets.KUBECONFIG_B64 }}" | base64 -d > ~/.kube/config
          chmod 600 ~/.kube/config
      - name: Déployer l'image
        run: |
          kubectl -n my-app-prd set image deployment/my-app \
            my-app=${{ needs.build.outputs.image }}
      - name: Attendre le déploiement
        run: |
          kubectl -n my-app-prd rollout status deployment/my-app --timeout=120s

Sécurité : Stocker un kubeconfig sous forme de secret CI encodé en Base64 est risqué. Le Base64 n’est pas un mécanisme de chiffrement, et ce fichier fournit souvent un accès privilégié et de longue durée au cluster. Privilégiez des alternatives plus sûres, comme un runner auto-hébergé avec des permissions Kubernetes limitées, ou une approche GitOps où le cluster récupère lui-même les changements, plutôt que d’exposer des identifiants d’accès au cluster à votre pipeline CI.

Exécution et Vérification

  • Déclenchement : Le workflow est configuré pour s'exécuter à chaque push et pull_request sur la branche main ou push sur la branche develop. Il peut également être déclenché manuellement grâce au déclencheur workflow_dispatch.
  • Suivi : Accédez à l'onglet Actions de votre dépôt GitHub. Vous y verrez le workflow CI/CD. Cliquez sur la dernière exécution pour observer les jobs de Test, Build et Deploy en temps réel.
    Interface des GitHub Actions

Cet exemple de workflow est composé de trois jobs :

  • Job de Test : Assure la stabilité du code. Nous utilisons npm ci pour une installation fiable des dépendances.
  • Job de Build : Ne s'exécute que sur les push vers main pour éviter les constructions d'images inutiles. Nous taguons l'image avec le SHA du commit spécifique (${{ github.sha }}) pour assurer une traçabilité exacte.
  • Job de Déploiement : Met à jour le cluster Kubernetes. La commande rollout status agit comme une étape de vérification automatisée pour confirmer que le nouveau déploiement est réussi.

Bien sûr, sans projet attaché, cela ne fera pas grand-chose. Si vous voulez voir un vrai dépôt avec un workflow simple, voici un exemple d'un petit projet sur lequel je travaille : https://github.com/Local-pie/PiTTS/blob/main/.github/workflows/deploy.yml. C'est un workflow très basique et non optimisé qui construit et pousse un frontend et un backend.

GitHub Actions Avancées

Maintenant que nous avons une compréhension approfondie de la syntaxe du workflow et de ses principales fonctionnalités, passons à un territoire plus avancé.

Variables Intégrées

GitHub Actions fournit des valeurs intégrées via des contextes. GitHub fournit des contextes tels que github, env, vars, secrets, steps, et runner pour accéder dynamiquement aux informations du workflow. L'un des contextes les plus utiles est le contexte github, qui contient des informations sur l'exécution du workflow, le dépôt, la branche, le commit, l'acteur et l'événement qui a déclenché le workflow. GitHub expose également beaucoup de ces valeurs comme variables d'environnement par défaut telles que GITHUB_SHA, GITHUB_REF, et GITHUB_RUN_ID.

Les valeurs utiles incluent :

VariableSignificationCas d'utilisation
${{ github.sha }}SHA du commit qui a déclenché le workflowTaguer les images Docker
${{ github.ref }}Référence Git complèteVérifier si le workflow s'exécute sur main
${{ github.ref_name }}Nom court de la branche ou du tagCréer des tags d'image basés sur la branche
${{ github.repository }}Nom du dépôt avec le propriétaireConstruire les noms d'image
${{ github.repository_owner }}Propriétaire ou organisation du dépôtConstruire les chemins de registre
${{ github.actor }}Utilisateur qui a déclenché le workflowMessages de journalisation ou d'audit
${{ github.event_name }}Événement qui a déclenché le workflowExécuter une logique différente pour push ou pull_request
${{ github.run_id }}ID unique de l'exécution du workflowCréer des ID de déploiement traçables
${{ github.run_number }}Numéro d'exécution incrémentiel pour le workflowCréer des numéros de build lisibles
${{ github.workflow }}Nom du workflowLogs, notifications, métadonnées de déploiement
${{ github.job }}ID du job actuelDébogage ou journalisation
${{ github.workspace }}Chemin où le dépôt est clonéChemins de fichiers dans les scripts

Pour les tags d'images Docker, ceux-ci sont particulièrement utiles :

${{ github.sha }} ${{ github.ref_name }} ${{ github.run_number }}

Une stratégie courante consiste à pousser plusieurs tags pour la même image :

docker build \
  -t ghcr.io/my-org/my-app:${{ github.sha }} \
  -t ghcr.io/my-org/my-app:${{ github.ref_name }} \
  -t ghcr.io/my-org/my-app:latest

Désormais, chaque build a un tag d'image unique. Au lieu de simplement pousser latest, nous savons exactement quel commit a produit chaque image. Cela vous donne différents niveaux de traçabilité :

TagUtilité
${{ github.sha }}Commit exact, idéal pour la reproductibilité
${{ github.ref_name }}Nom de la branche ou du tag, utile pour main, dev, ou v1.0.0
latestPratique par défaut, mais pas précis
build-${{ github.run_number }}Identifiant de build lisible par l'homme

Secrets Et Variables

Vous ne voudriez pas stocker votre clé API ou d'autres informations d'identification sur votre dépôt en texte clair. Ce serait imprudent. Les Secrets GitHub vous permettent de stocker ce genre de secrets et d'y accéder ensuite dans un workflow en utilisant le contexte secrets. Par exemple :

${{ secrets.REGISTRY_PASSWORD }}

Dans cet exemple, nous utilisons des secrets pour nous connecter à un registre de conteneurs :

  - name: Login to GHCR
	uses: docker/login-action@v3
	with:
	  registry: ghcr.io
	  username: ${{ github.actor }}
	  password: ${{ secrets.GITHUB_TOKEN }}

Pour créer un secret, allez dans les paramètres de votre dépôt sous Secrets and Variables. Ici, vous pouvez créer des secrets par environnement, au niveau du dépôt ou même au niveau de l'organisation si vous en faites partie.

Interface des secrets GitHub

Si un secret a le même nom dans les sections organisation, dépôt et environnement, GitHub utilisera le plus spécifique, donc : Environnement > Dépôt > Organisation.

D'ailleurs, si vous vous interrogez sur les environnements, c'est ce que fait le paramètre environment du job : il définit quel environnement utiliser et donc quels secrets et variables nous utilisons.

De même, vous pouvez aussi créer des variables qui sont un peu comme des secrets, sauf qu'une fois que vous écrivez un secret, vous ne pouvez plus jamais voir sa valeur depuis l'interface. Pour les variables, vous pouvez les lire plus tard.

Jobs Multiples

Un workflow peut également être divisé en plusieurs jobs. Par exemple :

name: CI/CD pipeline
on:
  push:
    branches:
      - main
jobs:
  test:
    name: Run tests
    runs-on: ubuntu-latest
    steps:
      - name: Checkout source code
        uses: actions/checkout@v4
      - name: Run tests
        run: |
          echo "Running tests..."
  deploy:
    name: Deploy application
    runs-on: ubuntu-latest
    needs: test
    steps:
      - name: Deploy
        run: |
          echo "Deploying application..."

Ici, nous avons deux jobs :

  • test : vérifie que l'application fonctionne
  • deploy : déploie l'application

La partie importante est :

needs: test

Les jobs s'exécutent en parallèle par défaut, ce qui peut être utile, mais parfois nous avons besoin qu'ils soient séquentiels. needs: test signifie que le job deploy ne démarre qu'après que le job test soit terminé et ait réussi. C'est utile car nous ne voulons généralement pas déployer une application si les tests échouent.

Grâce à cela, nous pouvons construire des pipelines complexes avec des dépendances et des jobs parallèles. Par exemple, exécuter plusieurs tests en parallèle avant de construire et de déployer.

Pipeline de déploiement avec des jobs parallèles et séquentiels
En règle générale, les étapes doivent rester dans le même job lorsqu'elles forment un processus continu et ont besoin de partager le même répertoire de travail.

Les jobs doivent être divisés lorsque nous voulons séparer les responsabilités. Par exemple, les tests, la construction et le déploiement peuvent être trois jobs différents. Cela rend le pipeline plus facile à lire, permet à certains jobs de s'exécuter en parallèle et nous permet de donner des permissions différentes à différentes parties du pipeline.

permissions

Un workflow peut également définir des permissions pour le GITHUB_TOKEN par défaut. C'est important car nous ne devrions donner au workflow que les permissions dont il a réellement besoin. GitHub recommande de configurer les permissions du token plutôt que de donner aux workflows un accès inutile.

Par exemple :

permissions:
  contents: read
  packages: write

Cela signifie que le workflow peut lire le contenu du dépôt et écrire des paquets, mais il ne reçoit pas automatiquement toutes les permissions possibles. D'autres permissions existent pour créer automatiquement des Pull Requests, commenter des issues, et plus encore !

Concurrency

Imaginez que vous poussez le commit A, puis rapidement le commit B. Sans contrôle de la concurrence, les deux déploiements pourraient s'exécuter en parallèle. Selon le timing, le commit A pourrait se terminer après le commit B et revenir accidentellement à une version antérieure de l'application.

Le paramètre concurrency résout ce problème :

concurrency:
  group: production-deploy
  cancel-in-progress: true

Un groupe de concurrence garantit qu'un seul workflow ou job du même groupe s'exécute à la fois. Avec cancel-in-progress: true, tout déploiement en cours est annulé lorsqu'un nouveau est déclenché, de sorte que seul le déploiement le plus récent continue. Avec cancel-in-progress: false, le déploiement en cours est autorisé à se terminer avant que le suivant ne commence.

Conseils de pro

Une meilleure étape de déploiement final

Après avoir déployé une application sur Kubernetes, ce que vous ferez très probablement, vous pouvez ajouter ceci :

 - name: Wait for rollout
	run: |
	  kubectl -n my-app-prd rollout status deployment/my-app --timeout=180s
 
  - name: Show running image
	run: |
	  kubectl -n my-app-prd get deployment my-app \
		-o=jsonpath='{.spec.template.spec.containers[0].image}'
	  echo
 
  - name: Smoke test
	run: |
	  curl --fail --silent --show-error https://example.com/health

Cela vous donne trois niveaux de confiance :

  1. Kubernetes a accepté le déploiement.
  2. Les nouveaux pods sont devenus prêts (ready).
  3. Le point d'accès public de l'application répond.

C'est bien mieux que de simplement appliquer du YAML.

Éviter le piège du latest

De nombreux tutoriels utilisent :

ghcr.io/owner/app:latest

C'est simple, mais cela crée une ambiguïté. Si le pod exécute latest, quel commit est réellement déployé ? Pire, Kubernetes peut ne pas toujours tirer l'image quand vous vous y attendez, en fonction du tag et de imagePullPolicy.

Pour les pipelines de déploiement, préférez des tags immuables ou traçables :

ghcr.io/owner/app:<commit-sha>

Optionnellement, publiez plusieurs tags :

ghcr.io/owner/app:<commit-sha>
ghcr.io/owner/app:main
ghcr.io/owner/app:v1.2.3

Mais déployez en utilisant le SHA du commit.

GitOps : CI/CD basé sur le Pull

Le workflow ci-dessus utilise un déploiement basé sur le push : GitHub Actions exécute le workflow et met à jour le déploiement.

Une autre approche est le GitOps. Avec GitOps, le cluster tire l'état désiré de Git en utilisant des outils comme Argo CD ou Flux. Dans ce modèle, GitHub Actions pourrait uniquement construire l'image et mettre à jour un dépôt de manifestes. Le cluster remarque le changement et l'applique.

Le GitOps est préférable lorsque vous ne voulez pas que GitHub Actions détienne des informations d'identification directes du cluster.

Utiliser les Environnements GitHub

Comme je l'ai expliqué dans la section Secrets et Variables, vous pouvez définir l'environnement dans lequel un job s'exécute avec :

jobs:
  build:
    runs-on: ubuntu-latest
 
    # Environnement à utiliser
	environment: ${{ github.ref_name == 'main' && 'production' || 'staging' }}
 
    steps:
      - uses: actions/checkout@v4
      - name: Use Node.js
        run: echo "Running with Node ${{ matrix.node-version }}"

Notez la condition ternaire utilisée. Pour la branche main, nous utilisons l'environnement de production, sinon nous utilisons staging.

C'est utile dans les situations où différents environnements ont des besoins différents. Par exemple, vous avez un secret de base de données qui change en fonction de l'environnement.

Avoir des environnements séparés est une bonne habitude même pour un petit projet. Un blog personnel n'a peut-être pas besoin d'un processus de release d'entreprise complet, mais il bénéficie toujours d'une frontière claire entre les tests et la production.

Une configuration courante est :

Pull request -> test uniquement Push vers develop -> build et déploiement en préproduction Pull request vers main -> approbation manuelle -> déploiement en production

Prochaines étapes

Une fois que ce pipeline de base fonctionne, vous pouvez l'améliorer progressivement :

  • Ajouter des environnements de staging et de production.
  • Ajouter une approbation manuelle avant la production.
  • Utiliser Helm ou Kustomize au lieu de kubectl direct.
  • Ajouter une analyse des vulnérabilités.
  • Signer les images.
  • Déployer par digest au lieu de tag.
  • Ajouter des environnements de prévisualisation pour les pull requests.
  • Créer des workflows réutilisables si plusieurs dépôts partagent le même pipeline.
  • Ajouter des notifications en cas d'échec de déploiement.

N'essayez pas d'ajouter tout cela le premier jour. Un bon pipeline CI/CD doit évoluer comme n'importe quel autre projet. Commencez par les tests, la construction d'images, le déploiement et la vérification du déploiement. Améliorez ensuite la sécurité et l'observabilité étape par étape.

Erreurs courantes dans les workflows de déploiement GitHub Actions

Erreur 1 : Déployer depuis des pull requests

Les pull requests peuvent contenir du code non fiable. Soyez prudent avec les secrets et les déploiements dans les workflows de pull request. Utilisez les pull requests pour les tests. Utilisez les mises à jour de branches pour le déploiement.

Erreur 2 : Oublier les secrets de tirage d'image (image pull secrets)

Si les images GHCR sont privées, Kubernetes a aussi besoin d'identifiants. Le fait que le workflow puisse pousser l'image ne signifie pas que le cluster peut la tirer.

Erreur 3 : Installer des outils manuellement dans chaque workflow

Si votre job de déploiement a besoin de kubectl, helm, kustomize, yq et des CLI cloud, envisagez de créer une petite image CI personnalisée ou d'utiliser une action de configuration bien maintenue. Cela garde les workflows plus rapides et plus propres.

Erreur 4 : Déployer en utilisant latest

Bien que plus simple, l'utilisation de l'image latest rend plus difficile le débogage et la traçabilité de l'origine d'une image. Cela peut également conduire à des situations où Kubernetes tire la mauvaise image en production si vous poussez latest en pré-production.

Dépannage

En attente qu'un runner prenne en charge ce job

Le job est en file d'attente mais aucun runner correspondant n'est disponible.

Vérifiez :

  • Est-ce que runs-on est correct ?
  • Si vous hébergez vous-même, le runner auto-hébergé est-il en ligne ?
  • Le runner a-t-il les labels requis ?
  • Si vous utilisez ARC, le pod listener est-il en cours d'exécution ?
  • Le runner est-il enregistré au niveau du dépôt, de l'organisation ou de l'entreprise ?
  • Si votre dépôt est public, avez-vous activé les dépôts publics pour le runner ?
    • Attention, cela signifie que n'importe qui pourrait démarrer un workflow si vous utilisez un déclencheur de pull request.

Pour ARC, rappelez-vous que la valeur de runs-on doit correspondre au nom d'installation du runner scale set.

Impossible de se connecter au démon Docker

Le runner n'a pas accès à Docker.

C'est courant avec les runners auto-hébergés basés sur Kubernetes. Les solutions possibles incluent :

  • Configurer Docker-in-Docker.
  • Utiliser BuildKit sans Docker.
  • Utiliser Kaniko ou Buildah.
  • Utiliser un builder distant.

Ne montez pas aveuglément /var/run/docker.sock depuis l'hôte. C'est puissant, mais cela peut donner au job un contrôle proche du niveau de l'hôte.

permission_denied: write_package

Le workflow ne peut pas pousser vers GHCR.

Vérifiez :

  • Le job a-t-il la permission packages: write ?
  • Utilisez-vous secrets.GITHUB_TOKEN correctement ?
  • Le paquet est-il connecté au dépôt ?
  • Le paquet a-t-il été créé manuellement avant le workflow ?
  • Poussez-vous vers le bon propriétaire et le bon chemin de dépôt ?

ImagePullBackOff

Kubernetes ne peut pas tirer l'image.

Vérifiez :

kubectl -n my-app-prd describe pod <nom-du-pod>

Causes courantes :

  • Mauvais nom d'image.
  • Mauvais tag.
  • Image GHCR privée sans imagePullSecret.
  • Le token n'a pas la permission read:packages.
  • Limitation de débit du registre ou problème réseau.

Si vous en avez assez des limitations de registre, vous pouvez auto-héberger un registre de conteneurs pour garder vos images plus près de l'endroit où elles sont construites et déployées.

N'oubliez pas l'architecture ARM64 vs AMD64

Pour un Raspberry Pi ou un cluster basé sur ARM, faites attention à l'architecture de l'image. Si GitHub construit une image amd64 et que votre Pi a besoin de arm64, le pod échouera. Docker Buildx peut construire des images multi-architectures :

     - name: Build and push multi-arch image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          platforms: linux/amd64,linux/arm64
          tags: ${{ steps.image.outputs.image }}

C'est utile si vous développez sur une architecture et déployez sur une autre.

#github-actions#ci-cd#devops#kubernetes#docker
Judicael Poumay (Ph.D.)

Judicael Poumay (Ph.D.)

Suivez-moi sur LinkedIn pour du contenu hebdomadaire Judicaël Poumay

En tant que chercheur/développeur IA indépendant spécialisé en Traitement du Langage Naturel (NLP), j'ai une expertise complète dans le développement et l'intégration de systèmes d'IA, ainsi que l'analyse de données.

Votre entreprise cherche à intégrer des solutions IA, analyser des données ou renforcer son développement back-end ? Contactez-moi !

Offrez-moi une bière 🍺

Articles Similaires