Skip to content
← Blog

The Engineer Left. The Cloud Bill Didn't: A Runbook for Ownership Tagging Every Resource

Learn how to implement cloud resource ownership tagging with enforcement, detection, and remediation steps, so you never pay for another orphaned instance.

7 min readSimon-Daniel März
The Engineer Left. The Cloud Bill Didn't: A Runbook for Ownership Tagging Every ResourceGenerated with the help of AI

An EC2 instance runs in eu-central-1. It costs €142/month. Nobody on your current team knows what it does, which team provisioned it, or whether turning it off will break production at 3 a.m. The engineer who spun it up left eight months ago. Sound familiar?

This is not a rare edge case. It is the default state of every cloud account that skips ownership tagging. And the longer you wait to fix it, the more painful the cleanup becomes, orphaned volumes, unattached Elastic IPs, stale RDS snapshots, and a monthly bill that reads like a ransom note.

This post walks through a concrete runbook for attaching an owner to every cloud resource you can find, enforcing that ownership at creation time, and detecting resources that slip through.

Why Most Tagging Strategies Fail Before They Start

The core problem is not technical, it is organizational. Teams adopt tagging guidelines in a wiki page, and six months later half the resources have no tags at all. Here is why:

  • No enforcement at creation time. Developers spin up resources through the console, CLI, or IaC without a gate that rejects untagged resources.
  • Vague ownership definitions. A tag like owner: devops tells you nothing about who gets paged when the bill spikes. "devops" is a team, not a person or a cost center.
  • No retroactive scanning. Resources created before the tagging policy existed sit untouched, invisible to any reporting pipeline.
  • Tag key proliferation. One team uses Owner, another uses owner, a third uses responsible. Without a canonical schema, aggregation tools choke.

Each of these problems has a specific technical fix. The sections below walk through them in order.

Step 1: Define the Ownership Schema

Before writing a single line of policy-as-code, agree on the minimum set of tags that every resource must carry. A practical baseline looks like this:

Tag KeyRequiredExample ValuePurpose
OwnerYes[email protected]Single accountable person
TeamYesplatform-infraOwning team or cost center
CostCenterYesCC-4710Finance allocation code
EnvironmentYesproductionproduction, staging, dev
ProjectNoinvoice-syncProduct or initiative name
ManagedByNoterraformProvisioning method

Why the single Owner email matters: a team name rots the moment someone transfers departments. An email address lets you build a Slack or Teams integration that notifies the right person directly. If the address bounces, you know the tag is stale and the resource needs re-assignment.

Store this schema in a shared repository, not a Confluence page nobody reads. A JSON Schema file works well because tooling can validate against it automatically:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "RequiredResourceTags",
  "type": "object",
  "required": ["Owner", "Team", "CostCenter", "Environment"],
  "properties": {
    "Owner": {
      "type": "string",
      "pattern": "^[a-z.]+@acme\\.de$"
    },
    "Team": {
      "type": "string",
      "minLength": 3
    },
    "CostCenter": {
      "type": "string",
      "pattern": "^CC-[0-9]{4}$"
    },
    "Environment": {
      "type": "string",
      "enum": ["production", "staging", "dev"]
    }
  },
  "additionalProperties": true
}

Commit this file alongside your infrastructure code. Every pull request that touches IaC can reference it.

Step 2: Enforce Tags at Creation Time

Enforcement is where most teams drop the ball. If you only check tags after deployment, you are already paying for the mistake. Here are three enforcement layers, from simplest to strongest.

2a. AWS Service Control Policies (SCPs)

SCPs act as a guardrail at the organization level. The following SCP denies resource creation if the Owner tag is missing:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RequireOwnerTag",
      "Effect": "Deny",
      "Action": [
        "ec2:RunInstances",
        "rds:CreateDBInstance",
        "s3:CreateBucket",
        "lambda:CreateFunction",
        "ecs:CreateService"
      ],
      "Resource": "*",
      "Condition": {
        "Null": {
          "aws:RequestTag/Owner": "true"
        }
      }
    }
  ]
}

Apply this SCP to every organizational unit except the root. Developers who forget the tag get an explicit error at deploy time, fast feedback, no silent orphan creation.

2b. Terraform Sentinel / OPA Policies

If your team provisions through Terraform, add a Sentinel or Open Policy Agent rule that blocks terraform apply when required tags are absent:

# variables.tf
variable "required_tags" {
  type    = list(string)
  default = ["Owner", "Team", "CostCenter", "Environment"]
}

Then in your CI pipeline, validate before apply:

# validate_tags.py
import json, sys

REQUIRED_TAGS = ["Owner", "Team", "CostCenter", "Environment"]

def check_plan(plan_file: str) -> list[str]:
    with open(plan_file) as f:
        plan = json.load(f)

    violations = []
    for resource in plan.get("resource_changes", []):
        if resource["change"]["actions"] == ["create"]:
            tags = resource["change"]["after"].get("tags", {}) or {}
            missing = [t for t in REQUIRED_TAGS if t not in tags]
            if missing:
                violations.append(
                    f"{resource['address']}: missing tags {missing}"
                )
    return violations

if __name__ == "__main__":
    errors = check_plan(sys.argv[1])
    for e in errors:
        print(f"ERROR: {e}", file=sys.stderr)
    if errors:
        sys.exit(1)

This script inspects the Terraplan plan JSON and fails the pipeline if any newly created resource lacks required tags. Wire it into your CI step between terraform plan -out=plan.json and terraform apply plan.json.

2c. CI Pipeline Gate for Console-CLI Resources

For resources created outside IaC (yes, it still happens), add a scheduled scan that flags untagged resources within 24 hours of creation and posts to a Slack channel. We will cover the detection script in Step 3.

Step 3: Detect Resources That Slipped Through

Even with SCPs and IaC policies, resources can sneak in, Lambda@Edge functions in us-east-1, DynamoDB global tables, or manually created IAM roles. A nightly audit catches them.

The following Python script uses Boto3 to scan for EC2 instances and RDS instances missing the Owner tag:

import boto3
from datetime import datetime, timezone

REQUIRED_TAG = "Owner"
REGIONS = ["eu-central-1", "eu-west-1", "us-east-1"]

def audit_ec2(region: str) -> list[dict]:
    ec2 = boto3.client("ec2", region_name=region)
    paginator = ec2.get_paginator("describe_instances")
    orphans = []

    for page in paginator.paginate():
        for reservation in page["Reservations"]:
            for instance in reservation["Instances"]:
                if instance["State"]["Name"] == "terminated":
                    continue
                tags = {t["Key"]: t["Value"] for t in instance.get("Tags", [])}
                if REQUIRED_TAG not in tags:
                    orphans.append({
                        "resource_type": "EC2",
                        "region": region,
                        "id": instance["InstanceId"],
                        "launched": instance["LaunchTime"].isoformat(),
                        "tags": tags,
                    })
    return orphans

def audit_rds(region: str) -> list[dict]:
    rds = boto3.client("rds", region_name=region)
    paginator = rds.get_paginator("describe_db_instances")
    orphans = []

    for page in paginator.paginate():
        for db in page["DBInstances"]:
            tag_list = rds.list_tags_for_resource(
                ResourceName=db["DBInstanceArn"]
            )["TagList"]
            tags = {t["Key"]: t["Value"] for t in tag_list}
            if REQUIRED_TAG not in tags:
                orphans.append({
                    "resource_type": "RDS",
                    "region": region,
                    "id": db["DBInstanceIdentifier"],
                    "launched": db["InstanceCreateTime"].isoformat(),
                    "tags": tags,
                })
    return orphans

if __name__ == "__main__":
    all_orphans = []
    for region in REGIONS:
        all_orphans.extend(audit_ec2(region))
        all_orphans.extend(audit_rds(region))

    print(f"Found {len(all_orphans)} resources missing '{REQUIRED_TAG}' tag:")
    for o in all_orphans:
        print(f"  [{o['resource_type']}] {o['id']} in {o['region']} "
              f"(launched {o['launched']})")

Run this script nightly via a CI/CD pipeline or a Lambda function. Send the output to a dedicated Slack channel like #cloud-orphans. If the list grows week-over-week, your enforcement layer has a gap.

Hypothetical scenario: A mid-sized SaaS company runs this scan across three accounts and 14 regions. Night one surfaces 237 resources with no Owner tag. After two weeks of triage, they identify 89 resources that were safe to terminate, saving an estimated €3,200/month in compute and storage costs. The remaining 148 get tagged, and the nightly scan drops to near-zero findings once enforcement is live.

Step 4: Remediate and Assign Ownership

Detection without action is just noise. Build a remediation workflow:

  1. Triage the orphan list. Group by AWS account, region, and resource type. Assign each group to the most likely owning team based on VPC, subnet, or security group naming.
  2. Ping the team. If your Owner tag schema includes an email, notify the suspected owner: "Resource i-0abc123 in eu-central-1 has no owner tag. Claim it or it will be scheduled for decommission in 14 days."
  3. Auto-tag what you can. For resources inside a clearly named VPC or CloudFormation stack, auto-apply the Owner and Team tags from the stack metadata.
  4. Schedule deletion. After the grace period, terminate resources that no one claimed. Use a Lambda function that checks a DynamoDB "claims" table before acting.

This workflow turns a one-time cleanup into a continuous process. New orphans surface nightly; the 14-day claim window gives teams time to react without slowing down development.

Step 5: Report Cost by Owner

Tagging is pointless if finance cannot use it. Set up a cost allocation report that groups spend by Owner, Team, and CostCenter. In AWS, activate Cost Allocation Tags in the Billing console, both user-defined and AWS-generated tags appear in Cost Explorer after a 24-hour propagation delay.

A practical reporting cadence:

  • Weekly: Email each team lead a one-page summary of their tagged spend vs. budget.
  • Monthly: Present a cross-team cost review showing the top 10 most expensive resources and who owns them.
  • Quarterly: Audit for tag accuracy, run the detection script and compare against the previous quarter.

When engineers see their name next to a €400/month bill for a forgotten dev database, behaviour changes faster than any policy document.

Best Practices: 5 Rules That Keep Your Tags Clean

  1. Enforce at creation, not after deployment. SCPs and IaC policy checks are non-negotiable. A resource created without tags will never tag itself.
  2. One person per Owner tag, not a team alias. Teams reorganise; accountability should not shuffle with them. Use a human email address.
  3. Standardise tag keys in code, not in documentation. Store the canonical tag list in a shared module (Terraform module, Pulumi component, or a Python constant) and import it everywhere.
  4. Run a nightly orphan scan across every account and region. The detection script from Step 3 should be a scheduled pipeline job, not a manual exercise.
  5. Tie cost reports to ownership data. If finance sends a monthly "your cloud spend is too high" email with no breakdown by owner, it will be ignored. Ownership-tagged cost reports create accountability that sticks.

Teams building or refactoring cloud infrastructure often find that the tagging and governance layer is the part nobody wants to touch, it is cross-cutting, tedious to get wrong, and invisible when it works. ProjectMakers delivers this as a fixed-price engagement: schema design, SCP and IaC enforcement, nightly audit pipelines, and cost reporting dashboards, so your team can focus on the product, not the plumbing.

If you are planning a cloud migration or re-architecting your infrastructure, the ownership model should be designed before the first resource is provisioned. Get in touch through our custom software development services to scope out what a proper governance layer looks like for your environment.

Start small: pick one AWS account, run the audit script tonight, and tag everything it finds. Tomorrow, enable the SCP. In a week, you will have a clean bill of ownership, and a cloud bill that finally makes sense.


Source: How to attach an owner to every cloud resource you find

Continue in this topic

Operations and open source