Resource tagging and cost allocation system
Without tagging, it's impossible to answer "how much does this product cost?" or "why did the bill grow?". Resource tagging is the foundation of FinOps: the ability to split cloud spending by products, teams, environments, and customers.
Tagging strategy
Standard set of mandatory tags:
| Tag | Values | Description |
|---|---|---|
Environment |
production, staging, dev | Deployment environment |
Team |
backend, frontend, data, devops | Resource owner |
Product |
api, dashboard, billing, data-pipeline | Product component |
CostCenter |
eng-001, data-002 | Cost center for accounting |
ManagedBy |
terraform, manual, helm | Management method |
Additional tags for specific cases: Customer (for SaaS per-customer billing), Project (for project work), ExpiresAt (for temporary resources).
Enforcement via AWS Config
AWS Config Rule to check for mandatory tags:
resource "aws_config_config_rule" "required_tags" {
name = "required-tags"
source {
owner = "AWS"
source_identifier = "REQUIRED_TAGS"
}
input_parameters = jsonencode({
tag1Key = "Environment"
tag1Value = "production,staging,dev,test"
tag2Key = "Team"
tag3Key = "Product"
tag4Key = "ManagedBy"
})
scope {
compliance_resource_types = [
"AWS::EC2::Instance",
"AWS::RDS::DBInstance",
"AWS::ElasticLoadBalancingV2::LoadBalancer",
"AWS::S3::Bucket",
"AWS::Lambda::Function"
]
}
}
# Auto-remediation: Lambda adds default tags when violation detected
resource "aws_config_remediation_configuration" "tag_remediation" {
config_rule_name = aws_config_config_rule.required_tags.name
target_type = "SSM_DOCUMENT"
target_id = "AWS-SetRequiredTags"
automatic = false # manual approval before applying
parameter {
name = "RequiredTags"
static_value = "Environment=unknown,Team=unknown"
}
}
Terraform: provider-level tagging
# provider.tf — default_tags applied to all resources
provider "aws" {
region = "eu-central-1"
default_tags {
tags = {
ManagedBy = "terraform"
Repository = "github.com/company/infrastructure"
Environment = var.environment
Team = var.team
}
}
}
# Specific tags added at resource level
resource "aws_instance" "api_server" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.medium"
tags = {
Name = "api-server-${var.environment}"
Product = "api"
# Environment and Team inherited from default_tags
}
}
Cost Allocation Tags and AWS Cost Categories
After creating tags, enable them in Cost Explorer:
import boto3
ce = boto3.client('ce', region_name='us-east-1')
# Activate tags for cost allocation (takes up to 24 hours)
ce.activate_tags(
Tags=['Environment', 'Team', 'Product', 'CostCenter']
)
# Create Cost Category for grouping by teams
ce.create_cost_category_definition(
Name='Team-Costs',
RuleVersion='CostCategoryExpression.v1',
Rules=[
{
'Value': 'Backend Team',
'Rule': {
'Tags': {
'Key': 'Team',
'Values': ['backend', 'api']
}
}
},
{
'Value': 'Data Team',
'Rule': {
'Tags': {
'Key': 'Team',
'Values': ['data', 'analytics', 'ml']
}
}
}
],
DefaultValue='Unallocated'
)
Untagged resources audit script
import boto3
from collections import defaultdict
REQUIRED_TAGS = {'Environment', 'Team', 'Product'}
def audit_untagged_resources(region='eu-central-1'):
session = boto3.Session(region_name=region)
untagged = defaultdict(list)
# EC2 Instances
ec2 = session.client('ec2')
instances = ec2.describe_instances(
Filters=[{'Name': 'instance-state-name', 'Values': ['running', 'stopped']}]
)
for reservation in instances['Reservations']:
for inst in reservation['Instances']:
tags = {t['Key']: t['Value'] for t in inst.get('Tags', [])}
missing = REQUIRED_TAGS - set(tags.keys())
if missing:
untagged['EC2'].append({
'id': inst['InstanceId'],
'missing_tags': list(missing),
'name': tags.get('Name', 'unnamed')
})
# RDS
rds = session.client('rds')
dbs = rds.describe_db_instances()
for db in dbs['DBInstances']:
arn = db['DBInstanceArn']
tags_resp = rds.list_tags_for_resource(ResourceName=arn)
tags = {t['Key']: t['Value'] for t in tags_resp['TagList']}
missing = REQUIRED_TAGS - set(tags.keys())
if missing:
untagged['RDS'].append({
'id': db['DBInstanceIdentifier'],
'missing_tags': list(missing)
})
return untagged
if __name__ == '__main__':
result = audit_untagged_resources()
total = sum(len(v) for v in result.values())
print(f"\nUntagged resources: {total}")
for service, resources in result.items():
print(f"\n{service}: {len(resources)} resources")
for r in resources[:5]: # show first 5
print(f" {r['id']}: missing {r['missing_tags']}")
Showback and Chargeback reports
def generate_team_cost_report(month: str):
"""month: '2025-11'"""
ce = boto3.client('ce', region_name='us-east-1')
start = f"{month}-01"
# last day of month
year, mon = map(int, month.split('-'))
import calendar
last_day = calendar.monthrange(year, mon)[1]
end = f"{month}-{last_day:02d}"
response = ce.get_cost_and_usage(
TimePeriod={'Start': start, 'End': end},
Granularity='MONTHLY',
Metrics=['UnblendedCost'],
GroupBy=[{'Type': 'TAG', 'Key': 'Team'}]
)
report = {}
for group in response['ResultsByTime'][0]['Groups']:
team = group['Keys'][0].replace('Team$', '') or 'Untagged'
cost = float(group['Metrics']['UnblendedCost']['Amount'])
report[team] = round(cost, 2)
return dict(sorted(report.items(), key=lambda x: x[1], reverse=True))
Implementation timeline
- Tagging strategy development + documentation — 1 day
- Terraform default_tags for all modules — 2-3 days
- AWS Config Rules for enforcement — 1 day
- Audit + tagging existing resources — 2-5 days (depends on scale)
- Cost Categories + reports — 1-2 days







