InsightsCloud
Cloud cost allocation and showback that teams trust
How to attribute cloud spend to the teams that cause it, with a small tagging standard that is enforced, account-per-team boundaries, activated cost allocation tags and a fair split of shared Kubernetes clusters.
Published April 29, 20256 min read
The cloud bill arrives as one number for the whole organization. Finance wants to know which product it belongs to, engineering managers want to know whether their team's spend is reasonable, and nobody can answer either question because a third of the resources have no owner recorded anywhere.
Showback is the practice of attributing costs to the teams that incur them and showing each team its share, without necessarily moving money between budgets. Chargeback, where the cost actually lands on the team's budget, is a later and optional step. Showback alone changes behavior, because teams start to see the cost of the idle cluster or the forgotten snapshot as their own.
It only works if the numbers are trusted. A team that finds a stranger's database in its report will stop reading it.
Account boundaries first, tags second
Tags are the obvious tool, but they have gaps. Data transfer, support charges, some managed service fees and resources created by other services are hard or impossible to tag. Tags are also only as good as the discipline of whoever created the resource.
Account boundaries have no gaps. Everything in an AWS account, an Azure subscription or a Google Cloud project belongs to whoever owns that container, including the line items nobody can tag. So the most reliable allocation design is:
- One account per team per environment, as a landing zone with AWS Organizations, Azure management groups or Google Cloud folders makes cheap.
- Owner metadata on the account itself: account tags in AWS Organizations, subscription tags in Azure, project labels in Google Cloud.
- Tags inside the account for finer splits: per service, per component, per customer-facing product.
With this design, most of the bill is allocated by account before a single resource tag is read. Tags then refine it, and a missing tag means a cost lands on the right team under "untagged" rather than on nobody.
A small tagging standard
The most common failure is a standard with fifteen required tags. Nobody fills them in correctly, and the data is useless. Start with a handful that answer real questions:
| Tag key | Question it answers | Allowed values |
|---|---|---|
team | Who owns this and gets the report? | From the team directory |
service | Which service is it part of? | From the service catalog |
environment | Is it production? | production, staging, development, sandbox |
cost-center | Which budget line does finance map it to? | Finance's list |
Pick one spelling and case and keep it. Tag keys are case sensitive in AWS billing, so Team, team and TEAM become three different columns, and every report then needs code to merge them.
Wherever resources are created through infrastructure as code, set tags once at the provider or module level, not per resource. In Terraform's AWS provider, default_tags applies a set of tags to every resource the provider creates:
provider "aws" {
region = "eu-west-1"
default_tags {
tags = {
team = "checkout"
service = "payments"
environment = "production"
cost-center = "cc-4120"
}
}
}Enforcement
A standard nobody enforces decays within a quarter. Enforce at three levels, from gentle to strict:
- In code review and CI. Policy checks on Terraform plans, with tools such as Checkov, OPA or tflint rules, flag missing tags before anything is created.
- In the organization. AWS Organizations tag policies define allowed keys, case and values, and can prevent non-compliant tag changes for supported resource types. Azure Policy has built-in definitions to require a tag and to inherit tags from the resource group. Google Cloud organization policies and tag bindings fill the same role.
- At creation time, for the expensive resources. A service control policy can deny creating compute and storage without a required tag:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RequireTeamTagOnCompute",
"Effect": "Deny",
"Action": ["ec2:RunInstances", "ec2:CreateVolume", "rds:CreateDBInstance"],
"Resource": "*",
"Condition": {
"Null": { "aws:RequestTag/team": "true" }
}
}
]
}Hard denies need care. Some services create resources on your behalf without passing request tags, and a deny that breaks an autoscaling group or a managed service is worse than a missing tag. Roll SCPs out to a sandbox organizational unit first, and limit them to the actions where untagged resources actually cost money.
Make the tags visible in billing
In AWS, user-defined tags do not appear in Cost Explorer or the Cost and Usage Report until they are activated as cost allocation tags in the Billing console, from the management account. It is a step many organizations miss. Activate the standard keys as soon as they exist, and the AWS-generated aws:createdBy tag as well, which helps track down who created untagged resources.
AWS Cost Categories can then map accounts, tags and services into the business structure finance uses, such as products or departments, including rules for what to do with untagged spend. Azure Cost Management and Google Cloud billing export to BigQuery support the same grouping by subscription, project, tag and label.
Shared costs
Some spend genuinely belongs to everyone: networking hubs, the observability platform, security tooling, support plans. Decide openly how it is handled. The common options are:
- Keep it central and show it as a platform line, not allocated to teams.
- Split it evenly across teams.
- Split it proportionally to each team's direct spend or to a usage driver, such as log volume for the observability bill.
Proportional splits are fairer, but each one is a formula someone has to maintain and explain. Start with central or even splits, and only move to a usage driver for shared costs large enough to justify the argument.
Splitting Kubernetes clusters
A shared cluster shows up in billing as nodes, volumes and load balancers. None of that maps to teams by itself, because many teams' pods run on the same node.
The standard approach allocates each node's cost to the pods on it, based on the resources they reserve or use, usually the larger of the two for CPU and memory. Unused capacity on the node is either spread across tenants or reported as idle, owned by the platform team. Idle is a useful number in its own right: it is the cost of overprovisioning, and it is the platform team's to reduce.
Tools that do this:
- OpenCost, the CNCF project, and Kubecost, built on it, which read cloud pricing and Kubernetes metrics and allocate by namespace, label or controller.
- AWS split cost allocation data for EKS, which adds pod-level cost rows to the Cost and Usage Report based on the pods' requests and usage, with Kubernetes labels available as cost allocation tags.
Either way, allocation depends on namespaces and labels following the same standard as cloud tags. Require team and service labels on namespaces and workloads, enforced by admission policy, and give each team its own namespaces. Teams that set requests far above usage will then see the cost of that choice, which is also the strongest argument for right-sizing them.
Where to start
- Agree on four tag keys with fixed spelling and allowed values, owned by one person.
- Move toward one account, subscription or project per team and environment, with owner metadata on each.
- Set tags through infrastructure-as-code defaults, and check plans in CI.
- Activate the tag keys as cost allocation tags, and build a first report of spend by team, with untagged spend shown openly.
- Install a Kubernetes cost allocation tool and require team labels on namespaces.
- Send each team its monthly report, with a named contact for corrections, before considering chargeback.