Le blog

Des images Docker construites 6 fois plus rapidement pour vos projets Symfony et API Platform

Publié le 03 août 2023

Chez Les-Tilleuls.coop, nous nous efforçons constamment de réduire l'empreinte environnementale et les coûts d'hébergement des projets sur lesquels nous travaillons (Éco-conception, GreenOps, FinOps...). En général, nous nous concentrons sur l'optimisation du code et de l’infrastructure de production, mais les pipelines CI/CD utilisés pour construire et déployer les applications peuvent également consommer beaucoup de ressources physiques et financières. De plus, des feedback loops DevOps rapides améliorent les conditions de travail et l'efficacité des devs.

Nous sommes les créateurs d’API Platform et de Symfony Docker. Nos équipes dev et SRE ont travaillé conjointement pour améliorer des ressources consommées pour la construction, les tests et le déploiement des projets les utilisant... avec des résultats impressionnants ! Bien que nous ayons utilisé ces projets comme exemples dans cet article, les optimisations décrites peuvent être appliquées à n'importe quel projet écrit dans n'importe quel langage.

Symfony Docker est un outil permettant d’installer et exécuter des projets Symfony. Comme son nom l’indique, il utilise Docker sous le capot. Le projet offre une configuration complète et optimisée pour la création de nouveaux projets Symfony et leur exécution en local, dans les jobs de CI/CD et en production. Symfony Docker permet d'exécuter des projets PHP sans nécessiter de dépendances locales, à l'exception de Docker lui-même.

La distribution officielle d'API Platform utilise un dérivé de Symfony Docker qui fournit un service Next.js supplémentaire, ainsi qu'un chart Helm pour déployer les projets API Platform sur Kubernetes.

#

Stockage du cache Docker Compose dans GitHub Actions grâce à Bake

Symfony Docker et API Platform offrent tous deux une intégration native avec GitHub. Les nouveaux projets peuvent être créés à partir de templates de dépôts, et un workflow GitHub Actions qui construit et teste les applications est inclus dans les installations par défaut.

Ce workflow GHA utilise Docker Compose pour construire les images, démarrer le projet et lancer les tests et les linters dans les conteneurs. 

L'utilisation de Docker Compose dans les workflow GHA est pratique et simple, mais cela engendre un problème majeur de performance : le cache du build n'est pas conservé entre les exécutions ! En effet, ni Docker ni GitHub ne fournissent d'actions officielles pour stocker les couches de cache Docker créées par Docker Compose dans le cache GHA. Cependant, il est possible de stocker le cache dans un registre Docker distant, mais cela rend les choses plus complexes, et cette stratégie n'est pas aussi rapide que l'utilisation du cache GHA local.

BuildKit et Buildx ont un support bêta pour exporter les couches de cache vers le cache de GitHub Actions, mais cette fonctionnalité n'est pas exposée via l'interface de ligne de commande Docker Compose (pour le moment ?).

Cependant, Docker a récemment lancé Bake, un outil expérimental de haut niveau qui tire parti de BuildKit et Buildx pour construire des projets Docker aussi rapidement et commodément que possible, et Bake expose cette fonctionnalité ! Bake dispose également d'un GitHub Actions officiel, maintenu par Docker Inc.

Bake est livré avec son propre format de configuration basé sur le HashiCorp Configuration Language (HCL). Attendez-vous à voir des articles de blog futurs expliquant comment libérer la puissance de Bake avec HCL. Suivez-nous sur Mastodon ou Twitter pour être informé(e) ! Par chance, Bake prend également en charge la spécification Compose ! Cela signifie qu'il devrait être possible d'utiliser l'action Bake pour construire des projets API Platform et Symfony, et de réutiliser les couches mises en cache d'un build à l'autre. Et en effet, après quelques améliorations des fichiers Docker Compose, ce workflow GitHub fonctionne comme prévu :

# .github/workflows/ci.yml

on: [push]
jobs:
  tests:
    steps:
      -
        name: Checkout
        uses: actions/checkout@v3
      -
        name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      -
        name: Build Docker images
        uses: docker/bake-action@v3
        with:
          load: true
          files: |
            docker-compose.yml
            docker-compose.override.yml
          set: |
            *.cache-from=type=gha,scope=${{github.ref}}
            *.cache-from=type=gha,scope=refs/heads/main
            *.cache-to=type=gha,scope=${{github.ref}},mode=max
      -
        name: Start services
        run: docker compose up --wait --no-build

      # Run your tests and linters

Après avoir obtenu le code et installé une version récente de Buildx, il construit les images à l'aide de Bake. 

Ces inputs de l’action Bake sont utilisées pour atteindre notre objectif : 

  • load indique à Bake d'exporter les images résultantes vers le client Docker local, afin qu'elles puissent être réutilisées par Docker Compose.
  • files définit la liste des fichiers Docker Compose à partir desquels les définitions d'images doivent être obtenues. docker-compose.override.yml doit figurer dans la liste car il contient les définitions des images de développement dont nous avons besoin pour exécuter les tests.
  • Les options cache-from et cache-to indiquent à Bake d'utiliser GitHub Actions pour stocker et récupérer les couches mises en cache. Le cache de la branche main  est toujours utilisé mais le cache des autres branches (feature) est cloisonné pour éviter la pollution du cache. Le paramètre mode=max indique à Bake de stocker toutes les couches dans le cache GHA (par défaut, seules les images finales sont stockées).
#

Réduction des temps de build Docker

Avant les optimisations décrites dans cet article, l'exécution du workflow GitHub Actions standard sur un projet API Platform vide prenait environ 5 minutes et 30 secondes. La plupart du temps (~5mn10) était consacré à la construction des images Docker utilisées pour exécuter les tests et divers linters. La mise en cache des couches de build a considérablement amélioré la situation. Malheureusement, parfois le cache expire. 

De plus, même avec un cache chaud, certains builds étaient toujours refaits. Enfin, certaines personnes utilisent des systèmes d'intégration continue autres que GitHub Actions, et avoir des constructions rapides pour tout le monde, y compris les utilisatrices et utilisateurs installant API Platform ou Symfony localement pour la première fois, serait encore plus intéressant.

#
Optimisation des couches de cache grâce aux builds multi-étapes

Depuis un an maintenant, Symfony Docker et API Platform utilisent des builds Docker multi-étapes pour fournir des images différentes pour les environnements de développement et de production. Par exemple, Xdebug, le célèbre débogueur PHP, est disponible et prêt à être utilisé en développement, mais il n'est pas inclus dans l'image de production.

En analysant les builds, nous avons découvert que nous pourrions également tirer parti des constructions multi-étapes pour éviter l'invalidation fréquente de nombreuses couches de cache Docker.

Chaque fois qu'une couche est modifiée, toutes les couches ultérieures sont jetées. Par conséquent, la copie du code source d'une application mettra à la poubelle toutes les couches ultérieures. Symfony Docker construisait l'image de développement PHP sur la base de l'image de production. C'était dommage car l'image de production contient évidemment le code source de l'application, donc toute modification de l'application nécessitait la reconstruction des couches utilisées pour le développement (comme l'installation de l'extension Xdebug).

Example of cache layers for a C program

Pire encore, l'image Caddy et l'image Next.js fournie par API Platform souffraient de problèmes similaires. Nous avons apporté deux modifications à ces images qui améliorent considérablement la dynamique du cache ainsi que les temps de construction, même lorsque le cache est froid : 

  • Nous avons introduit de nouvelles étapes « base » contenant toutes les couches partagées entre les images de production et de développement, mais pas le code source. Ces étapes de base sont héritées par deux images : une image de production (qui contient le code source) et une image de développement (auparavant, l'image de développement étendait l'image de production). Ce changement simple évite la nécessité de reconstruire les images de développement lorsque le code source change.
  • Côté développement, le code source est monté dans l'image sous forme de volumes (les volumes sont définis dans le fichier docker-compose.override.yml que nous fournissons). Les images de développement incluaient également le code source, mais c'était inutile en raison de ce volume. Le code source n'est plus copié dans les images de développement, ce qui accélère encore davantage les builds. Cette étape a également permis des simplifications et des optimisations spécifiques à la manière dont Symfony Docker installe initialement Symfony.
#
Téléchargement de Caddy à la place de sa construction

API Platform et Symfony Docker utilisent tous deux le serveur web Caddy (que nous affectionnons particulièrement et dont la version 2.7 vient de sortir) avec ses modules Mercure (temps réel) et Vulcain (API web pilotée par le client sans effort). Comme ces modules ne sont pas inclus dans l'image officielle de Caddy, nous utilisions l'image xcaddy pour construire une version de Caddy les contenant. Cette étape à elle seule prenait plus de 2 minutes !

xcaddy nécessite une chaîne de construction complète pour Go, de télécharger toutes les dépendances de Caddy, Mercure et Vulcain, et enfin, de compiler le binaire localement. Mais toutes ces étapes peuvent être évitées en utilisant l'API de téléchargement officielle de Caddy, qui vous permet de télécharger des binaires Caddy pré-compilés contenant des modules supplémentaires.

Notre Dockerfile pour Caddy ressemble désormais à ça : 

FROM caddy:2-alpine

ARG TARGETARCH

# Download Caddy compiled with the Mercure and Vulcain modules
ADD --chmod=500 https://caddyserver.com/api/download?os=linux&arch=$TARGETARCH&p=github.com/dunglas/mercure/caddy&p=github.com/dunglas/vulcain/caddy /usr/bin/caddy

Deux astuces à expliquer : 

  1. Nous utilisons l'instruction ADD pour remplacer le binaire Caddy dans l'image officielle par un binaire contenant les modules Mercure et Vulcain directement téléchargés à partir des serveurs du projet Caddy. Grâce à cette utilisation astucieuse d’ADD, nous n'avons pas besoin d'installer curl ni aucune autre commande supplémentaire.
  2. Caddy est écrit en Go, nous devons donc télécharger un binaire compilé pour l'architecture CPU que nous utilisons. Par exemple, sur Apple Silicon, nous avons besoin d'un binaire compilé pour l'architecture arm64. Par chance, Docker fournit l’argument TARGETARCH qui nous permet de détecter l’architecture de la plateforme cible de la transmettre à l'API Caddy (le nouveau backend BuildKit est nécessaire pour bénéficier de cet argument) !

Télécharger le binaire pré-compilé ne prend que 1,5 seconde.

#

De 6 minutes à 40 secondes

Le résultat cumulatif de toutes ces optimisations est impressionnant : avec un cache chaud, le workflow de test d'un projet vide utilisant Symfony Docker prend 50 secondes pour s'exécuter, alors qu'il en prenait presque 6 minutes auparavant ! Les résultats sont similaires pour API Platform : de 5 à 7 minutes à environ 1 minute.

#

Migration de vos projets existants

Si les nouveaux projets API Platform et Symfony Docker vont bénéficier  automatiquement de ces améliorations, ce ne sera pas le cas des projets existants.

Dockerfiles et les définitions Docker Compose font partie des squelettes des nouveaux projets, pas des bibliothèques vendor. Ils sont conçus pour être modifiés par les utilisateurs finaux. Vous êtes propriétaire de ces fichiers et vous devez les ajuster en fonction des besoins de vos projets. Cela signifie que pour bénéficier des modifications décrites dans cet article, vous devrez les rétroporter dans vos projets existants.

#

Faites appel à nos experts !

Si vous souhaitez accélérer vos applications et vos pipelines CI/CD, réduire votre impact écologique et/ou votre facture Cloud, n'hésitez pas à contacter notre équipe SRE, nous avons toutes les clés en main pour optimiser vos projets ! Vous pourrez nous retrouver sur divers événements cet automne : le 6 septembre à Lyon à l’occasion de Cloud Alpes, et le 12 octobre à Cloud Nord

Spoiler : ce sujet sera également au cœur de la keynote d'ouverture de Kévin Dunglas lors de l'API Platform Con 2023, prévue les 21 et 22 septembre (à Lille et en ligne). Il expliquera comment ces optimisations, combinées avec FrankenPHP, offrent des résultats encore meilleurs tout en simplifiant à la fois la compilation et le déploiement. Il n'est pas trop tard pour prendre votre place, rejoignez-nous !

Kevin Dunglas

Kevin Dunglas

CEO & technical director

Mots-clésAPI Platform, Docker, Performance, Symfony

Le blog

Pour aller plus loin