<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ci on Guillaume Delré</title><link>https://guillaumedelre.github.io/fr/tags/ci/</link><description>Recent content in Ci on Guillaume Delré</description><generator>Hugo</generator><language>fr-FR</language><lastBuildDate>Sat, 16 May 2026 10:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/fr/tags/ci/index.xml" rel="self" type="application/rss+xml"/><item><title>Quinze minutes avant le premier test</title><link>https://guillaumedelre.github.io/fr/2026/05/16/quinze-minutes-avant-le-premier-test/</link><pubDate>Sat, 16 May 2026 10:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2026/05/16/quinze-minutes-avant-le-premier-test/</guid><description>Part 5 of 8 in &amp;quot;Symfony vers le Cloud : Douze Facteurs, Treize Services&amp;quot;: Comment un pipeline CI qui provisionnait une VM Azure par run — sans RabbitMQ, MinIO ni Varnish — est devenu un pipeline qui assemble l&amp;#39;environnement de production depuis les mêmes images qu&amp;#39;il livre.</description><category>symfony-to-the-cloud</category><content:encoded><![CDATA[<p>Le pipeline avait deux stages qui n&rsquo;avaient rien à voir avec le code : <code>provision</code> et <code>deprovision</code>. Entre eux, dans l&rsquo;ordre : <code>phpunit</code>, <code>phpmetrics</code>, <code>behat</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">stages</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">build</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">provision</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">phpunit</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">phpmetrics</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">behat</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">deprovision</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">deploy</span>
</span></span></code></pre></div><p>Avant que la première assertion s&rsquo;exécute, quinze minutes s&rsquo;étaient écoulées. Terraform avait cloné un dépôt d&rsquo;infrastructure, s&rsquo;était authentifié sur Azure, avait appliqué une configuration de VM. Ansible s&rsquo;était connecté à la nouvelle VM, avait installé PHP, configuré l&rsquo;application, câblé une base de données et une instance Redis. Ensuite les tests tournaient. Ensuite Terraform détruisait ce qu&rsquo;Ansible avait construit.</p>
<p>Pour chaque pipeline. Depuis chaque branche. Pour chaque pull request, de l&rsquo;ouverture au merge.</p>
<h2 id="ce-que-ces-quinze-minutes-ne-contenaient-pas">Ce que ces quinze minutes ne contenaient pas</h2>
<p>Le stage <code>provision</code> mettait en place deux services : PostgreSQL et Redis. Trois services dont l&rsquo;application dépendait en production étaient absents : RabbitMQ, MinIO et Varnish.</p>
<p>RabbitMQ traitait tout le travail asynchrone — 56 consumers sur 14 microservices. MinIO gérait le stockage de médias. Varnish était devant le cache HTTP. En CI, aucun d&rsquo;eux n&rsquo;existait. Les tests qui couvraient les files de messages ou le stockage de fichiers avaient deux options : ignorer ces chemins, ou les laisser non testés jusqu&rsquo;au staging. Varnish est un cas à part : les tests tapent directement dans l&rsquo;application et contournent intentionnellement la couche de cache, son absence en CI est donc un choix délibéré plutôt qu&rsquo;un manque.</p>
<p>C&rsquo;est le problème que le <a href="https://12factor.net/dev-prod-parity" target="_blank" rel="noopener noreferrer">Facteur X</a>
 décrit comme l&rsquo;écart d&rsquo;environnement. L&rsquo;écart ici n&rsquo;était pas une question de configuration — il était structurel. La VM était construite par Ansible depuis un script dans un dépôt séparé. Ce n&rsquo;était pas une image de container. Elle n&rsquo;était pas versionnée aux côtés de l&rsquo;application. Si une branche modifiait la topologie de messages RabbitMQ, il n&rsquo;y avait aucun moyen de tester cette modification en CI. Le changement de topologie et le code qui en dépendait ne se rencontreraient qu&rsquo;en staging.</p>
<p>Le script de provisioning Ansible lui-même fait partie du problème :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">launch_vm</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">stage</span>: <span style="color:#ae81ff">provision</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">script</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">git clone git@gitlab.internal/infra/ci-vm.git</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">cd ci-vm</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">az login --service-principal -u $ARM_CLIENT_ID ...</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">terraform apply -var &#34;prefix=${CI_PIPELINE_ID}-vm&#34; ...</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">sleep 45</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">ansible-playbook behat/test-env.yml ...</span>
</span></span></code></pre></div><p>Le <code>sleep 45</code> est là parce qu&rsquo;Ansible a besoin que la VM finisse de booter avant de pouvoir s&rsquo;y connecter. Ce n&rsquo;est pas un oubli — c&rsquo;est le délai minimum qu&rsquo;une VM fraîchement provisionnée nécessite avant que SSH fonctionne. C&rsquo;est inscrit dans le processus.</p>
<h2 id="ce-qui-la-remplacé">Ce qui l&rsquo;a remplacé</h2>
<p>Le nouveau pipeline n&rsquo;a pas de stage <code>provision</code>. Il n&rsquo;a pas de stage <code>deprovision</code>. L&rsquo;environnement, ce sont les images, et les images existent avant que les tests commencent.</p>
<p>Chaque job de test déclare ses dépendances comme des services Docker :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">$REGISTRY_URL/platform/rabbitmq:$CI_COMMIT_REF_SLUG</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">alias</span>: <span style="color:#ae81ff">rabbitmq</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">$REGISTRY_URL/platform/minio:$CI_COMMIT_REF_SLUG</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">alias</span>: <span style="color:#ae81ff">minio</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">redis:7.4.1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">alias</span>: <span style="color:#ae81ff">redis</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">$ARTIFACTORY_URL/postgresql:13</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">alias</span>: <span style="color:#ae81ff">postgresql</span>
</span></span></code></pre></div><p>Les services démarrent en parallèle quand le job commence. Avant que le script de test tourne, un <code>before_script</code> attend qu&rsquo;ils soient tous prêts :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">before_script</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">$CI_PROJECT_DIR/dockerize</span>
</span></span><span style="display:flex;"><span>      -<span style="color:#ae81ff">wait tcp://postgresql:5432</span>
</span></span><span style="display:flex;"><span>      -<span style="color:#ae81ff">wait tcp://rabbitmq:5672</span>
</span></span><span style="display:flex;"><span>      -<span style="color:#ae81ff">wait tcp://minio:9000</span>
</span></span><span style="display:flex;"><span>      -<span style="color:#ae81ff">wait tcp://redis:6379</span>
</span></span><span style="display:flex;"><span>      -<span style="color:#ae81ff">timeout 120s</span>
</span></span></code></pre></div><p>Du démarrage du pipeline à la première assertion : quatre-vingt-dix secondes — en supposant que les images sont déjà dans le cache du runner ; un cold pull rallonge les choses, mais devient négligeable une fois que le pipeline a tourné une fois sur une branche donnée.</p>
<h2 id="ce-que-signifie-ci_commit_ref_slug">Ce que signifie <code>$CI_COMMIT_REF_SLUG</code></h2>
<p>Le timing est le résultat visible. Ce qui le produit est plus intéressant encore : les noms des images.</p>
<p><code>$REGISTRY_URL/platform/rabbitmq:$CI_COMMIT_REF_SLUG</code> n&rsquo;est pas l&rsquo;image officielle RabbitMQ de Docker Hub. C&rsquo;est une image construite par le même pipeline, depuis la même branche, au même commit que le code testé. L&rsquo;image RabbitMQ embarque la topologie : un <code>definitions.json</code> avec chaque exchange, chaque queue, chaque binding, chaque configuration de dead-letter — versionné dans git aux côtés de l&rsquo;application qui en dépend.</p>
<p>Si une branche modifie la topologie de messages, le pipeline CI construit une nouvelle image RabbitMQ qui inclut ces modifications, puis exécute les tests contre elle. Le changement de topologie et le code qui en dépend sont testés ensemble, au même commit, avant que quoi que ce soit n&rsquo;atteigne le staging.</p>
<p>La même logique s&rsquo;applique à MinIO, décrite dans le <a href="/2026/05/14/the-ghost-of-the-ci-runner/">premier article de cette série</a>
 : l&rsquo;image MinIO embarque des fixtures de test préchargées. L&rsquo;environnement CI n&rsquo;a pas besoin d&rsquo;une étape de setup pour peupler le stockage. L&rsquo;état est intégré à l&rsquo;artefact.</p>
<p>Le runner de tests lui-même suit le même pattern. Chaque job utilise une variante debug de l&rsquo;image applicative — construite depuis la même branche, au même commit — avec les dépendances de test incluses :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">image</span>: <span style="color:#ae81ff">$REGISTRY_URL/platform/$service:$CI_COMMIT_REF_SLUG-debug</span>
</span></span></code></pre></div><p>Tout l&rsquo;environnement s&rsquo;assemble depuis des artefacts construits au même point de l&rsquo;historique git.</p>
<h2 id="ce-que-ça-a-demandé-dabandonner">Ce que ça a demandé d&rsquo;abandonner</h2>
<p>Behat et la VM provisionnée étaient couplés. La suite de tests Behat tournait contre un serveur HTTP sur la VM ; supprimer la VM signifiait supprimer Behat.</p>
<p>Ça s&rsquo;est révélé moins bloquant que ça n&rsquo;en avait l&rsquo;air. La suite Behat vivait dans un dépôt séparé, nécessitait la VM pour tourner, et avait accumulé une charge de maintenance significative. PHPUnit, tournant dans le container applicatif avec les services Docker, couvrait les mêmes scénarios par un chemin plus direct : tests fonctionnels qui exercent la couche HTTP, tests unitaires pour les composants individuels, suites organisées par domaine fonctionnel et générées dynamiquement en jobs CI parallèles.</p>
<p>La couche BDD a disparu. La couverture de tests est restée — et pouvait désormais tourner contre les vrais services.</p>
<h2 id="le-facteur-x-appliqué">Le Facteur X, appliqué</h2>
<p>Le <a href="https://12factor.net/dev-prod-parity" target="_blank" rel="noopener noreferrer">Facteur X</a>
 se lit souvent comme &ldquo;utilise la même base de données en local qu&rsquo;en production.&rdquo; C&rsquo;est la version la plus simple. La version plus profonde concerne l&rsquo;écart entre ce qu&rsquo;on teste et ce qu&rsquo;on livre.</p>
<p>L&rsquo;écart dans l&rsquo;ancien pipeline était large : une VM configurée manuellement, privée de services clés, reconstruite de zéro à chaque run. L&rsquo;écart dans le nouveau pipeline est étroit : le CI assemble l&rsquo;environnement depuis les mêmes images que la production, construites au même commit que le code sous test.</p>
<p>Les quinze minutes de Terraform et Ansible n&rsquo;étaient pas seulement lentes. Elles construisaient quelque chose qui n&rsquo;était pas ce que la production faisait tourner, à chaque fois, avant que le moindre test puisse commencer. Les quatre-vingt-dix secondes de <code>docker pull</code> construisent exactement ce que la production fait tourner — et les tests qui suivent testent ça, pas une approximation.</p>
]]></content:encoded></item><item><title>Le fantôme du runner CI</title><link>https://guillaumedelre.github.io/fr/2026/05/14/le-fant%C3%B4me-du-runner-ci/</link><pubDate>Thu, 14 May 2026 10:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2026/05/14/le-fant%C3%B4me-du-runner-ci/</guid><description>Part 1 of 8 in &amp;quot;Symfony vers le Cloud : Douze Facteurs, Treize Services&amp;quot;: Comment un chemin pointant vers un runner CI dans la config de production a révélé une dette de stockage — et comment les lazy adapters Flysystem l&amp;#39;ont résolue.</description><category>symfony-to-the-cloud</category><content:encoded><![CDATA[<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">APP__COLD_STORAGE__FILESYSTEM_PATH=&#34;/home/jenkins-slave/share_media/media&#34;
APP__COLD_STORAGE__FILESYSTEM_PATH_CACHE=&#34;/home/jenkins-slave/share_media/media/cache&#34;
APP__COLD_STORAGE__RAW_IMAGE_PATH=&#34;/home/jenkins-slave/share_media/media_raw&#34;
APP__SHARE_STORAGE__FILESYSTEM_PATH=&#34;/home/jenkins-slave/share_storage&#34;
</code></pre><p>Ces lignes se trouvaient dans le <code>.env</code> de production du service media. Pas le staging. Pas un override local. La production, committée dans le dépôt, lue à chaque démarrage.</p>
<p>Les chemins se terminent là où on s&rsquo;y attendrait : <code>/media</code>, <code>/share_storage</code>. Ils commencent ailleurs : <code>/home/jenkins-slave</code>, le répertoire home d&rsquo;un runner CI issu d&rsquo;une ancienne installation Jenkins.</p>
<h2 id="comment-le-home-dun-runner-atterrit-dans-la-config-de-production">Comment le home d&rsquo;un runner atterrit dans la config de production</h2>
<p>La plateforme avait grandi depuis une seule machine. Un serveur faisait tout tourner — l&rsquo;application, le runner CI, la base de données, le stockage de fichiers. Les fichiers transitaient entre l&rsquo;app et le système CI via NFS : un répertoire monté sur le même hôte, accessible aux containers comme au runner.</p>
<p>Le chemin <code>/home/jenkins-slave/share_media</code> était là où le partage NFS atterrissait sur cette machine. Quand l&rsquo;équipe a migré vers Docker Compose, les containers ont hérité du montage NFS. Le chemin est entré dans le <code>.env</code> parce que l&rsquo;application devait savoir où trouver les fichiers. Personne ne l&rsquo;a changé parce que ça marchait. Le montage était toujours là. Le chemin était valide. L&rsquo;application démarrait. Les fichiers apparaissaient où ils devaient.</p>
<p>Trois ans plus tard, personne n&rsquo;y pensait plus du tout. C&rsquo;était juste comme ça que le chemin media était configuré.</p>
<h2 id="ce-que-kubectl-apply-a-trouvé">Ce que kubectl apply a trouvé</h2>
<p>Le premier <code>kubectl apply</code> du service media s&rsquo;est terminé avec un pod bloqué en CrashLoopBackOff. Le container démarrait. L&rsquo;entrypoint tournait. L&rsquo;application essayait d&rsquo;accéder à <code>/home/jenkins-slave/share_media/media</code>. Fichier ou répertoire inexistant. Pas de montage NFS. Pas de runner.</p>
<p>Le chemin ne documentait pas une décision de design. Il documentait la machine qui tournait par hasard au moment où le <code>.env</code> avait été écrit.</p>
<p>C&rsquo;est exactement le problème que le <a href="https://12factor.net/backing-services" target="_blank" rel="noopener noreferrer">Facteur IV</a>
 de l&rsquo;application twelve-factor décrit. Les backing services — stockage, files, bases de données — doivent être des ressources attachées, configurées via URL ou chaîne de connexion, interchangeables entre environnements sans toucher au code. Un chemin de fichier sur un hôte partagé n&rsquo;est pas un backing service. C&rsquo;est une hypothèse physique sur la machine. Quand la machine change, l&rsquo;hypothèse lâche.</p>
<h2 id="le-chemin-était-le-symptôme">Le chemin était le symptôme</h2>
<p>La première étape évidente était de supprimer la référence au runner :</p>
<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">APP__COLD_STORAGE__FILESYSTEM_PATH=&#34;/share_media/media&#34;
APP__SHARE_STORAGE__FILESYSTEM_PATH=&#34;/share_storage&#34;
</code></pre><p>Plus propre. Plus de références CI dans une config de production. Toujours incorrect. L&rsquo;application supposait encore un système de fichiers POSIX — soit un volume monté, soit un répertoire sur le nœud. Dans Kubernetes, un volume partagé entre plusieurs pods nécessite un PersistentVolumeClaim en mode <code>ReadWriteMany</code>. La plupart des fournisseurs de stockage ne le supportent pas. Ceux qui le font ont tendance à être lents et coûteux. Et même là où ça fonctionne, on a juste remplacé une hypothèse sur le système de fichiers par une autre.</p>
<p>Renommer le chemin gagnait du temps. Ça ne réglait pas le problème.</p>
<p>Le problème, c&rsquo;est qu&rsquo;environ douze téraoctets d&rsquo;images — originaux et déclinaisons pré-générées dans différents formats — pour plusieurs marques éditoriales — étaient traités comme un répertoire. Un répertoire ne se monte pas proprement sur plusieurs pods. Un backing service, si.</p>
<h2 id="flysystem-comme-forme-de-la-solution">Flysystem comme forme de la solution</h2>
<p>Le service media avait déjà Flysystem de configuré. Trois adaptateurs concrets — système de fichiers local, AWS S3, Azure Blob — et un adaptateur lazy par-dessus :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># config/packages/flysystem.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">flysystem</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">storages</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">media.storage.local</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">adapter</span>: <span style="color:#e6db74">&#39;local&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">options</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">directory</span>: <span style="color:#e6db74">&#34;/&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">media.storage.aws</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">adapter</span>: <span style="color:#e6db74">&#39;aws&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">options</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">client</span>: <span style="color:#e6db74">&#39;aws_client_service&#39;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">bucket</span>: <span style="color:#e6db74">&#39;media&#39;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">streamReads</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">media.storage</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">adapter</span>: <span style="color:#e6db74">&#39;lazy&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">options</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">source</span>: <span style="color:#e6db74">&#39;%env(APP__FLYSYSTEM_MEDIA_STORAGE)%&#39;</span>
</span></span></code></pre></div><p>Tout le code de l&rsquo;application dépend de <code>media.storage</code>. Il ne sait pas si les fichiers vivent sur le système de fichiers ou dans un bucket cloud. Une variable d&rsquo;environnement détermine quel backend est actif :</p>
<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">APP__FLYSYSTEM_MEDIA_STORAGE=media.storage.aws   # production
APP__FLYSYSTEM_MEDIA_STORAGE=media.storage.local  # fallback local toujours disponible
</code></pre><p>Le chemin est parti. L&rsquo;hypothèse sur le système de fichiers est partie. Ce qui reste, c&rsquo;est un nom de service — une ressource attachée au sens twelve-factor, configurable sans rebuilder l&rsquo;image.</p>
<p>Le même pattern s&rsquo;étend au cache de vignettes. <a href="https://github.com/liip/LiipImagineBundle" target="_blank" rel="noopener noreferrer">LiipImagine</a>
 génère des images redimensionnées à la demande ; les originaux et le cache généré passent par des adaptateurs Flysystem séparés :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">liip_imagine</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">loaders</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">default</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">flysystem</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">filesystem_service</span>: <span style="color:#e6db74">&#39;media.storage&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">default_cache</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">flysystem</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">filesystem_service</span>: <span style="color:#e6db74">&#39;media.cache.storage&#39;</span>
</span></span></code></pre></div><p>Deux variables d&rsquo;environnement, deux buckets. Toute la chaîne — recevoir l&rsquo;upload, stocker l&rsquo;original, générer la vignette, la mettre en cache — est portable vers le cloud sans toucher une ligne de PHP.</p>
<p>Ce que l&rsquo;article ne couvre pas, c&rsquo;est le déplacement des données. Le lazy adapter change une variable d&rsquo;environnement. Faire passer douze téraoctets d&rsquo;un montage NFS vers un bucket S3, c&rsquo;est un autre projet — une fenêtre de migration, une double-écriture pendant le cutover, une vérification qu&rsquo;il ne manque rien.</p>
<h2 id="ce-que-minio-rend-possible-en-ci">Ce que Minio rend possible en CI</h2>
<p>La production utilise S3. Le développement local utilise <a href="https://min.io/" target="_blank" rel="noopener noreferrer">Minio</a>
, un stockage objet compatible S3 qui tourne dans un container Docker. L&rsquo;adaptateur AWS parle à Minio en local et à S3 en production. L&rsquo;application ne voit pas la différence :</p>
<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv"># local/CI
APP__FLYSYSTEM_MEDIA_STORAGE=media.storage.aws
APP__MINIO_ENDPOINT=http://minio:9000
APP__MINIO_ACCESS_KEY=minioadmin
APP__MINIO_SECRET_KEY=minioadmin
</code></pre><p>Le même code, le même adaptateur, un endpoint différent. Pas de mock, pas de chemins de test spéciaux, pas de branches conditionnelles par environnement.</p>
<p>Mais la configuration CI va un cran plus loin. L&rsquo;image Minio utilisée dans le pipeline n&rsquo;est pas l&rsquo;image officielle upstream — c&rsquo;est une image custom buildée avec des fixtures de test préchargées :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">minio/minio:latest</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">COPY</span> tests/fixtures/ /fixtures_media/<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>Chaque run CI démarre avec une instance Minio qui contient déjà les données attendues par la suite de tests. Pas de script de setup, pas de commande de seed, pas d&rsquo;étape &ldquo;attendre le chargement des fixtures&rdquo; avant que les tests commencent. L&rsquo;état initial de l&rsquo;environnement de test fait partie de l&rsquo;artefact de build.</p>
<p>Le <a href="https://12factor.net/build-release-run" target="_blank" rel="noopener noreferrer">Facteur V</a>
 appliqué à l&rsquo;infrastructure de test : l&rsquo;état de l&rsquo;environnement est buildé, versionné, immuable. Le pipeline CI construit l&rsquo;image Minio depuis la même source et au même commit que l&rsquo;image applicative. Les fixtures de test et le code qui les exploite sont toujours synchronisés.</p>
<h2 id="le-compromis-s3-honnêtement">Le compromis S3, honnêtement</h2>
<p>S3 introduit un coût de latence que le stockage local n&rsquo;a pas. Les premières données d&rsquo;un fichier prennent 10 à 30 millisecondes à arriver depuis S3 — c&rsquo;est la latence first-byte documentée du service, pas une mesure sur ce trafic spécifique.</p>
<p>À 300 requêtes par seconde, le raisonnement pour accepter ce compromis était le suivant : la majorité des lectures touche des vignettes déjà générées dans le cache S3, pas les fichiers originaux. Une image fraîchement uploadée paie la pénalité du cold miss une fois, à la première demande de vignette. Tout ce qui suit est un cache hit. Savoir si la latence de queue sous charge réelle confirmait ce raisonnement nécessitait des tests de charge suivis séparément — la décision d&rsquo;architecture et la validation étaient découplées.</p>
<p>Le compromis a été accepté : comportement prévisible sur plusieurs pods, pas de problèmes d&rsquo;état partagé, une couche de stockage qui scale sans coordination. L&rsquo;histoire complète des mesures appartient au rapport de tests de performance, pas ici.</p>
<h2 id="le-fantôme-sen-va">Le fantôme s&rsquo;en va</h2>
<p>Le chemin <code>/home/jenkins-slave</code> n&rsquo;apparaît plus dans la configuration. Mais ce à quoi il pointait était un couplage qui précédait Docker, précédait les microservices, précédait n&rsquo;importe quelle conversation sur la migration cloud. Le runner CI et l&rsquo;application de production partageaient un système de fichiers parce qu&rsquo;ils vivaient sur la même machine. Personne ne l&rsquo;avait conçu comme ça. Ça s&rsquo;était accumulé.</p>
<p>Une erreur <code>kubectl apply</code> sur un chemin qui n&rsquo;aurait pas dû exister a forcé la question : pourquoi cette application suppose-t-elle qu&rsquo;un runner CI spécifique est présent sur l&rsquo;hôte ? La réponse était &ldquo;parce que ça a toujours été comme ça.&rdquo; Ce n&rsquo;est pas une raison. C&rsquo;est une histoire.</p>
<p>Renommer le chemin était un correctif en carton. L&rsquo;adaptateur lazy de Flysystem était la vraie réponse — pas parce qu&rsquo;il est plus élégant, mais parce qu&rsquo;il fait du backend de stockage une décision qui appartient à l&rsquo;environnement, pas à l&rsquo;application. Le container démarre, lit une variable, se connecte à ce qui est à l&rsquo;autre bout. Il ne sait pas si c&rsquo;est un bucket dans un datacenter ou un container sur un laptop.</p>
<p>Le répertoire home du runner a disparu de la config. Ce qui l&rsquo;a remplacé, c&rsquo;est un nom de service. C&rsquo;est la différence.</p>
]]></content:encoded></item></channel></rss>