← Toutes les fiches

Fiche 18 Parcours : DevOps Lecture : 25 minutes

La culture DevOps et la livraison continue

Livrer souvent, par petits morceaux, avec des machines qui vérifient tout : ce n'est pas plus risqué, c'est le contraire.

Les fiches 11, 12 et 13 t'ont appris à déployer une application Symfony. Cette fiche prend du recul : comment une équipe organise-t-elle la livraison, quel que soit le langage ? Ce qu'elle présente vaut aussi bien pour une API en Go, un front en TypeScript ou une application mobile. Les exemples utilisent un projet Symfony parce que c'est ton terrain, mais seules quelques commandes changeraient pour un autre langage.

Aucun pipeline de cette fiche n'a été exécuté : ce sont des illustrations, relues et comparées à la documentation officielle. Seuls les deux exemples en PHP et en SQL ont tourné.

1.DevOps : une culture, pas un poste

Pendant longtemps, deux équipes se partageaient une application. Les développeurs étaient jugés sur les fonctionnalités livrées. L'exploitation (les « ops ») était jugée sur la stabilité de la production. Leurs objectifs s'opposaient : chaque livraison était un risque pour les uns et une victoire pour les autres. Le code passait « par-dessus le mur », avec un « ça marche sur ma machine », et les mises en production devenaient des soirées tendues, deux fois par an.

Le mot DevOps est né en 2009, autour de Patrick Debois et des premières conférences DevOpsDays. Il désigne une façon de travailler où la même équipe est responsable du code, de sa livraison et de son fonctionnement en production. Werner Vogels, d'Amazon, l'avait résumé dès 2006 : « You build it, you run it », tu le construis, tu le fais tourner.

  1. Une responsabilité partagée : un développeur qui reçoit les alertes de son propre code écrit des logs utiles et des migrations prudentes.
  2. L'automatisation : ce qui est répété (tests, construction, déploiement) est confié à des machines, qui ne se fatiguent pas et n'oublient pas d'étape.
  3. De petits lots : beaucoup de petites livraisons plutôt qu'une grosse. Chacune est facile à comprendre, à vérifier et à annuler (fiche 13).
  4. La mesure : on regarde ce qui se passe vraiment en production, plutôt que de supposer (section 7 et fiche 21).
Du mur entre deux équipes à une seule boucle En haut, avant DevOps : l'équipe de développement, qui veut livrer vite, envoie son code par-dessus un mur à l'équipe d'exploitation, qui veut que rien ne bouge. En bas, la boucle DevOps en huit étapes, portée par une seule équipe : planifier, coder, construire, tester, livrer, déployer, exploiter, surveiller, puis on recommence. Avant : un mur entre deux équipes Développement objectif : livrer vite le mur Exploitation objectif : que rien ne bouge « ça marche chez moi » DevOps : une seule boucle, et une équipe responsable de bout en bout Dev Ops 1. Planifier 2. Coder 3. Construire 4. Tester 5. Livrer 6. Déployer 7. Exploiter 8. Surveiller Ce qu'on apprend en surveillant la production nourrit la planification suivante.
Schéma 18.1 · Du mur entre deux équipes à une seule boucle
Et l'« ingénieur DevOps » ?

L'intitulé existe sur beaucoup d'offres d'emploi. Il désigne en général quelqu'un qui construit les outils de livraison et l'infrastructure. C'est utile, mais si cette personne devient la seule à savoir déployer, on a simplement recréé le mur avec un nouveau nom. Le but reste que chaque développeur puisse livrer et comprendre la production.

2.L'intégration continue

Deux développeurs travaillent trois semaines chacun sur leur branche. Le jour de la fusion, tout entre en conflit, et personne ne sait plus quelle combinaison a été testée. L'intégration continue (CI, Continuous Integration) prend le problème à l'envers : intégrer petit et souvent dans la branche principale, au moins une fois par jour, et vérifier automatiquement chaque changement.

Concrètement, chaque push déclenche un pipeline : une suite d'étapes exécutée sur une machine propre. Avec GitHub Actions :

exemples/18/ci.yml · illustration, non exécuté
# .github/workflows/ci.yml : à chaque push et à chaque pull request
name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read                  # lire le dépôt, rien de plus

concurrency:                      # un nouveau push annule l'exécution précédente
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  tests:
    runs-on: ubuntu-24.04
    timeout-minutes: 15
    services:
      postgres:
        image: postgres:17
        env:
          POSTGRES_USER: app
          POSTGRES_PASSWORD: app        # base jetable, détruite après le job
          POSTGRES_DB: app_test
        ports: ['5432:5432']
        options: >-
          --health-cmd pg_isready --health-interval 5s
          --health-timeout 5s --health-retries 10
    env:
      # Symfony ajoute « _test » au nom de la base en environnement de test
      DATABASE_URL: postgresql://app:app@127.0.0.1:5432/app?serverVersion=17
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          tools: composer
      - run: composer install --no-progress --prefer-dist
      - run: composer audit                      # failles connues des dépendances
      - run: vendor/bin/php-cs-fixer fix --dry-run --diff
      - run: vendor/bin/phpstan analyse --no-progress
      - run: bin/console doctrine:migrations:migrate --no-interaction --env=test
      - run: vendor/bin/phpunit
  1. Une machine neuve à chaque fois : rien ne dépend de ce qui traîne sur le poste d'un développeur. Si ça passe ici, ça passe partout.
  2. Du moins cher au plus cher : le style et l'audit échouent en quelques secondes, les tests prennent plus longtemps. On met en premier ce qui échoue vite.
  3. Une vraie base de données, dans un conteneur jetable, pour les tests d'intégration (fiche 3).
  4. Un pipeline rouge se répare en premier, avant toute nouvelle fonctionnalité. Un pipeline qu'on laisse rouge n'avertit plus personne.
  5. Un pipeline rapide : au-delà d'une dizaine de minutes, l'équipe commence à le contourner. Parallélise les jobs et mets en cache les dépendances.

Cette pratique va de pair avec des branches courtes : quelques heures ou quelques jours, pas des semaines (fiche 4). Poussée jusqu'au bout, c'est le trunk-based development : tout le monde intègre directement dans la branche principale, plusieurs fois par jour, et le code pas encore prêt est caché derrière un feature flag (section 5).

3.Livraison et déploiement continus

Une fois le code vérifié, il faut l'amener en production. Deux pratiques portent des noms proches, et la différence compte :

  1. La livraison continue (Continuous Delivery) : chaque changement fusionné est prêt à partir en production. Une personne décide du moment, d'un clic.
  2. Le déploiement continu (Continuous Deployment) : chaque changement qui passe toutes les étapes part en production automatiquement, sans clic.
Un pipeline, de la modification à la production Cinq étapes : un push ou une pull request, l'intégration (style, analyse, audit des dépendances, tests), la construction d'une image unique étiquetée par le commit, le déploiement automatique en recette avec un test de fumée, puis la production avec la même image. L'intégration continue couvre les deux premières étapes. La livraison continue s'arrête avant la production, qui attend un bouton. Le déploiement continu va jusqu'en production sans intervention. Push ou pull request Intégration style, analyse, audit, tests Image une seule fois app:3f9c2a1 Recette automatique, test de fumée Production même image un bouton ? Intégration continue : chaque push est vérifié Livraison continue : la production est à un bouton Déploiement continu : pas de bouton, tout part si tout est vert Un échec à n'importe quelle étape arrête tout : rien n'avance sans être vert.
Schéma 18.2 · Un pipeline, de la modification à la production

Voici un pipeline de livraison continue, avec GitHub Actions. Il reprend une règle de la fiche 13 : l'image est construite une seule fois, étiquetée par son commit, et c'est exactement elle qui part en recette puis en production.

exemples/18/cd.yml · illustration, non exécuté
# .github/workflows/deliver.yml : après chaque fusion dans main
name: Livraison

on:
  push:
    branches: [main]

permissions:
  contents: read
  packages: write                 # publier l'image dans le registre du dépôt

jobs:
  image:                          # construite UNE fois, étiquetée par le commit
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          push: true
          tags: ghcr.io/boutique/app:${{ github.sha }}

  staging:
    needs: image
    runs-on: ubuntu-24.04
    environment: staging          # ses propres secrets : ceux de la recette
    steps:
      - uses: actions/checkout@v4
      - run: ./deploy.sh staging ${{ github.sha }}
      - run: curl --fail --retry 5 --retry-delay 5 https://staging.example.com/health

  production:
    needs: staging
    runs-on: ubuntu-24.04
    environment: production       # règle de protection : une personne doit approuver
    steps:
      - uses: actions/checkout@v4
      - run: ./deploy.sh production ${{ github.sha }}
      - run: curl --fail --retry 5 --retry-delay 5 https://www.example.com/health
  1. Un environnement GitHub par cible (staging, production), chacun avec ses propres secrets. Une règle de protection peut exiger qu'une personne approuve avant la production. Sa disponibilité dépend de ton offre GitHub.
  2. Un test de fumée (smoke test) après chaque déploiement : une requête sur une route de santé qui vérifie que l'application démarre et joint sa base. S'il échoue, le pipeline s'arrête avant la production.
  3. Le script deploy.sh contient la bascule vue en fiche 13 (lien symbolique, ou mise à jour progressive des conteneurs). Le pipeline ne fait que l'appeler.

Les mêmes idées s'écrivent avec une autre syntaxe dans GitLab CI. Ici, le bouton de mise en production est la ligne when: manual :

exemples/18/gitlab-ci.yml · illustration, non exécuté
# .gitlab-ci.yml : le même pipeline, avec GitLab CI
stages: [test, build, deploy]

tests:
  stage: test
  image: registry.example.com/boutique/php-ci:8.4   # PHP, ses extensions et Composer
  services:
    - postgres:17
  variables:
    POSTGRES_USER: app
    POSTGRES_PASSWORD: app
    POSTGRES_DB: app_test
    DATABASE_URL: postgresql://app:app@postgres:5432/app?serverVersion=17
  script:
    - composer install --no-progress --prefer-dist
    - composer audit
    - vendor/bin/phpstan analyse --no-progress
    - bin/console doctrine:migrations:migrate --no-interaction --env=test
    - vendor/bin/phpunit

image:
  stage: build
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  script:
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"

deploy_staging:
  stage: deploy
  environment: staging
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  script:
    - ./deploy.sh staging "$CI_COMMIT_SHA"

deploy_production:
  stage: deploy
  needs: [deploy_staging]
  environment: production
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: manual              # un bouton : livraison continue, pas déploiement continu
  script:
    - ./deploy.sh production "$CI_COMMIT_SHA"
Livraison ou déploiement continu ?

La livraison continue est le bon objectif pour presque toutes les équipes : la production est toujours à un clic, et une personne garde la main sur le moment.

Le déploiement continu demande une grande confiance dans les tests, de la surveillance, des feature flags et un retour en arrière rapide. Sans eux, il envoie les bugs en production plus vite. C'est une destination, pas un point de départ.

4.Les environnements

Un environnement est un endroit où l'application tourne, avec sa configuration et ses données. La fiche 13 en a présenté trois. Le principe qui les relie : le même code partout, seule la configuration change.

  1. La parité : mêmes versions de PHP, de PostgreSQL, de Redis sur ton poste, en recette et en production. Docker Compose sert à ça sur le poste (fiche 12). Chaque différence est un bug qui n'apparaîtra qu'en production.
  2. La configuration dans l'environnement : des variables d'environnement, jamais des fichiers propres à chaque serveur ajoutés à la main.
  3. Des données de recette anonymisées : une copie de la production avec les vrais noms, e-mails et adresses de tes clients expose des données personnelles à toute l'équipe. Le RGPD l'interdit sans raison solide.
  4. Des environnements éphémères : certaines équipes créent un environnement temporaire par pull request, détruit à la fusion. Pratique pour faire valider une fonctionnalité, mais il faut une infrastructure qui le permette (fiche 20).
Une seule image, une configuration par environnement Une image Docker, étiquetée par son commit, est construite une seule fois par la CI, puis déployée telle quelle en recette et en production. Chaque environnement a sa propre configuration : URL de base de données, secrets et données (anonymisées en recette, réelles en production). Le poste du développeur utilise Docker Compose avec les mêmes versions de PHP et de PostgreSQL. Image app:3f9c2a1 construite une fois par la CI Ton poste APP_ENV=dev DATABASE_URL=…/app aucun vrai secret données de test Recette APP_ENV=prod DATABASE_URL=…/recette secrets de recette données anonymisées Production APP_ENV=prod DATABASE_URL=…/prod secrets de production données réelles Ton poste : Docker Compose, mêmes versions de PHP et de PostgreSQL, code monté en direct. Entre la recette et la production, le code ne change jamais : seule la configuration, lue dans l'environnement, est différente.
Schéma 18.3 · Une seule image, une configuration par environnement

5.Les feature flags : déployer n'est pas livrer

Un feature flag (interrupteur de fonctionnalité) est une condition dans le code qui active ou non une fonctionnalité, réglée sans redéployer. Il sépare deux moments qu'on confond souvent : déployer (le code est en production) et mettre en service (les utilisateurs s'en servent).

exemples/18/feature_flags.php
<?php

declare(strict_types=1);

// Des interrupteurs de fonctionnalités, avec une ouverture progressive
final class FeatureFlags
{
    /**
     * @param array<string, array{enabled: bool, percentage?: int, users?: string[]}> $flags
     */
    public function __construct(private array $flags)
    {
    }

    public function isEnabled(string $flag, string $userId): bool
    {
        $config = $this->flags[$flag] ?? null;
        if ($config === null || !$config['enabled']) {
            return false;                       // inconnu ou coupé : désactivé, par prudence
        }
        if (in_array($userId, $config['users'] ?? [], true)) {
            return true;                        // les testeurs désignés le voient toujours
        }
        // Le même utilisateur tombe toujours dans le même « seau », de 0 à 99
        $bucket = crc32("$flag:$userId") % 100;

        return $bucket < ($config['percentage'] ?? 100);
    }
}

// En vrai, cette configuration vient d'une base ou d'un service : on la change sans redéployer
$flags = new FeatureFlags([
    'new_checkout' => ['enabled' => true, 'percentage' => 20, 'users' => ['alice']],
    'pdf_invoices' => ['enabled' => false],    // coupé en urgence : un bug en production
]);

$enabled = 0;
for ($i = 1; $i <= 1000; $i++) {
    $enabled += $flags->isEnabled('new_checkout', "user-$i") ? 1 : 0;
}
echo "Nouveau tunnel de commande : $enabled utilisateurs sur 1000\n";

$show = fn (bool $enabled): string => $enabled ? 'activé' : 'désactivé';
$first = $flags->isEnabled('new_checkout', 'user-42');
$second = $flags->isEnabled('new_checkout', 'user-42');
echo "user-42, deux visites : {$show($first)} puis {$show($second)}\n";
echo 'alice, testeuse désignée : ' . $show($flags->isEnabled('new_checkout', 'alice')) . "\n";
echo 'Factures PDF : ' . $show($flags->isEnabled('pdf_invoices', 'alice')) . "\n";
echo 'Interrupteur inconnu : ' . $show($flags->isEnabled('dark_mode', 'alice')) . "\n";
Sortie
Nouveau tunnel de commande : 206 utilisateurs sur 1000
user-42, deux visites : désactivé puis désactivé
alice, testeuse désignée : activé
Factures PDF : désactivé
Interrupteur inconnu : désactivé
Déployer n'est pas mettre en service Un graphique de la part des utilisateurs qui voient une fonctionnalité. Le code est déployé une seule fois, interrupteur coupé. On l'ouvre aux testeurs, puis à 5 % des utilisateurs. Un bug apparaît : on coupe, sans redéployer. On rouvre à 5 %, puis 50 %, puis 100 %. 100 % 50 % 0 % déployé, coupé testeurs 5 % bug : coupé 5 % 50 % 100 % Part des utilisateurs qui voient la fonctionnalité un seul déploiement, au début La mise en service avance et recule sans redéployer : en cas de bug, on coupe en quelques secondes. Dernière étape, souvent oubliée : retirer l'interrupteur du code une fois à 100 %.
Schéma 18.4 · Déployer n'est pas mettre en service
  1. Le même utilisateur voit toujours la même chose : le seau est calculé à partir de son identifiant, pas tiré au hasard à chaque visite.
  2. Un interrupteur inconnu est coupé : une faute de frappe dans un nom ne doit pas mettre en service une fonctionnalité inachevée.
  3. Quatre usages courants : cacher du code pas fini (trunk-based), ouvrir progressivement, couper en urgence une fonctionnalité qui pose problème, comparer deux versions (test A/B).
  4. Des outils existent : Unleash est un service open source, et OpenFeature, un projet de la CNCF, propose une interface commune à plusieurs fournisseurs. Pour quelques interrupteurs, une table en base suffit.
Le coût des feature flags

Chaque interrupteur double les chemins possibles dans le code : deux interrupteurs, quatre combinaisons à tester. Un interrupteur oublié devient une dette, et des années plus tard, plus personne n'ose le retirer. Donne-lui une date de fin, et supprime-le du code dès qu'il est à 100 %. Pour une petite modification sans risque, il n'en faut pas : un déploiement suffit.

6.Les 12 facteurs

En 2011, des ingénieurs de Heroku, une plateforme d'hébergement, ont résumé en douze règles ce qui rend une application facile à déployer et à faire grandir : The Twelve-Factor App. Elles valent pour tous les langages, et tu en as déjà appliqué la plupart sans le savoir :

FacteurEn une phraseChez toi
1. Base de codeUn dépôt par application, déployé dans plusieurs environnementsUn dépôt Git
2. DépendancesDéclarées et isolées, jamais supposées présentescomposer.json et composer.lock
3. ConfigurationDans l'environnement, pas dans le codeVariables d'environnement, %env()%
4. Services externesBase, cache, e-mail : des ressources attachées par une URLDATABASE_URL, MAILER_DSN
5. Construire, livrer, exécuterTrois étapes séparées : on ne modifie pas le code en productionL'image construite une fois (section 3)
6. ProcessusSans état : rien d'important sur le disque localSessions et fichiers hors du serveur (fiche 19)
7. PortL'application s'expose elle-même sur un portMoins direct en PHP : nginx devant PHP-FPM, ou FrankenPHP
8. ConcurrenceGrandir en ajoutant des processus, pas en grossissant un seulPlus de conteneurs, plus de workers Messenger
9. JetableDémarrer vite, s'arrêter proprementUn worker qui finit son message avant de s'arrêter
10. ParitéLe développement ressemble à la productionDocker Compose (section 4)
11. LogsUn flux écrit sur la sortie standard, pas des fichiers à gérerMonolog vers php://stderr, que Docker récupère
12. Tâches d'administrationLancées comme des processus ponctuels, avec le même codeUne commande console dans le même conteneur

Ces règles ont été écrites pour une plateforme précise, en 2011, et certaines se discutent pour PHP, comme le facteur 7. Prends-les comme une liste de questions à se poser, pas comme un examen à réussir.

7.Mesurer : les indicateurs DORA

Comment savoir si une équipe livre bien ? Le programme de recherche DORA (DevOps Research and Assessment) a interrogé des dizaines de milliers de professionnels pendant des années. Ses résultats, publiés dans le livre Accelerate (2018) et dans un rapport annuel, retiennent quatre indicateurs, deux pour la vitesse et deux pour la stabilité : la fréquence de déploiement, le délai de mise en production, le taux d'échec des changements et le temps de rétablissement.

Ces chiffres se calculent simplement, à partir des déploiements que ton pipeline enregistre. Le SQL suffit :

exemples/18/dora.sql
-- Les déploiements en production de deux semaines, tels qu'un pipeline peut les enregistrer
CREATE TABLE deployment (
    id              INTEGER PRIMARY KEY,
    first_commit_at TEXT    NOT NULL,   -- premier commit du changement livré
    deployed_at     TEXT    NOT NULL,
    failed          INTEGER NOT NULL,   -- 1 si le déploiement a causé un incident
    restored_at     TEXT                -- retour à la normale, si échec
);

INSERT INTO deployment VALUES
    (1, '2026-10-19 09:10', '2026-10-19 16:40', 0, NULL),
    (2, '2026-10-20 10:00', '2026-10-21 11:15', 0, NULL),
    (3, '2026-10-21 14:30', '2026-10-22 10:05', 1, '2026-10-22 10:47'),
    (4, '2026-10-22 11:00', '2026-10-22 17:20', 0, NULL),
    (5, '2026-10-23 09:45', '2026-10-23 15:00', 0, NULL),
    (6, '2026-10-26 10:20', '2026-10-26 16:30', 0, NULL),
    (7, '2026-10-26 15:00', '2026-10-28 09:30', 0, NULL),
    (8, '2026-10-28 08:50', '2026-10-28 14:10', 1, '2026-10-28 15:55'),
    (9, '2026-10-29 13:15', '2026-10-29 17:45', 0, NULL),
    (10, '2026-10-30 09:00', '2026-10-30 11:40', 0, NULL);

-- 1. Fréquence de déploiement : combien par semaine ?
SELECT strftime('%W', deployed_at) AS week, COUNT(*) AS deployments
FROM deployment
GROUP BY week;

-- 2. Délai de mise en production : du premier commit à la production, en heures
SELECT ROUND(AVG((julianday(deployed_at) - julianday(first_commit_at)) * 24), 1)
       AS average_lead_time_hours
FROM deployment;

-- 3. Taux d'échec des changements
SELECT ROUND(100.0 * SUM(failed) / COUNT(*), 1) AS change_failure_rate_percent
FROM deployment;

-- 4. Temps de rétablissement après un échec, en minutes
SELECT ROUND(AVG((julianday(restored_at) - julianday(deployed_at)) * 24 * 60))
       AS average_recovery_minutes
FROM deployment
WHERE failed = 1;
Sortie
▸ 1. Fréquence de déploiement : combien par semaine ?
week | deployments
-----+------------
42   | 5
43   | 5
(2 lignes)

▸ 2. Délai de mise en production : du premier commit à la production, en heures
average_lead_time_hours
-----------------------
12.5
(1 ligne)

▸ 3. Taux d'échec des changements
change_failure_rate_percent
---------------------------
20
(1 ligne)

▸ 4. Temps de rétablissement après un échec, en minutes
average_recovery_minutes
------------------------
73
(1 ligne)
Les quatre indicateurs DORA : vitesse et stabilité Deux groupes d'indicateurs. Vitesse : la fréquence de déploiement (exemple : 5 par semaine) et le délai de mise en production, du premier commit à la production (exemple : 12,5 heures). Stabilité : le taux d'échec des changements (exemple : 20 %) et le temps de rétablissement après un incident (exemple : 73 minutes). Les meilleures équipes sont bonnes sur les quatre à la fois. Vitesse Fréquence de déploiement combien de fois en production ? exemple : 5 par semaine Délai de mise en production du premier commit à la production exemple : 12,5 h en moyenne Stabilité Taux d'échec des changements quelle part cause un incident ? exemple : 20 % Temps de rétablissement combien de temps pour réparer ? exemple : 73 min en moyenne Les rapports DORA observent que les équipes les plus performantes sont bonnes sur les quatre à la fois : livrer souvent, par petits morceaux, ne rend pas moins stable. Au contraire.
Schéma 18.5 · Les quatre indicateurs DORA : vitesse et stabilité
  1. Une moyenne cache les extrêmes : un seul changement resté une semaine en attente fausse tout. La médiane est plus juste, mais SQLite ne la calcule pas directement (PostgreSQL, si : percentile_cont(0.5)).
  2. Le résultat le plus important des rapports DORA : vitesse et stabilité ne s'opposent pas. Les équipes qui livrent le plus souvent sont aussi celles qui cassent le moins et réparent le plus vite, parce que chaque changement est petit.
  3. Les noms et les seuils évoluent d'un rapport à l'autre : le temps de rétablissement s'appelle désormais « temps de rétablissement après un déploiement en échec ». Consulte le site de DORA plutôt que des chiffres recopiés.
Mesurer une équipe, jamais une personne

Dès qu'un indicateur devient un objectif, il cesse d'être une bonne mesure : c'est la loi de Goodhart. Fixe un objectif de « 20 déploiements par semaine », et l'équipe découpera artificiellement ses changements. Utilise ces chiffres pour repérer ce qui ralentit l'équipe (une recette manuelle, des tests trop lents), pas pour comparer ou noter des personnes.

8.Sécuriser le pipeline

Un pipeline a accès au code, aux secrets de production et au droit de déployer. Pour un attaquant, c'est une cible plus intéressante que le serveur lui-même. En mars 2025, une action GitHub très utilisée, tj-actions/changed-files, a été modifiée par un attaquant pour afficher dans les logs les secrets des dépôts qui l'utilisaient. Des milliers de projets étaient concernés.

  1. Des droits minimaux : permissions: contents: read par défaut, et on n'ajoute que ce qu'un job précis demande, comme packages: write pour publier une image.
  2. Épingle les actions tierces sur l'identifiant complet d'un commit plutôt que sur une étiquette comme @v4 : une étiquette peut être déplacée vers un code malveillant, un identifiant de commit non. Dependabot ou Renovate proposent ensuite les mises à jour.
  3. Des secrets par environnement, jamais affichés. GitHub masque les secrets dans les logs, mais une simple transformation (encodage, découpage) contourne ce masque.
  4. Pas de clé de cloud permanente dans les secrets : avec OIDC, le pipeline obtient du fournisseur cloud un jeton valable quelques minutes (fiche 19).
  5. Méfie-toi des pull requests venues d'un fork : le déclencheur pull_request_target s'exécute avec les secrets du dépôt. Combiné avec la récupération du code de la pull request, il donne tes secrets à n'importe quel contributeur extérieur.
  6. Analyse ce que tu livres : composer audit pour les dépendances PHP, et un outil comme Trivy pour les failles connues de l'image Docker.

Pièges fréquents

PiègeCe qui arriveLa parade
Une seule personne sait déployerElle part en congés, plus rien ne sortUn pipeline que toute l'équipe sait lancer
Des branches qui vivent des semainesDes fusions douloureuses, des combinaisons jamais testéesDes branches courtes, et des feature flags
Un pipeline rouge qu'on ignorePlus personne ne remarque les vraies erreursUn pipeline rouge se répare en premier
Un pipeline de 40 minutesL'équipe le contourne ou attend sans rien faireParalléliser, mettre en cache, échouer vite
Reconstruire l'image pour la productionOn livre autre chose que ce qui a été vérifiéUne image par commit, promue d'un environnement à l'autre
Une recette différente de la productionLes bugs n'apparaissent qu'en productionMêmes versions, même image, seule la configuration change
Une copie de la production en recetteDes données personnelles exposées à toute l'équipeDes données anonymisées
Des feature flags jamais retirésDes chemins de code morts que personne n'ose toucherUne date de fin, et un ticket pour le retrait
Fixer un objectif sur un indicateur DORALes chiffres montent, la qualité nonDes indicateurs pour comprendre, pas pour noter
Des actions tierces non épingléesUne action compromise vole tes secretsUn identifiant de commit, et des droits minimaux

Checklist d'un pipeline

Exercice : la revue d'un pipeline

Un collègue propose ce fichier pour le dépôt de la boutique. Il « marche » : le site est bien déployé. Ne l'exécute pas : lis-le comme tu lirais une pull request.

exemples/18/exercice.yml · illustration, non exécuté
name: CI

on:
  pull_request_target:
  push:

permissions: write-all

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - uses: un-inconnu/setup-everything@main
      - run: curl -s https://example.com/install-tools.sh | bash
      - run: composer install
      - run: 'echo "Clé de déploiement : ${{ secrets.PROD_DEPLOY_KEY }}"'
      - run: ./deploy.sh production
        env:
          DEPLOY_KEY: ${{ secrets.PROD_DEPLOY_KEY }}
      - run: vendor/bin/phpunit || true
  1. Trouve au moins huit problèmes, de sécurité ou de fiabilité. Pour chacun, explique ce qui peut arriver concrètement.
  2. Classe-les : lesquels peuvent faire fuir des secrets ? Lesquels peuvent envoyer en production du code non testé ?
  3. Réécris le fichier en deux workflows, sur le modèle des sections 2 et 3 : un pour l'intégration continue, un pour la livraison.
  4. Rédige le commentaire de revue que tu laisserais à ton collègue. Il doit pouvoir corriger sans se sentir jugé (la fiche 15 reviendra sur la revue de code).
  5. Question de recul : l'équipe déploie aujourd'hui une fois par mois, un vendredi soir, à la main. Par quelle étape de cette fiche commencerais-tu pour changer ça, et pourquoi celle-là ?

Conseils

  1. Automatise ce qui fait mal en premier. Pas besoin de tout construire d'un coup : commence par l'étape manuelle qui cause le plus d'oublis ou de stress.
  2. Garde les scripts lisibles hors du pipeline. Un deploy.sh ou un Makefile se lance aussi sur ton poste. Le pipeline ne fait que l'appeler, et changer d'outil de CI devient facile.
  3. Lis les journaux de ton pipeline quand il est vert aussi : des avertissements, une étape qui ralentit d'un mois à l'autre, une dépendance qui ne se met plus en cache.
  4. DevOps, c'est d'abord des conversations. Avant d'écrire une ligne de YAML, demande à la personne qui déploie aujourd'hui ce qui lui fait peur. C'est souvent là qu'il faut commencer.

Documentation officielle