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 family | Types added |
|---|---|
| Logs and alarms | CloudWatch log groups, CloudWatch alarms |
| Data and cache | ElastiCache, ElastiCache nodes, EFS, OpenSearch, MSK, Redshift |
| Security and identity | KMS keys, web application firewalls, ACM certificates |
| Networking and delivery | Route 53 zones, API Gateway, API Gateway v2 |
| Data processing | Glue 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.
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.
| Failure | What the row said | What the template produced |
|---|---|---|
| Route 53 zones | Region is global | A call to a global tagging endpoint that does not exist, dying at account resolution with a DNS error before any write |
| ECR repositories | Identifier stored with a service and region prefix | A repository path with the region embedded in it, naming a repository nobody has |
| Rollup rows | A same-type summary row alongside real resources | A 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.
| Measurement | Value |
|---|---|
| Resources in the estate | 19,803 |
| Identifiers longer than 64 characters | 5,756 |
| Longest identifier | 155 characters |
| Load balancers already failing | 290 of 290 |
| ECS services already failing | 189 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.
