<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Tagging on PHP Boy Scout</title><link>https://blog-570662.gitlab.io/tags/tagging/</link><description>Recent content in Tagging on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Fri, 08 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog-570662.gitlab.io/tags/tagging/index.xml" rel="self" type="application/rss+xml"/><item><title>Two layers of tags, and which one wins</title><link>https://blog-570662.gitlab.io/two-layers-of-tags/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/two-layers-of-tags/</guid><description>&lt;img src="https://blog-570662.gitlab.io/two-layers-of-tags/cover-two-layers-of-tags.png" alt="Featured image of post Two layers of tags, and which one wins" /&gt;&lt;p&gt;Tagging cloud resources is one of those jobs that&amp;rsquo;s trivial to do badly and surprisingly fiddly to do well. Everyone agrees resources should be tagged. The argument nobody quite has out loud is &lt;em&gt;where the tags should come from&lt;/em&gt;, and getting that wrong gives you either a giant copy-pasted tag block on every resource, or a set of tags that quietly disagree with each other across the account.&lt;/p&gt;
&lt;h2 id="tags-answer-two-different-questions"&gt;Tags answer two different questions
&lt;/h2&gt;&lt;p&gt;If you look at what tags are actually &lt;em&gt;for&lt;/em&gt;, they split cleanly into two kinds, and the split is the whole point.&lt;/p&gt;
&lt;p&gt;Some tags are true of every single resource in the account, identically. The environment it belongs to. The fact that OpenTofu manages it. The project or owner it rolls up to for cost reporting. These are invariants: a resource that didn&amp;rsquo;t carry them would be the bug.&lt;/p&gt;
&lt;p&gt;Other tags are specific to a particular piece of infrastructure. Which component this resource belongs to, what subsystem it&amp;rsquo;s part of. The CloudTrail bucket is part of audit logging; the Config recorder is part of &lt;code&gt;aws-config&lt;/code&gt;. That&amp;rsquo;s a fact about the module, not about the account.&lt;/p&gt;
&lt;p&gt;Treat those two kinds the same and you end up repeating the invariants by hand on every resource, which is exactly the copy-paste that drifts. So the &lt;code&gt;infra&lt;/code&gt; setup gives each kind its own home.&lt;/p&gt;
&lt;h2 id="layer-one-declared-once-on-the-provider"&gt;Layer one: declared once, on the provider
&lt;/h2&gt;&lt;p&gt;The invariants live on the AWS provider itself, as &lt;code&gt;default_tags&lt;/code&gt;, set one time in the provider block:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-hcl" data-lang="hcl"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;aws&amp;#34;&lt;/span&gt; {&lt;span class="c1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt; # ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;default_tags&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; {&lt;span class="c1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt; # Environment, project, managed-by: the things true of
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt; # every resource in this account.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;default_tags&lt;/code&gt; applies those tags to every taggable resource the provider creates, automatically, without a single resource having to mention them. Change the environment label once, here, and it propagates to everything on the next apply. No resource carries a copy; they all inherit the originals. The invariants are stated exactly once, in the one place that&amp;rsquo;s true for all of them.&lt;/p&gt;
&lt;h2 id="layer-two-merged-in-by-the-module"&gt;Layer two: merged in by the module
&lt;/h2&gt;&lt;p&gt;The resource-specific tags live where the resource does: inside the module. Each module merges its own component tag over whatever tags it was handed, which you can see in the public &lt;a class="link" href="https://gitlab.com/phpboyscout/terraform-aws-security-baseline/-/blob/v0.2.0/modules/aws-config/main.tf#L10" target="_blank" rel="noopener"
 &gt;&lt;code&gt;terraform-aws-security-baseline&lt;/code&gt;&lt;/a&gt; modules:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-hcl" data-lang="hcl"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt; merge({ Component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;aws-config&amp;#34;&lt;/span&gt; }&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So the &lt;code&gt;aws-config&lt;/code&gt; module stamps &lt;code&gt;Component = &amp;quot;aws-config&amp;quot;&lt;/code&gt; onto the things it builds, the &lt;code&gt;account-hardening&lt;/code&gt; module stamps its own, and so on. The caller can pass extra tags down through &lt;code&gt;var.tags&lt;/code&gt;, and because they come last in the &lt;code&gt;merge&lt;/code&gt;, the caller can override the module&amp;rsquo;s defaults when it genuinely needs to. Module-specific knowledge stays in the module; per-call adjustments stay with the caller.&lt;/p&gt;
&lt;h2 id="which-layer-wins"&gt;Which layer wins
&lt;/h2&gt;&lt;p&gt;Now the question that actually bites: a resource is getting tags from the provider&amp;rsquo;s &lt;code&gt;default_tags&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; from the module&amp;rsquo;s &lt;code&gt;merge&lt;/code&gt;. What happens when both set the same key?&lt;/p&gt;
&lt;p&gt;The resource-level tags win. AWS&amp;rsquo;s provider treats tags set directly on a resource as an override of &lt;code&gt;default_tags&lt;/code&gt; on a key collision, so the module&amp;rsquo;s merged tags take precedence over the account-wide defaults. That&amp;rsquo;s the right way round: the invariants are a sensible baseline, and a module that has a specific reason to set a key differently can, without having to reach up and edit the provider block that everything else depends on. Most of the time the two layers are simply disjoint, the invariants saying &lt;em&gt;what account this is&lt;/em&gt; and the module tags saying &lt;em&gt;what this resource is for&lt;/em&gt;, and they never collide at all. When they do, local intent beats the global default, which is the precedence you&amp;rsquo;d want.&lt;/p&gt;
&lt;h2 id="why-bother-splitting-it"&gt;Why bother splitting it
&lt;/h2&gt;&lt;p&gt;The payoff is that neither layer has to know about the other. The provider declares the invariants once and never thinks about components. Each module declares its component and never hard-codes the environment. Add a new module and it inherits every account-wide tag for free, while contributing its own. Change an account-wide tag and you touch one block, not two hundred resources. The tags stay consistent not because someone&amp;rsquo;s policing them, but because the place each tag is declared is the one place it &lt;em&gt;can&lt;/em&gt; be declared.&lt;/p&gt;
&lt;h2 id="the-short-version"&gt;The short version
&lt;/h2&gt;&lt;p&gt;Resource tags answer two questions, and they want two homes. Account-wide invariants (environment, ownership, managed-by) go on the provider&amp;rsquo;s &lt;code&gt;default_tags&lt;/code&gt;, declared once and inherited by everything. Resource-specific tags go in the module, via &lt;code&gt;merge({ Component = &amp;quot;...&amp;quot; }, var.tags)&lt;/code&gt;, so each module owns its own labels and the caller can still override. On a key conflict the resource-level tag wins, which means the module&amp;rsquo;s intent beats the account default exactly when it should. Two layers, each declared in the one place it belongs, and no copy-pasted tag block anywhere in sight.&lt;/p&gt;</description></item></channel></rss>