<?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>Gitlab on Guillaume Delré</title><link>https://guillaumedelre.github.io/tags/gitlab/</link><description>Recent content in Gitlab on Guillaume Delré</description><generator>Hugo</generator><language>en</language><lastBuildDate>Sat, 16 May 2026 10:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/tags/gitlab/index.xml" rel="self" type="application/rss+xml"/><item><title>Fifteen Minutes Before the First Test</title><link>https://guillaumedelre.github.io/2026/05/16/fifteen-minutes-before-the-first-test/</link><pubDate>Sat, 16 May 2026 10:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2026/05/16/fifteen-minutes-before-the-first-test/</guid><description>Part 5 of 8 in &amp;quot;Symfony to the Cloud: Twelve Factors, Thirteen Services&amp;quot;: How a CI pipeline that provisioned an Azure VM per run — missing RabbitMQ, MinIO, and Varnish — became one that assembles the production environment from the same images it ships.</description><category>symfony-to-the-cloud</category><content:encoded><![CDATA[<p>The pipeline had two stages that had nothing to do with code: <code>provision</code> and <code>deprovision</code>. Between them, in sequence, came <code>phpunit</code>, <code>phpmetrics</code>, and <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>Before the first assertion ran, fifteen minutes had passed. Terraform had cloned an infrastructure repository, authenticated to Azure, and applied a VM configuration. Ansible had connected to the new VM, installed PHP, configured the application, wired up a database and a Redis instance. Then the tests ran. Then Terraform destroyed what Ansible had built.</p>
<p>For every pipeline. From every branch. For every pull request, from open to merge.</p>
<h2 id="what-those-fifteen-minutes-were-missing">What those fifteen minutes were missing</h2>
<p>The <code>provision</code> stage set up two services: PostgreSQL and Redis. Three services that the application depended on in production were absent: RabbitMQ, MinIO, and Varnish.</p>
<p>RabbitMQ processed all asynchronous work — 56 consumers across 14 microservices. MinIO handled media storage. Varnish fronted the HTTP cache. In CI, none of them existed. Tests that exercised message queuing or file storage had two options: skip these paths, or leave them untested until staging. Varnish is a different case: tests hit the application directly and intentionally bypass the cache layer, so its absence in CI is a deliberate choice rather than a gap.</p>
<p>This is the problem <a href="https://12factor.net/dev-prod-parity" target="_blank" rel="noopener noreferrer">Factor X</a>
 describes as the environment gap. The gap here wasn&rsquo;t a matter of configuration — it was structural. The VM was built by Ansible from a script in a separate repository. It wasn&rsquo;t a container image. It wasn&rsquo;t versioned alongside the application. If a branch modified the RabbitMQ message topology, there was no way to test that modification in CI. The topology change and the code that relied on it would only meet in staging.</p>
<p>The Ansible provisioning script itself is part of the problem:</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>The <code>sleep 45</code> is there because Ansible needs the VM to finish booting before it can connect. It&rsquo;s not an oversight — it&rsquo;s the minimum time a freshly provisioned VM needs before SSH works. It&rsquo;s baked into the process.</p>
<h2 id="what-replaced-it">What replaced it</h2>
<p>The new pipeline has no <code>provision</code> stage. It has no <code>deprovision</code> stage. The environment is the images, and the images exist before the tests begin.</p>
<p>Each test job declares its dependencies as Docker services:</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>The services start in parallel when the job begins. Before the test script runs, a <code>before_script</code> waits for all of them to be ready:</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>From pipeline start to first assertion: ninety seconds — assuming images are already cached on the runner; a cold pull adds time, but becomes negligible once the pipeline has run once on a given branch.</p>
<h2 id="what-ci_commit_ref_slug-means">What <code>$CI_COMMIT_REF_SLUG</code> means</h2>
<p>The timing is the visible result. What produces it is more interesting: the image names.</p>
<p><code>$REGISTRY_URL/platform/rabbitmq:$CI_COMMIT_REF_SLUG</code> is not the official RabbitMQ image from Docker Hub. It&rsquo;s an image built by the same pipeline, from the same branch, at the same commit as the code being tested. The RabbitMQ image carries the topology: a <code>definitions.json</code> with every exchange, every queue, every binding, every dead-letter configuration — versioned in git alongside the application that depends on them.</p>
<p>If a branch modifies the messaging topology, the CI pipeline builds a new RabbitMQ image that includes those modifications, then runs the tests against it. The topology change and the code that relies on it are tested together, at the same commit, before anything reaches staging.</p>
<p>The same logic applies to MinIO, as described in the <a href="/2026/05/14/the-ghost-of-the-ci-runner/">first article in this series</a>
: the MinIO image carries preloaded test fixtures. The CI environment doesn&rsquo;t need a setup step to populate storage. The state is built in.</p>
<p>The test runner itself follows the same pattern. Each job uses a debug variant of the application image — built from the same branch, same commit — with the test dependencies included:</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>The whole environment assembles from artifacts built at the same point in the git history.</p>
<h2 id="what-this-required-dropping">What this required dropping</h2>
<p>Behat and the provisioned VM were coupled. The Behat test suite ran against an HTTP server on the VM; removing the VM meant removing Behat.</p>
<p>That turned out not to be the obstacle it looked like. The Behat suite lived in a separate repository, required the VM to run, and had accumulated significant maintenance overhead. PHPUnit, running inside the application container with Docker services, covered the same scenarios through a more direct path: functional tests exercising the HTTP layer, unit tests for individual components, suites organized per feature area and generated dynamically into parallel CI jobs.</p>
<p>The BDD layer went away. The test coverage stayed — and could now run against the actual services.</p>
<h2 id="factor-x-applied">Factor X, applied</h2>
<p><a href="https://12factor.net/dev-prod-parity" target="_blank" rel="noopener noreferrer">Factor X</a>
 is often read as &ldquo;use the same database locally as in production.&rdquo; That&rsquo;s the simplest version. The deeper version is about the gap between what you test and what you ship.</p>
<p>The gap in the old pipeline was wide: a manually configured VM, missing key services, rebuilt from scratch on every run. The gap in the new pipeline is narrow: the CI assembles the environment from the same images as production, built from the same commit as the code under test.</p>
<p>The fifteen minutes of Terraform and Ansible were not just slow. They were building something that wasn&rsquo;t what production ran, every time, before any test could begin. The ninety seconds of <code>docker pull</code> build exactly what production runs — and the tests that follow are testing that, not an approximation of it.</p>
]]></content:encoded></item></channel></rss>