Skip to main content
← Back to blog

Tag Detection Is Not Tagging Governance Until You Write It Back

Muskan Bandta
Muskan Bandta Engineer · Zop.Dev
9 min read
Tag Detection Is Not Tagging Governance Until You Write It Back

Tag Detection Is Not Tagging Governance Until You Write It Back

Most tagging tools are very good at telling you what is untagged. Point one at an AWS account with 20,000 resources and it will produce a list of every one missing owner, environment or cost-center, sorted, filtered, exportable, and completely inert.

ZopNight’s tag apply, which writes a derived tag back to the cloud, previously covered 21 AWS resource types. It now covers 36, a 71% increase. The fifteen added types were detection-only before: the platform could tell you a tag was missing and could not do anything about it.

The interesting engineering is not in the fifteen new API calls. It is in the four places where writing a tag looked trivial and would have tagged the wrong resource.

Detection Without a Write Path Moves the Work Rather Than Doing It

A detection-only tagging tool has a predictable arc. Week one, it finds 4,000 untagged resources and somebody is impressed. Week six, it finds 4,300, because the estate grew faster than anyone tagged it by hand. Nothing in the loop closes, and the list becomes a thing people stop opening.

The reason is arithmetic rather than discipline. Fixing a tag by hand means finding the resource in the console, checking who owns it, deciding what the value should be, and applying it. That is a few minutes each. Detection produces work at the rate the estate changes, and a console produces fixes at the rate a human can click, and those two rates are not close.

An in-product write path changes the unit of work from one resource to one policy. The reader approves a derivation rule, and every resource matching it is tagged. That is the difference between a tagging report and tag governance, and it is why the count of apply-capable types is a more honest measure of a tagging feature than the count of detected violations.

There is also a subtler cost to detection-only. A violation nobody can fix in-product trains people to filter it out of the view, and the filter stays on after the fix ships. CloudWatch is a good example of what that costs: it is the AWS metrics and logging service, and its log groups and alarms are ordinary taggable resources like any instance or volume, so a tag policy that cannot write to them leaves a permanent hole.

Fifteen More Types, Under One Invariant: Offer Equals Write Equals Verify

Service familyTypes added
Logs and alarmsCloudWatch log groups, CloudWatch alarms
Data and cacheElastiCache, ElastiCache nodes, EFS, OpenSearch, MSK, Redshift
Security and identityKMS keys, web application firewalls, ACM certificates
Networking and deliveryRoute 53 zones, API Gateway, API Gateway v2
Data processingGlue jobs

The rule governing what may appear on that list is worth naming. The apply invariant is that a type is offered for apply only if the platform can also write its tags and read them back afterwards through the ordinary discovery pass, with the read-back pinned by a test rather than assumed. Offer, write and verify move together or the type is not offered.

That invariant does most of the work in this feature, because it rules out the tempting middle state: a type where the write is implemented and the read-back is not. In that state the platform writes a tag, cannot see it on the next scan, and re-raises the same violation. The reader applies the fix again. The tag is now set twice, the violation is still on the list, and the only visible symptom is that a policy never converges.

Two Types Refuse to Guess Their Own ARN

Tagging an AWS resource means naming it by ARN, the Amazon Resource Name: a structured identifier carrying partition, service, region, account and a resource path, and the only thing the tagging API will accept. For most types the ARN can be assembled from what the discoverer already stored: partition, service, region, account and identifier. For two of the new types, it cannot.

MSK cluster ARNs end in an AWS-generated id that is not derivable from the cluster name. So the ARN is resolved by asking the account, through a kafka cluster listing with an exact name match. The exactness is the point: the API’s own cluster-name filter is a prefix match, so a lookup for a cluster named orders would also return orders-staging, and a tag intended for one would land on the other. A prefix match is a correct-looking implementation that is wrong only for estates with consistent naming conventions, which is to say the estates that invested in governance.

ElastiCache is ambiguous in a different way. The discoverer emits the same resource type string for a replication group and for a standalone cache cluster, and the two have different ARN forms. The stored type and identifier together do not say which one you have, so the resolver asks the account instead of picking. Against a live estate the only ElastiCache resource present was a standalone cluster, so hardcoding the replication-group form would have produced an ARN for a resource that does not exist.

Architecture diagram

Two guards sit underneath both cases. The ARN builder refuses these types outright, so a missing resolver can never quietly fall through to the generic template. And an empty resolved ARN is refused rather than sent to the tagging API, because the tagging API accepts a well-formed ARN that matches nothing and reports success. A guess is not caught by the cloud; it is confirmed by it.

Three Ways an ARN Template Silently Tags the Wrong Thing

Three apply failures already existed on types shipped earlier, and all three came from treating a stored row as if it were an ARN.

FailureWhat the row saidWhat the template produced
Route 53 zonesRegion is globalA call to a global tagging endpoint that does not exist, dying at account resolution with a DNS error before any write
ECR repositoriesIdentifier stored with a service and region prefixA repository path with the region embedded in it, naming a repository nobody has
Rollup rowsA same-type summary row alongside real resourcesA well-formed ARN for an aggregate that is not a resource

The Route 53 fix is a per-type mapping from the global region to us-east-1, and it was kept deliberately narrow rather than applied as a blanket rewrite. The returned region also feeds ARN resolution, so rewriting global everywhere would build well-formed ARNs pointing at the wrong region for types whose ARN embeds one. Peering resources keep failing loudly instead, which is the correct outcome for a type nobody has verified.

The ECR fix strips the prefix and cuts the identifier at the first slash rather than taking the last segment. That choice is load-bearing: ECR supports namespaced repositories, so taking the last segment of a deeply namespaced repository path would tag a repository named after the final component and leave the real one untouched.

Rollup rows are the quietest of the three. Five apply-capable types emit a same-type summary row, and several types now refuse theirs, backed by a general guard on the template path. That guard also closed a live case of 22 elastic IP rows and covers future types without anybody remembering to.

A Column Too Narrow for an Identifier Fails Before the Cloud API Is Called

One migration underneath this work widened a subject_id column from 64 characters to 255. The measurements from a live estate explain why it was not optional.

MeasurementValue
Resources in the estate19,803
Identifiers longer than 64 characters5,756
Longest identifier155 characters
Load balancers already failing290 of 290
ECS services already failing189 of 189

Every one of those was failing with a MySQL data-too-long error, before any AWS call was made. This failure mode is worth recognising because it is almost always misdiagnosed. The symptom is that tag apply does not work for a whole resource type, which looks exactly like a missing IAM permission, and the first four hours go into policy documents.

The widening is metadata-only in InnoDB and the composite index stays inside the row-size limit, so it applies without a rebuild. That matters less than the general lesson: a write path is only as wide as its narrowest identifier column, and the types with the longest identifiers are the ones with the most structure in their names, which tend to be the ones a governance policy cares about most.

The Write Path Needs a Permission You Have Not Granted Yet

Tag apply on the added types requires a second AWS policy to be attached. Without it, the applies are denied at the remediation action.

That is a real operational step and it is worth stating plainly rather than burying. A platform that reads your estate holds read permissions, and no amount of product work converts those into write permissions. Any tool that appears to gain a write capability without you granting one is either not writing what you think it is writing, or was holding more access than you believed from the start.

The honest version of this feature is therefore: fifteen more types can be fixed in-product, once you decide to let the platform write to them.

Tagged
Muskan Bandta

Muskan Bandta

Engineer · Zop.Dev

Muskan works on the platform-engineering side of Zop.Dev, focused on multi-cloud provisioning and the developer experience of shipping services across AWS, GCP, and Azure. She writes about IDP design, golden paths, and what production-grade defaults actually look like.

Stop watching the waste.
Start cutting it.

See. Find. Fix. Automatic.

Connect your first cloud account in under 5 minutes. See your first remediation in under 7. No credit card required.

CDCR connect detect classify remediate
full audit every action traceable
read-only default access
Multi-cloud automation· Production-ready in 30 min· SOC 2 · ISO 27001· 20–60% off the bill, first month· 4 platforms · 1 console· Multi-cloud automation· Production-ready in 30 min· SOC 2 · ISO 27001· 20–60% off the bill, first month· 4 platforms · 1 console·