Tagging Cloud Resources to Attribute AI Spend by Team
10 min read · updated August 11, 2026
Tagging every resource and then finding that Cost Explorer shows one undifferentiated blob is the standard first experience of cost allocation on AWS. Two mechanisms explain almost all of it, and neither is visible from the tagging side.
Activation is the step people miss
A tag applied to a resource is not a cost allocation tag. It becomes one only after you activate the key in the billing console, and AWS documents two separate delays around that: it can take up to 24 hours for a newly-used tag key to appear on the cost allocation tags page at all, and up to a further 24 hours for an activated key to take effect. AWS documents the activation flow here.
Activation is an API call, so it belongs in your infrastructure code rather than in a runbook nobody runs:
aws ce update-cost-allocation-tags-status \
--cost-allocation-tags-status \
TagKey=team,Status=Active \
TagKey=env,Status=Active \
TagKey=workload,Status=ActiveThe call accepts a maximum of 20 tag keys per request. Once active, user-defined tags appear in cost and usage reports under a user: prefix — so the column for your team tag is user:team — while AWS-generated tags use aws:. If you are writing Athena queries against a CUR, that prefix is the thing that makes the first query return nothing.
Activation is also not retroactive. Costs incurred before the key was activated stay untagged forever. Activate the keys on day one of the scheme, before you have finished tagging anything, because the alternative is a month of unattributable spend.
Bedrock is not a resource you can tag
Here is the structural surprise. On-demand Bedrock inference is not a resource in your account. There is no endpoint object, no instance, no ARN sitting there accruing cost that you could hang a tag on. The charge is generated by an API call, and an API call has no tags.
The mechanism AWS added for this is the application inference profile. You create a profile that points at a model, tag the profile, and then invoke the profile ARN instead of the model id. The invocation cost is attributed to the profile, and the profile’s tags flow through to Cost Explorer and to the Cost and Usage Report. AWS documents application inference profiles here.
aws bedrock create-inference-profile \
--inference-profile-name claims-chatbot \
--model-source copyFrom=arn:aws:bedrock:us-east-1::foundation-model/MODEL_ID \
--tags key=team,value=claims key=workload,value=chatbot
# then invoke the profile ARN, not the model id
aws bedrock-runtime converse \
--model-id arn:aws:bedrock:us-east-1:ACCOUNT:application-inference-profile/PROFILE_ID \
--messages '[{"role":"user","content":[{"text":"hello"}]}]'The profile is model-specific, so a team using three models needs three profiles carrying the same tags. Tags can also be added afterwards with TagResource. The important architectural consequence is that the model id becomes a deployment-time configuration value rather than a constant in code — every call site has to be able to take a profile ARN.
Lambda, S3 and the rest
The rest of the estate is more conventional but has its own edges.
- Lambda tags at the function level, and the tags cover invocation and duration charges. A function is usually owned by exactly one team, so this is the clean case. Tag in the deployment tool, not by hand — a redeploy that omits tags silently un-attributes the function.
- S3 tags at the bucket level. There is no per-prefix cost allocation, so a shared bucket with
/team-a/and/team-b/prefixes cannot be split by tag at all. If per-team storage attribution matters, that is an argument for per-team buckets, made at design time rather than at invoice time. - EC2 and EBS tag independently. A tagged instance with an untagged volume attributes compute and orphans storage, and an orphaned volume left behind by a terminated instance is untagged spend that persists. Tag volumes in the launch template.
- Data transfer is frequently the line nobody can attribute, because it is generated by traffic rather than by a resource. NAT gateway processing charges are the usual offender for model-calling workloads in a private subnet.
Enforcing the scheme
A tagging scheme decays without enforcement, and enforcement has three layers with very different strengths.
- Define the allowed keys and values as an Organizations tag policy. This constrains value casing and spelling, which is the difference between one
teamdimension and four (Platform,platform,plat,Platform-Team). - Add an SCP or IAM condition requiring the tag at creation time for the resource types that matter, using
aws:RequestTag/teamandaws:TagKeys. This is the only layer that actually prevents an untagged resource existing. - Run a config rule or a scheduled query over the CUR for untagged spend, and report it as a number per team per week. Untagged spend that has an owner gets fixed; untagged spend that is everyone’s problem does not.
Reading the split
With keys activated and resources tagged, Cost Explorer can group by team and filter by service, and the CUR carries the same dimension as a user:team column for arbitrary SQL. Two habits make the resulting report trustworthy.
Always show the untagged bucket. A report that quietly drops unattributable spend looks tidier and lies about the total; a report with an explicit “unallocated” row makes the gap visible and shrinking it becomes somebody’s job. And reconcile the sum of the per-team figures against the invoice total every month, because a drift between them is the earliest signal that a new service or a new deployment path is escaping the scheme.