<?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>Microservices on Guillaume Delré</title><link>https://guillaumedelre.github.io/tags/microservices/</link><description>Recent content in Microservices on Guillaume Delré</description><generator>Hugo</generator><language>en</language><lastBuildDate>Fri, 15 May 2026 15:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/tags/microservices/index.xml" rel="self" type="application/rss+xml"/><item><title>The Host That Hid the Graph</title><link>https://guillaumedelre.github.io/2026/05/15/the-host-that-hid-the-graph/</link><pubDate>Fri, 15 May 2026 15:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2026/05/15/the-host-that-hid-the-graph/</guid><description>Part 4 of 8 in &amp;quot;Symfony to the Cloud: Twelve Factors, Thirteen Services&amp;quot;: Thirteen services sharing six identical gateway variables. The config looked simple. The dependency graph was invisible — until Kubernetes asked where each service actually lived.</description><category>symfony-to-the-cloud</category><content:encoded><![CDATA[<p>Every service in the platform had these six variables:</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-bash" data-lang="bash"><span style="display:flex;"><span>APP__GATEWAY__PRIVATE__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__GATEWAY__PRIVATE__PORT<span style="color:#f92672">=</span><span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span>APP__GATEWAY__PRIVATE__SCHEME<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http&#34;</span>
</span></span><span style="display:flex;"><span>APP__GATEWAY__PUBLIC__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__GATEWAY__PUBLIC__PORT<span style="color:#f92672">=</span><span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span>APP__GATEWAY__PUBLIC__SCHEME<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http&#34;</span>
</span></span></code></pre></div><p>Thirteen services, six variables each, one value. Reading any service&rsquo;s configuration, the architecture looked flat. Everything talked to the same host. That was the whole picture.</p>
<p>It wasn&rsquo;t.</p>
<h2 id="how-the-gateway-worked">How the gateway worked</h2>
<p>The gateway sat in front of every service and handled all inter-service traffic. A service calling the content API would construct a request to <code>http://platform.internal/content/api/</code> — the gateway received it, identified the target from the URL path, and forwarded it to the right backend. Every inter-service HTTP client in <code>framework.yaml</code> followed the same pattern:</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">content.client</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">base_uri</span>: <span style="color:#e6db74">&#34;%http_client.gateway.base_uri%/content/api/&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">headers</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">Host</span>: <span style="color:#e6db74">&#34;%env(APP__GATEWAY__PRIVATE__HOST)%&#34;</span>
</span></span></code></pre></div><p>The <code>http_client.gateway.base_uri</code> parameter was assembled from the GATEWAY vars. The gateway knew where each service ran. The services didn&rsquo;t need to know. From their perspective, everything was <code>platform.internal</code>.</p>
<p>This worked. For years, it worked well. Adding a service meant adding one DNS alias in the gateway config, not touching thirteen <code>.env</code> files. The gateway abstracted the topology. The services stayed decoupled from the infrastructure detail of who ran where.</p>
<h2 id="what-the-gateway-was-absorbing">What the gateway was absorbing</h2>
<p>The abstraction had a cost that didn&rsquo;t show up until you tried to read the system.</p>
<p>Looking at <code>content</code>&rsquo;s env file, you saw six gateway variables and nothing else about inter-service communication. To find out that <code>content</code> called <code>conversion</code>, <code>shorty</code>, and <code>media</code>, you had to read <code>framework.yaml</code>. To find out that <code>pilot</code> called ten external services, you had to trace through the HTTP clients one by one and count.</p>
<p>The number was ten. Authentication, bam, config, content, conversion, media, product, shorty, sitemap, social. Ten of the platform&rsquo;s thirteen services that <code>pilot</code> depended on at runtime, none of them visible from its configuration. Six variables said: talk to the gateway. They said nothing about the shape of what lay behind it.</p>
<p>That information existed — in the code, in the framework config, in the heads of the people who had built those integrations. It just didn&rsquo;t live anywhere you could read at a glance.</p>
<h2 id="what-kubernetes-made-explicit">What Kubernetes made explicit</h2>
<p>On-premise, the gateway was a single resolvable hostname. One DNS record, one set of variables, one place to update. Kubernetes doesn&rsquo;t work that way. Each service gets its own DNS name inside the cluster — <code>content.namespace.svc.cluster.local</code>, <code>conversion.namespace.svc.cluster.local</code>. Inter-service traffic goes directly, service to service, not through a shared gateway.</p>
<p>Moving to Kubernetes meant the gateway abstraction had to give way. Each service needed to know, concretely, where each of its dependencies lived. The six generic variables couldn&rsquo;t express that.</p>
<p>The refactor replaced them with per-target HOST variables — one per service dependency, named for the target:</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-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># content/.env — content calls these four services</span>
</span></span><span style="display:flex;"><span>APP__CONFIG__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__CONVERSION__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__MEDIA__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__SHORTY__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span></code></pre></div><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-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># pilot/.env — ten service dependencies</span>
</span></span><span style="display:flex;"><span>APP__AUTHENTICATION__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__BAM__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__CONFIG__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__CONTENT__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__CONVERSION__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__MEDIA__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__PRODUCT__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__SHORTY__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__SITEMAP__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__SOCIAL__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span></code></pre></div><p>Each HTTP client in <code>framework.yaml</code> got its own <code>base_uri</code> built from its target&rsquo;s HOST variable, and the <code>Host</code> header gave way to a <code>User-Agent</code> that identified the caller:</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">content.client</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">base_uri</span>: <span style="color:#e6db74">&#34;%env(APP__HTTP__SCHEME)%://%env(APP__CONTENT__HOST)%:%env(APP__HTTP__PORT)%/content/api/&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">headers</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">User-Agent</span>: <span style="color:#e6db74">&#34;Platform Content - %semver%&#34;</span>
</span></span></code></pre></div><p>The change isn&rsquo;t cosmetic. In the old setup, the explicit <code>Host</code> header ensured requests reached the correct gateway virtual host regardless of URL resolution. In the new setup, each client points directly at its target&rsquo;s DNS name — the right <code>Host</code> is derived from the <code>base_uri</code> automatically. The header slot doesn&rsquo;t go empty: <code>User-Agent</code> now identifies the calling service, which surfaces in logs and distributed traces without any additional instrumentation.</p>
<h2 id="the-discomfort-of-legibility">The discomfort of legibility</h2>
<p><code>pilot</code>&rsquo;s env file went from nine gateway variables to ten service-specific HOST variables. The file got longer. The architecture didn&rsquo;t get simpler — the ten dependencies were there before and they&rsquo;re still there now. What changed is that they&rsquo;re readable.</p>
<p><a href="https://12factor.net/config" target="_blank" rel="noopener noreferrer">Factor III</a>
 says to store configuration in the environment. The old approach satisfied that literally: six variables, all in env files, none hardcoded. But variables that collapse the entire dependency graph into a single opaque hostname aren&rsquo;t really configuration — they&rsquo;re a shorthand that trades legibility for convenience. Factor III doesn&rsquo;t ask only that configuration be externalized — it implicitly assumes the externalized configuration remains informative.</p>
<p>The refactor didn&rsquo;t simplify anything. It made the complexity visible. <code>pilot</code>&rsquo;s ten HOST variables document, in the <code>.env</code> file itself, the ten services it depends on. A new team member reading that file learns something real about the architecture. The old file taught them that there was a gateway.</p>
<p>There&rsquo;s a version of this story where you read the final state and conclude the team did unnecessary work — they replaced six variables with ten, all pointing at the same host anyway. In local development, <code>platform.internal</code> still resolves to the same place. The functional behavior didn&rsquo;t change.</p>
<p>The change is in what the configuration communicates. In Kubernetes, the HOST values diverge: each target gets its own cluster-internal DNS name, different per environment. The variables now carry real information. The refactor prepared the config to be honest about a topology it had been quietly simplifying for years.</p>
]]></content:encoded></item></channel></rss>