🎉 DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
All Articles

Build an AI Cost Allocation Tagger with Claude API

Build a tool that scans untagged or inconsistently tagged AWS resources, infers the correct team/project/environment tags from naming patterns and context, and opens a PR to apply them — closing the FinOps visibility gap without a manual tagging sprint.

Shubham4 min read
Share:Tweet

Every FinOps initiative hits the same wall: cost allocation by team is impossible because 30-40% of resources have missing or inconsistent tags. A manual tagging sprint is tedious and goes stale again within a quarter. This tool infers the right tags from naming conventions, resource relationships, and context, then proposes them for review instead of guessing silently.

Setup

bash
pip install anthropic boto3

Resource Scanner

python
import boto3
import anthropic
import json
 
client = anthropic.Anthropic()
ec2 = boto3.client("ec2")
rds = boto3.client("rds")
 
 
def get_untagged_resources() -> list[dict]:
    """Find resources missing required tags: team, environment, project."""
    required_tags = {"team", "environment", "project"}
    untagged = []
 
    for reservation in ec2.describe_instances()["Reservations"]:
        for instance in reservation["Instances"]:
            tags = {t["Key"].lower(): t["Value"] for t in instance.get("Tags", [])}
            missing = required_tags - set(tags.keys())
            if missing:
                untagged.append({
                    "resource_id": instance["InstanceId"],
                    "resource_type": "ec2_instance",
                    "existing_tags": tags,
                    "missing_tags": list(missing),
                    "name": tags.get("name", ""),
                    "vpc_id": instance.get("VpcId"),
                    "security_groups": [sg["GroupName"] for sg in instance.get("SecurityGroups", [])],
                })
 
    return untagged

Context Gathering — What Claude Needs to Infer Correctly

python
def gather_context(resource: dict) -> dict:
    """Pull surrounding signals that hint at ownership — name patterns,
    which VPC/subnet it's in, what security groups it shares with tagged resources."""
    context = {"resource": resource}
 
    # Find tagged resources in the same VPC — they often belong to the same team
    if resource.get("vpc_id"):
        same_vpc = ec2.describe_instances(
            Filters=[{"Name": "vpc-id", "Values": [resource["vpc_id"]]}]
        )
        tagged_neighbors = []
        for reservation in same_vpc["Reservations"]:
            for instance in reservation["Instances"]:
                tags = {t["Key"].lower(): t["Value"] for t in instance.get("Tags", [])}
                if "team" in tags:
                    tagged_neighbors.append({"name": tags.get("name", ""), "team": tags["team"]})
        context["tagged_neighbors_in_vpc"] = tagged_neighbors[:5]
 
    return context

Tag Inference with Claude

python
INFER_PROMPT = """Infer the missing tags for this AWS resource based on naming
patterns and surrounding context. Be conservative — only suggest a tag if
you have reasonable evidence, otherwise mark it "uncertain".
 
Resource: {resource_id} ({resource_type})
Existing tags: {existing_tags}
Missing tags: {missing_tags}
Resource name: {name}
Security groups: {security_groups}
 
Tagged resources in the same VPC (evidence of team ownership by proximity):
{tagged_neighbors}
 
Respond with ONLY valid JSON:
{{"inferred_tags": {{"team": "...", "environment": "...", "project": "..."}},
  "confidence": "high"|"medium"|"low",
  "reasoning": "one sentence explaining the inference"}}
 
For any tag you cannot infer with reasonable confidence, use "uncertain" as the value."""
 
 
def infer_tags(context: dict) -> dict:
    resource = context["resource"]
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=400,
        messages=[{
            "role": "user",
            "content": INFER_PROMPT.format(
                resource_id=resource["resource_id"],
                resource_type=resource["resource_type"],
                existing_tags=resource["existing_tags"],
                missing_tags=resource["missing_tags"],
                name=resource["name"],
                security_groups=resource["security_groups"],
                tagged_neighbors=context.get("tagged_neighbors_in_vpc", []),
            )
        }]
    )
    text = response.content[0].text.strip()
    if text.startswith("```"):
        text = text.split("```")[1].replace("json", "", 1).strip()
    return json.loads(text)

Output: A Reviewable Terraform Plan, Not a Silent Apply

python
def generate_tagging_plan(untagged_resources: list[dict]) -> str:
    """Never apply tags directly — always output a reviewable diff."""
    plan_lines = ["# Proposed tag changes — review before applying\n"]
 
    for resource in untagged_resources:
        context = gather_context(resource)
        inference = infer_tags(context)
 
        if inference["confidence"] == "low":
            plan_lines.append(f"# SKIPPED (low confidence): {resource['resource_id']} — needs manual review")
            continue
 
        plan_lines.append(f"# {resource['resource_id']} ({resource.get('name', 'unnamed')})")
        plan_lines.append(f"# Confidence: {inference['confidence']}{inference['reasoning']}")
        for key, value in inference["inferred_tags"].items():
            if value != "uncertain":
                plan_lines.append(f"aws ec2 create-tags --resources {resource['resource_id']} --tags Key={key},Value={value}")
        plan_lines.append("")
 
    return "\n".join(plan_lines)

Usage

bash
python cost_tagger.py --output tagging-plan.txt
 
# tagging-plan.txt:
# # i-0abc123 (worker-node-payments-3)
# # Confidence: high — name pattern matches 4 other tagged "payments" resources in same VPC
# aws ec2 create-tags --resources i-0abc123 --tags Key=team,Value=payments
# aws ec2 create-tags --resources i-0abc123 --tags Key=environment,Value=production
#
# # SKIPPED (low confidence): i-0xyz789 — needs manual review

Why This Stays Human-Reviewed, Not Automated

Wrong cost allocation tags are worse than missing tags — a resource mistagged to the wrong team pollutes that team's cost dashboard with a number they'll spend hours investigating and never resolve, because it isn't actually theirs. Low-confidence inferences are explicitly skipped and flagged for manual review rather than guessed at. Treat this as a first draft that cuts a tagging sprint from days to an afternoon of review, not a fire-and-forget automation.


More FinOps and AI tooling? Read our FinOps guide for DevOps engineers and Build AI AWS cost anomaly detector with Claude API.

🔧

Today I Fixed

Short real fixes from production — posted daily

Browse fixes
Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments