Organizational

AWS Resource Tagging

Pattern for applying a consistent tagging taxonomy to all AWS resources for cost allocation, operational traceability, and client billing.

Production

Status: Approved Type: Organizational (Required*) ADR: ADR-0006 — AWS Resource Tagging Taxonomy

*Required for all repositories that provision AWS resources.


Problem

AWS bills aggregate all resource costs by default. Without a tagging standard there is no reliable way to:

  • Attribute spend to a product, team, or client engagement
  • Reconcile infrastructure costs against client invoices
  • Trace a running resource back to the Terraform code that created it
  • Route cost anomaly alerts or security findings to the right owner

Context

When to Use This Pattern

  • Provisioning any AWS resource via Terraform (Lambda, SQS, DynamoDB, S3, IAM roles, etc.)
  • Creating a new project, client engagement, or internal workstream
  • Remediating existing untagged resources

When NOT to Use This Pattern

  • Resources provisioned outside AWS (tagging has no equivalent in other contexts)
  • Local sandbox resources that never reach AWS (e.g. Docker Compose, LocalStack)

Solution

Apply a three-tier tag set to every AWS resource. Tags are defined once in a Terraform locals block and injected via default_tags on the AWS provider. No per-resource tag blocks unless an individual resource needs an override.


Tag Taxonomy

Tier 1 — Required (all resources)

Every AWS resource MUST carry these tags without exception.

Tag keyPurposeValues
productOntopix product line that owns this resourceagents · audits · mistery · platform · shared
billing-modeWho bears the costclient · saas · internal · rd
envDeployment environmentprod · pre · dev · sandbox
teamTeam accountable for this resourceengineering · marketing · operations · infra · finance
ownerAccountable email — team alias or personal addressplatform@ontopix.ai · engineering@ontopix.ai
sourceTerraform root module path that created this resourceontopix/infra/global · ontopix/my-service/.infra
managed-byProvisioning methodterraform · manual · cdk

billing-mode reference

ValueMeaning
clientCost is attributable to a specific client; may be recovered through invoicing
saasCost belongs to the shared SaaS platform, not a specific client
internalOntopix internal tooling — not tied to any client or product line
rdResearch and development; no current billing target

env reference

ValueMeaning
prodProduction — live, customer-facing
prePre-production — mirrors production
devDevelopment — active integration and development testing
sandboxIsolated developer sandbox; ephemeral, non-shared

source convention

Format: ontopix/{repository-name}/{path-to-root-module}

The path is the directory containing the Terraform root module (the directory with backend.tf). This is always a static string — never interpolated from a variable.

ontopix/infra/global
ontopix/schemas/.infra
ontopix/mcp-services/examples/hello-world/.infra

For repositories with a single .infra/ directory, set source once in locals.tf. For repositories with multiple root modules (e.g. per-environment directories), override source per-module as a local.

owner convention

Use team aliases for shared or long-lived infrastructure (platform@ontopix.ai, engineering@ontopix.ai). Use personal addresses only for short-lived or individually-owned resources. Personal addresses must be updated when ownership changes.


Tier 2 — Contextual (required when billing-mode = client)

These tags provide the detail needed to attribute costs to a specific client engagement and produce accurate invoices. They are not used on saas, internal, or rd resources.

Tag keyPurposeValues / format
clientClient identifierLowercase, single word or acronym (e.g. acme, retailco)
projectSpecific engagement within the client relationshipShort slug (e.g. support-agent-v1, audit-pilot)
cost-centerFinance cost center ID for billing roll-upProvided by Finance — use tbd until the registry is published
componentArchitectural layer this resource belongs toapi · worker · db · infra · ml · frontend · edge

client: No canonical registry exists yet. Use lowercase, no hyphens, no spaces. A client registry will be established separately.

project: Only set when billing-mode = client. Not applicable to other billing modes.

cost-center: The taxonomy and IDs will be defined by Finance. Set to tbd until the registry is published. Every billing-mode = client resource MUST carry this tag even with a placeholder value.


Tier 3 — Optional (extended attributes)

All Tier 3 tag keys use the prefix extra: to distinguish them from required tags and prevent future collisions. Values carry a matching type prefix.

Tag keyPurposeValue format
extra:featureSpecific product feature or architectural experiment{name} (e.g. debounce)
extra:channelCommunication channel served by this resource{name} (e.g. whatsapp)
extra:data-classData sensitivity classificationpii · internal · public

The extra: prefix means Tier 3 tags are immediately recognisable in Cost Explorer filters and AWS Config, and can be excluded or included as a group in IAM tag conditions.


Implementation

Step 1 — Define tags in locals.tf

# .infra/locals.tf

locals {
  tags = {
    # Tier 1 — always required
    product      = "agents"
    billing-mode = "client"
    env          = var.environment
    team         = "engineering"
    owner        = "engineering@ontopix.ai"
    source       = "ontopix/my-service/.infra"
    managed-by   = "terraform"

    # Tier 2 — required because billing-mode = "client"
    client       = "acme"
    project      = "support-agent-v1"
    cost-center  = "tbd"          # replace with Finance-issued ID
    component    = "worker"

    # Tier 3 — optional, add as needed
    "extra:channel" = "channel-whatsapp"
  }
}

Step 2 — Inject via default_tags on the AWS provider

# .infra/providers.tf

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = local.tags
  }
}

default_tags propagates the tag set to every resource managed by this provider. Resources that need an override (e.g. a shared bucket spanning multiple components) can declare individual tags blocks — these merge with and take precedence over default_tags.

Step 3 — Validate the env variable

# .infra/variables.tf

variable "environment" {
  description = "Deployment environment"
  type        = string
  validation {
    condition     = contains(["prod", "pre", "dev", "sandbox"], var.environment)
    error_message = "environment must be one of: prod, pre, dev, sandbox"
  }
}

Step 4 — Enforce Tier 1 tags via AWS Config (central infra repo)

The central infra repository maintains a Config rule that flags any resource missing a Tier 1 tag. Enable in alerting-only mode initially; block non-compliant creates via SCP after the remediation window closes.

Alerts route to the resource owner and to infra@ontopix.ai as a catch-all. Where feasible, create a GitHub issue on the repository identified by the source tag.

# infra/global/config/tagging.tf

resource "aws_config_config_rule" "required_tags" {
  name = "ontopix-required-tags"

  source {
    owner             = "AWS"
    source_identifier = "REQUIRED_TAGS"
  }

  input_parameters = jsonencode({
    tag1Key = "product"
    tag2Key = "billing-mode"
    tag3Key = "env"
    tag4Key = "team"
    tag5Key = "owner"
    tag6Key = "source"
    # tag7Key = "managed-by"  # enable after remediation window
  })
}

Applies Principles

  • Evidence Over Assumptions — tagged resources produce cost data; untagged resources require guesswork.
  • Ownership & Responsibilityowner and team make accountability visible in billing reports and security findings without manual lookup.
  • Automation Over Manual Workdefault_tags on the provider is a one-time definition; tagging is never per-resource boilerplate.
  • Security by Designmanaged-by=manual is a drift signal; owner routes security findings to the right contact.
  • Long-Term Thinking — the taxonomy maps directly to AWS Organizations account-level separation when multi-account structure is adopted.

Consequences

Per-client, per-product, and per-team cost reports immediately available in AWS Cost Explorer
source enables one-click navigation from any AWS resource to the exact Terraform module that created it
owner routes cost anomaly alerts and security findings without manual lookup
billing-mode + client tags map directly to future AWS Organizations account-level separation
AI agents provisioning infrastructure have an explicit, enforceable tagging contract
⚠️source cannot be set via default_tags alone in repositories with multiple Terraform root modules — requires per-module local override
⚠️cost-center carries tbd until Finance publishes the cost center registry — limits invoice reconciliation accuracy in the short term
⚠️Existing untagged resources require a one-time remediation pass before Config enforcement is enabled
⚠️billing-mode=rd can accumulate spend silently — review rd-tagged costs monthly

Examples

SaaS platform resource

locals {
  tags = {
    product      = "agents"
    billing-mode = "saas"
    env          = var.environment
    team         = "engineering"
    owner        = "engineering@ontopix.ai"
    source       = "ontopix/agents-service/.infra"
    managed-by   = "terraform"
    component    = "worker"
  }
}

Internal shared infrastructure

locals {
  tags = {
    product      = "shared"
    billing-mode = "internal"
    env          = var.environment
    team         = "infra"
    owner        = "platform@ontopix.ai"
    source       = "ontopix/infra/global"
    managed-by   = "terraform"
  }
}

R&D workload

locals {
  tags = {
    product      = "platform"
    billing-mode = "rd"
    env          = "sandbox"
    team         = "engineering"
    owner        = "engineering@ontopix.ai"
    source       = "ontopix/experiments/.infra"
    managed-by   = "terraform"
    "extra:feature" = "new-retrieval-pipeline"
  }
}

Multi-component repository

When a repository provisions resources spanning multiple components, define a base tag set and merge per component:

# .infra/locals.tf

locals {
  base_tags = {
    product      = "agents"
    billing-mode = "client"
    env          = var.environment
    team         = "engineering"
    owner        = "engineering@ontopix.ai"
    source       = "ontopix/my-service/.infra"
    managed-by   = "terraform"
    client       = "acme"
    project      = "support-agent-v1"
    cost-center  = "tbd"
  }

  api_tags    = merge(local.base_tags, { component = "api" })
  worker_tags = merge(local.base_tags, { component = "worker" })
  infra_tags  = merge(local.base_tags, { component = "infra" })
}

Reference the appropriate local per resource group rather than overriding in individual resource blocks.


AI Agent Rules

When provisioning AWS resources, agents MUST:

  • Apply all Tier 1 tags on every resource — there are no exceptions.
  • Apply Tier 2 tags when billing-mode = client. If client, project, or cost-center values are not already defined in the repository's locals.tf, stop and ask the engineer before proceeding.
  • Never default to billing-mode=rd without explicit instruction — it obscures costs.
  • Set managed-by=terraform on all Terraform-provisioned resources.
  • Set source to the correct ontopix/{repo}/{path} for the root module being worked in.
  • Flag any existing resource in the repository that is missing Tier 1 tags and surface it to the engineer.

Variations

Overriding tags on individual resources

For a resource that belongs to a different component than the module default:

resource "aws_s3_bucket" "static_assets" {
  bucket = "my-service-static-assets"

  tags = {
    component = "frontend"   # overrides the module-level default
  }
}

AWS merges this with default_tags; the component key is overridden, all other tags are inherited.


References