Tag policies exist in every org. Clean, consistent tags exist in almost none of them. The wiki has a table. The platform team has a Confluence page. New accounts still launch with env, Env, environment, and a cost center that is someone's first name.
Three tags are the line between a tagging strategy and tagging theatre: Environment, Team, and CostCenter. If a resource does not carry those three, it does not get created. Everything else is optional. Project, application, data classification, and a ticket id are useful. They do not belong in the gate. The moment the required set grows past what finance and ownership need, people invent values to get a pipeline green, and Cost Explorer splits one team across five spellings.
What the three tags are for
Environment is a closed list: prod, staging, dev. It answers which spend is the cost of running the business and which spend is the cost of building it. Free text turns into Prod, production, and prd, and those are three different rows in a bill.
Team is the group that can explain the resource and gets paged for it. A person's name looks precise until they leave. The tag stays. A stable team name survives the reorg longer than an email alias, and it is the handle you use when a budget alert needs an owner.
CostCenter is the code finance already recognizes. It is the field that makes a slice of the AWS bill chargeable to a department. A project nickname in this field feels descriptive in the console and lands as unallocated spend in the monthly close.
Put the same capitalization on every key. CostCenter and costcenter are different tags. Cost Explorer will not merge them, and a tag policy that enforces case will treat them as a violation.
Tag policies describe the tags. They do not require them.
An AWS Organizations tag policy is the dictionary. It fixes the key spelling and the allowed values, and it can block a create or an update that supplies a noncompliant value on supported resource types.
It does not block a create that omits the key. Untagged resources stay invisible to tag-compliance reports, because a resource that was never tagged has nothing to evaluate. Account admins can still launch them. That is why a tag policy alone produces a clean standard and a dirty account.
A minimal policy for the environment key looks like this:
{
"tags": {
"Environment": {
"tag_key": { "@@assign": "Environment" },
"tag_value": { "@@assign": ["prod", "staging", "dev"] },
"enforced_for": {
"@@assign": ["ec2:instance", "ec2:volume", "rds:db", "s3:bucket"]
}
}
}
}
Repeat the pattern for Team and CostCenter, with the real team names and finance codes as the allowed values. Keep that list short enough that adding a team is a pull request, and long enough that people are not tempted to pick a neighbor's code to get past the check.
The SCP is the gate
The control that changes behavior is a service control policy that denies the create call when any required tag is absent. You catch it at provisioning time, while the author of the Terraform is still at the keyboard. Six weeks later, the person staring at a Cost Explorer chart cannot tell who owns $40k of EC2, and the instance has already been running.
aws:RequestTag is present only when the tag is passed on that API call. A Null condition set to "true" matches when the key is missing. Put each required tag in its own statement. Several keys inside one Null block are combined with AND, so the deny fires only when every listed tag is missing. One present tag would let the other two through.
Scope the resource. ec2:RunInstances authorizes the instance, the volume, the network interface, and the AMI in a single call. A deny on Resource: "*" rejects the request even when the instance is tagged, because the AMI in the same call has no Environment tag. Limit the statement to the resource types you actually require tags on.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyRunInstancesWithoutEnvironment",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": [
"arn:aws:ec2:*:*:instance/*",
"arn:aws:ec2:*:*:volume/*"
],
"Condition": {
"Null": { "aws:RequestTag/Environment": "true" }
}
},
{
"Sid": "DenyRunInstancesWithoutTeam",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": [
"arn:aws:ec2:*:*:instance/*",
"arn:aws:ec2:*:*:volume/*"
],
"Condition": {
"Null": { "aws:RequestTag/Team": "true" }
}
},
{
"Sid": "DenyRunInstancesWithoutCostCenter",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": [
"arn:aws:ec2:*:*:instance/*",
"arn:aws:ec2:*:*:volume/*"
],
"Condition": {
"Null": { "aws:RequestTag/CostCenter": "true" }
}
}
]
}
Repeat that shape for the create APIs your platform actually uses and that support tag-based conditions: RDS instance and cluster creation, Lambda function creation, S3 bucket creation, ECS service creation. Check the service's IAM authorization reference before you add an action. If the API does not support aws:RequestTag, the SCP looks strict and does nothing.
Then deny removal of the same keys, or the tag exists for the length of the apply and disappears in the next PR:
{
"Sid": "DenyRemovingRequiredTags",
"Effect": "Deny",
"Action": "ec2:DeleteTags",
"Resource": "*",
"Condition": {
"ForAnyValue:StringEquals": {
"aws:TagKeys": ["Environment", "Team", "CostCenter"]
}
}
}
Attach the SCP to the workload OUs. It does not apply to the management account, and it does not retag anything already running. The backlog is a separate pass. AWS Config's required-tags rule flags anything still missing a key, and the Tag Editor in AWS Resource Groups can list those resources so a team can fix them.
Make the pipeline the easy path
People route around a gate that their tooling cannot satisfy. In Terraform, set the three tags in the provider so every resource that supports tags inherits them, and set them again on the resource when the create API is what the SCP evaluates. default_tags are merged into the create call for many resources. Some resources still apply tags in a follow-up CreateTags call. Those creates fail the SCP, because the request the policy sees has no tags. When that happens, put the tags in the resource block itself.
provider "aws" {
default_tags {
tags = {
Environment = var.environment
Team = var.team
CostCenter = var.cost_center
}
}
}
CloudFormation stack-level tags propagate to supported resources at create time. CDK's Tags.of(app).add(...) does the same if it is applied before synthesis, on a scope the construct actually inherits. A pipeline role should pass the tags. Exempt a break-glass role if you must, log its use, and keep it out of the daily path. An exemption for "CI" becomes the way every untagged stack gets deployed.
Activate them, or Cost Explorer still cannot see them
Consistent tags on resources are still invisible in billing until the payer account activates them as cost allocation tags. User-defined tags show up in the billing console only after AWS has seen them on a resource, which can take up to 24 hours. Activate Environment, Team, and CostCenter there. Activation itself can take another 24 hours before Cost Explorer can group by them.
After that, Cost Explorer can group and filter by team, environment, and cost center. AWS Budgets can alert on a single CostCenter at a forecasted overrun, while the owner can still change the design, instead of at invoice time, when the spend is already committed. The same tags give Compute Optimizer a place to land: a rightsizing recommendation is a specific instance, and the instance's Team and CostCenter say whose workload is oversized and whose budget the savings return to.
That is the loop. The tag policy defines the structure. The SCP enforces the gate. Cost allocation tags, Cost Explorer, and Budgets make the data something finance and engineering can both act on.
Ship the tag policy and the SCP to a sandbox OU first. Watch which pipelines fail, fix the ones that tag after create, then move the policy to production OUs. A tagging standard that is enforced becomes boring within a quarter. A tagging standard that lives in a wiki stays a suggestion, and the $40k chart stays unassigned.