Why "just tag everything" fails
The standard advice for AWS cost attribution is to create a tag policy, activate cost allocation tags in the Billing Console, and enforce the policy via Service Control Policies. This advice is correct as a starting point and insufficient as a complete strategy for organizations with $1M+ in annual cloud spend and multiple engineering teams.
The problem is not that tagging is wrong. The problem is that tag coverage has a ceiling in practice, and that ceiling is lower than most organizations assume. Resources created through automated pipelines, CDK constructs, or third-party tooling often do not inherit the tags applied to their parent stack. Auto-scaling groups propagate instance tags, but the instances created by an ASG may not pick up all the custom tags applied after the ASG was created. AWS Lambda function tags do not propagate to the log groups or X-Ray traces that Lambda creates. Data Transfer costs in the CUR frequently have no resource ID at all, making them impossible to attribute by tag.
The result is a tagging approach that works well for a portion of spend and then hits a wall where 20–40% of costs remain unattributed — not because engineers are noncompliant, but because the underlying tagging model has structural gaps that tag enforcement cannot address.
The three-tier attribution model
A realistic attribution strategy for $1M+ AWS spend uses tags as the first signal and fills the gaps with two additional ownership signals: Terraform state and GitHub CODEOWNERS.
Tags are the highest-confidence signal when present. A resource with a well-formed team or cost-center tag is attributed directly. This tier handles the majority of resources for organizations with active tagging policies — typically 50–70% of spend in environments with 12+ months of tag policy enforcement.
Terraform state is the gap-filler for managed resources without tags. Every resource in a Terraform state file has a module path, a workspace, and a provider configuration. If the state file uses a backend that encodes team or environment context — an S3 key like infra/payments/prod/terraform.tfstate — the resource ARN can be mapped to the Payments team without relying on the tag on the resource itself. This picks up the CDK constructs, the auto-scaling instances, and the Lambda functions that were created before the tagging policy existed.
GitHub CODEOWNERS provides the final attribution layer for resources whose IaC is owned by a specific team. A repository path like infrastructure/services/data-pipeline with a CODEOWNERS entry pointing to the Data team maps every Terraform-declared resource in that path to Data. Combined with Terraform state coverage, this three-tier cascade can reach 90%+ attribution coverage for most multi-team AWS environments.
Which tags to activate as cost allocation tags
AWS allows up to 500 user-defined cost allocation tags, but activating too many creates noise in the CUR and makes queries harder to build. A practical minimum viable set for team-level attribution is three tags: team (the engineering squad or service domain), environment (prod / staging / dev), and service (the application or microservice name). With these three, most spending patterns become legible at a useful granularity.
Additional tags add value when the attribution question requires more specificity. A feature tag is useful for teams doing A/B testing with significant infrastructure cost differences between variants. A cost-center tag is useful when FinOps reporting needs to map to finance budget codes. But adding tags beyond a working set of three to five increases the burden on engineers creating resources and increases the chance of inconsistent values that break attribution queries.
Tag values need normalization. A tag where some resources have team = payments, others have team = payments-team, and others have team = PAYMENTS breaks grouping in the CUR. Before publishing a tagging policy, define the exact allowed values for each tag key and make those values available in a resource that engineers can reference without ambiguity — an internal wiki page, a Terraform variable set, a GitHub repository with allowed values documented in a README.
Enforcing tagging without blocking deployments
Service Control Policies that deny resource creation when required tags are absent are effective at preventing new untagged resources but require careful scoping. An SCP that is too broad will block IAM automation, CloudFormation service roles, and other AWS-managed resource creation that should not require custom tags. A common failure mode is an SCP that blocks CloudFormation stack creation because the service role used by CloudFormation does not pass the required tag condition.
A less disruptive enforcement approach is to use AWS Config rules to detect noncompliant resources and route compliance findings to the team Slack channel — a "detect and notify" model rather than a "block and prevent" model. This allows deployments to proceed while creating a feedback loop that drives tag coverage improvement over time without creating emergency rollback situations when an SCP misconfiguration blocks a production deployment.
For organizations early in their tagging journey, starting with detection rather than prevention is typically the right sequence. Build the detection, establish the baseline coverage, drive improvement over 2–3 quarters, and then consider SCP enforcement once tag coverage is consistently above 85% and teams have internalized the policy.
What to do with the unattributable spend
Even with a mature tagging strategy and Terraform state attribution, some spend will remain unattributable. Data Transfer charges, cross-region traffic, and costs from AWS-managed services that do not support resource-level tagging will appear in the CUR without a team attribution signal. Shared infrastructure — a transit VPC, a centralized logging account, a shared DNS zone — is genuinely multi-team spend that cannot be attributed to a single owner.
The correct approach is to handle this explicitly rather than to leave it as an "unattributed" bucket in cost reports. Shared service costs can be distributed to consuming teams proportionally — by request count, by data volume transferred, or simply by equal distribution across teams that use the service. The distribution methodology does not need to be perfectly accurate to be useful. A reasonable proportional allocation is more actionable than a large "shared / unattributed" line item that nobody takes responsibility for.