CostCenter and costcenter are two different tags, and the history never comes back
30 July 2026
There is a tool for this on the siteTwo teams tag their resources. One uses CostCenter. The other, following a
runbook someone typed from memory, uses costcenter.
Both are valid. Both are applied. Both appear in cost allocation as separate
dimensions. And every report grouped by CostCenter silently excludes
everything the second team owns.
Why it does not look like anything
The failure mode here is unusually quiet, and the reason is worth being precise about.
The grand total is correct. Every dollar is accounted for; it is just distributed across two columns instead of one. There is no discrepancy to notice, no reconciliation that fails, no alert, and no sum that does not add up.
A cost report showing CostCenter: platform at $40,000 looks exactly like a
cost report showing CostCenter: platform at $40,000, whether or not another
$12,000 sits under costcenter: platform a few rows down. If the second key is
not in your chosen grouping, its rows are simply not there.
It surfaces months later, usually as a team saying their spend does not match what they know they are running. By then the question is not how to fix the tag.
The history is the expensive part
Cost allocation data is not restated. Activating a tag key applies from that point forward; costs already incurred are not broken down retroactively.
So a split that ran for six months stays split in the history forever. You can retag every resource today and the reports for those six months remain unusable, which means any year-over-year comparison crossing the boundary is comparing two different measurements.
This is why the fix is worth doing before the tags are in use, and why “we will clean it up later” is a different kind of promise from usual. Later does not recover the data.
Applying a tag is not activating it
A separate failure with the same silence, and the two frequently arrive together.
A perfectly tagged estate produces nothing in cost reports until each key is activated as a cost allocation tag. That is a separate action, in a different console, in the management account, by someone who may not be the person who wrote the policy.
The tags are on the resources. The policy is enforced. The compliance report is green. And Cost Explorer has no such dimension.
Since activation is not retroactive either, every day between rollout and activation is a day of history you cannot analyse. Activate the key when you define it, before the rollout, not after. The ordering of those two steps matters more than either one individually.
A policy without enforced_for prevents nothing
The third thing people configure and then misread:
{
"tags": {
"owner": { "tag_key": { "@@assign": "Owner" } }
}
}
This is a valid tag policy. It does not stop anybody creating an untagged resource.
Without enforced_for, a tag policy produces a compliance report:
non-compliant resources are created successfully and appear in a list
afterwards. That is a reasonable first step and it is not what most people
believe they just configured.
Two further details that catch people:
enforced_for constrains tag values, not tag presence. It prevents a
non-compliant value on the listed resource types. Without a tag_value
allow-list beside it there is nothing to be non-compliant with, so enforcement
has no effect at all.
It does not require the tag. A resource created with no Owner tag is not
violating a policy that governs what Owner may contain. Requiring presence is
a different mechanism: an SCP with a Null condition on the tag key, or an IaC
policy check at deploy time.
That distinction accounts for a lot of “we have tag policies and half the estate is untagged”.
Values are case-sensitive too
The same problem one level down, and it can be baked into the policy itself:
"tag_value": { "@@assign": ["prod", "Prod", "staging"] }
Permitting both capitalisations splits the dimension exactly as a key collision does, with the blessing of the allow-list that existed to prevent it.
Trailing whitespace does the same thing and is worse, because "prod " and
"prod" are visually identical in every console and every CSV export. Nobody
finds that by looking.
Pick one form. Lowercase is the usual choice, mostly because it is the only one nobody argues about.
The wrong instinct: fix it in the report
When the split is discovered, the tempting fix is a mapping in the BI layer:
alias costcenter to CostCenter and move on. It works, and it is a trap for
three reasons.
It has to be maintained forever, by whoever inherits the dashboard. It only fixes the reports that go through that layer, so Cost Explorer, budgets, anomaly detection and the raw CUR export all still show the split. And it removes the pressure to fix the underlying tags, so the estate keeps drifting.
Fix the tags. Use the report mapping as a stopgap during the migration, with a date on it.
What changed recently
Tag policies can now require tags on resource creation in more places than
they once could, but the belt-and-braces answer is still an SCP with a Null
condition, or policy-as-code in the pipeline. Catching it at terraform plan is
better than catching it at creation, which is better than catching it in a
report.
Data exports replaced the older cost and usage report format. If you have tooling reading the legacy CUR, the migration is also the natural moment to audit which tag columns you are actually consuming, because unused activated tags carry no cost but do add columns.
Resource Explorer and the tag editor make a bulk audit practical without writing scripts, which is worth knowing before anybody starts building one.
Fixing an estate that has already drifted
1. Find the collisions before deciding anything. Group every distinct tag key by its lowercase form. Any group with more than one member is a split. Do the same for values within a key.
2. Pick the canonical form and write it down somewhere that is not a runbook nobody reads. The policy document is the right place.
3. Activate the canonical key first, before retagging, so that the moment resources start carrying it the data begins accumulating.
4. Retag in bulk, then verify the old key is gone rather than assuming. Resource Explorer or the tag editor will do this without scripting for most resource types; the exceptions are usually in services with their own tagging APIs.
5. Add enforced_for with a tag_value allow-list only once the estate
complies. Enforcing before that blocks legitimate work and produces an
exception process that outlives the policy.
6. Add a presence check separately, in the pipeline or via SCP, because the tag policy will not do it.
When this does not matter much
A single-account estate with one team does not really have an attribution problem. The tags are still worth having for lifecycle and automation, and the case-collision risk is proportionally smaller because fewer hands are creating resources.
If you allocate cost by account rather than by tag, which is a legitimate and much simpler model, the entire tag-splitting problem does not arise. Account boundaries cannot be misspelled. The trade-off is granularity and the overhead of managing many accounts.
If nothing consumes the tag, an inconsistent one is untidy rather than expensive. Worth checking before a large retagging project: some estates carry tags that were required by a policy nobody remembers writing and that no report has ever grouped by.
The short version
- Tag keys and values are case-sensitive.
CostCenterandcostcenterare two tags. - The grand total stays correct, so nothing looks wrong. That is what makes it survive.
- Cost allocation data is not restated. A split period stays split forever.
- Activate the key before rolling the tag out. Activation is not retroactive either.
enforced_forconstrains values, not presence, and does nothing without atag_valuelist.- Requiring a tag to exist needs an SCP or a pipeline check, not a tag policy.
- Fix the tags, not the dashboard.
Paste a policy or a plain Key=Value list into the
cloud tagging policy validator and it
reports case collisions across rules, case variants inside a single allow-list,
whitespace you cannot see, reserved aws: prefixes, and rules defined without
enforcement.