skip to content

Your Terraform root module must stamp the same owner, environment and cost-centre tags on every AWS resource, plus a per-resource Name tag. How do you structure that with locals, and what happens on the next plan when someone adds a tag?

level: middleimportance: should knowfreq 62%

answer

  1. one map, many use sites
  2. later arguments win
  3. adding a key touches every user
  4. tags update in place, names often replace
  5. the provider has default_tags too

basics

~20 s

Define the shared tags once in a locals block as a map, then set each resource's tags to merge(local.common_tags, { Name = "..." }) so per-resource keys win. Adding a key is a one-line edit that produces an in-place tag update across every resource using the local.

solid answer

~50 s

Put the shared set in a `locals` block as a single map — `local.common_tags` — built from the module's input variables so environment and owner are not hardcoded. Every resource then sets `tags = merge(local.common_tags, { Name = "${local.name_prefix}-web" })`. `merge` takes maps left to right with later keys winning, so a resource can add a `Name` or override a shared key without touching the base map. The payoff is the change profile: adding a tag is one line in one place, and the next plan shows an in-place update on every resource that reads the local, because tags on most AWS resources are updatable in place rather than forcing replacement. Review it before applying — a plan touching two hundred resources is normal here, but you want to see that it really is only tags. The AWS provider also offers a provider-level `default_tags` block that stamps every resource it manages; teams usually pick one mechanism rather than mixing both.

code

hcl · 26 lines
hcl
variable "environment" {
  type = string
}

variable "owner" {
  type = string
}

locals {
  name_prefix = "payments-${var.environment}"

  common_tags = {
    Environment = var.environment
    Owner       = var.owner
    ManagedBy   = "terraform"
  }
}

resource "aws_s3_bucket" "artifacts" {
  bucket = "${local.name_prefix}-artifacts"

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-artifacts"
    Role = "build-output"
  })
}

go deeper

for a junior

Know that tags is a map and that a locals entry can hold that map so it is written once. Be able to read merge(local.common_tags, { Name = "x" }) and say what the resulting map contains.

for a middle

Explain merge's left-to-right override rule and why the base map is a local built from variables rather than a variable itself. Describe the plan you expect after adding a key.

for a senior

Show the operational judgment: a two-hundred-resource tag plan is normal but still gets read line by line, tags update in place while names usually force replacement, and mixing this with provider default_tags creates ambiguity.

for a principal

Own the estate-wide convention — one tagging mechanism, one canonical key casing, and tags that actually feed cost allocation and ownership routing rather than accumulating as decoration nobody queries.

## The pattern Almost every organisation requires a baseline tag set for cost allocation, ownership and cleanup automation. The Terraform expression of that is one map in a `locals` block, merged at each use site: ```hcl variable "environment" { type = string } variable "owner" { type = string } locals { name_prefix = "payments-${var.environment}" common_tags = { Environment = var.environment Owner = var.owner CostCentre = var.cost_centre ManagedBy = "terraform" } } resource "aws_instance" "web" { # ... tags = merge(local.common_tags, { Name = "${local.name_prefix}-web" Role = "frontend" }) } ``` Two things make this work. First, the local is *derived from variables*, not hardcoded — the same configuration produces the right tags in every environment. Second, `merge` combines maps left to right, with keys in later arguments overriding earlier ones, so the per-resource literal is free to add `Name` and `Role` and even to override `Owner` for one exceptional resource without anyone editing the shared map. ## Why a local and not a variable You could declare `variable "common_tags"` with a default, but then the tag policy becomes part of the module's public interface: any caller can replace the whole map, including dropping `ManagedBy`. As a local it is a decision the module owns, and the *inputs* to it — environment, owner, cost centre — stay as variables. That is the general shape: expose the parameters, keep the derivation internal. ## The change profile This is what interviewers usually probe. Adding a key to `local.common_tags` is one line, but the next `terraform plan` touches every resource that reads the local. For tags on most AWS resources that is an in-place update — Terraform shows `~ tags` with the added key and no `forces replacement` marker — because the provider maps tag changes onto tagging API calls rather than a recreate. A plan of two hundred in-place tag updates is expected here and is not a reason to panic; it *is* a reason to read the diff, because a genuinely large plan is where a destructive change hides in plain sight. Two cautions worth voicing. Not every taggable object is a plain in-place update — some resources expose tags on nested or separate objects, and a few third-party providers implement tag changes less gracefully — so you read the plan rather than assume. And a large tag-only apply is still an apply against production, so it belongs in the normal review-and-approve flow, not a quick unreviewed run. ## Interactions worth knowing **Provider `default_tags`.** The AWS provider supports a `default_tags` block in the provider configuration that applies a tag set to every resource that provider manages, which removes the need to write `tags =` on each resource. It is an alternative to the merge pattern, not a complement — running both invites confusion about which layer set a key. Pick one and state which in the repository's conventions. **Type consistency.** A tag map is `map(string)`, so every value must be a string. A boolean or number sneaking in through a variable causes a type error at plan time; wrap with `tostring()` when in doubt. **Case sensitivity.** Tag keys are case-sensitive in AWS. `Environment` and `environment` are two separate tags, and `merge` will not treat them as the same key — a common source of duplicated tags in a repository where two people wrote the map differently. **Do not put secrets in tags.** Tags are visible in the console, in state, in cost reports and in plan output. The shared map is exactly the wrong place for anything sensitive. ## Naming built from the same idea The same locals block usually carries `name_prefix`. A single derived prefix used by every resource name keeps naming consistent, makes an estate greppable, and means renaming a project is one edit — though be aware that for most resources the name *is* part of identity, so changing a prefix plans as destroy-and-create, not an update. That asymmetry between tags (mutable) and names (often identity) is the useful thing to be able to say out loud.

  • A plan after your tag edit shows one resource being destroyed and recreated rather than updated. What would you look at?
    Read that resource's diff for a `forces replacement` marker and check which attribute carries it — it is usually not the tag at all but something else that changed in the same commit, or a resource whose name or an immutable attribute is derived from the same local you edited. Locals fan out, so one edit can reach an identity attribute. Confirm before applying; `-target` is for isolating the investigation, not for shipping the change.
  • Why prefer building common_tags from input variables rather than writing the environment name as a literal?
    Because the same root module or module call is used per environment, and a literal makes the configuration wrong everywhere but one place. Keeping `var.environment` as the input and the map as the derivation means the tag policy lives in one place while the value varies per environment, and the module stays reusable rather than being copied and edited.
  • What breaks if a value in the tag map is a number rather than a string?
    Terraform fails at plan time with a type error, because a resource's `tags` argument is `map(string)` and Terraform's type system will convert only where a conversion is unambiguous. A number generally converts, but a list or object does not, and `null` behaves differently again. Wrap with `tostring()` for anything that arrives from a variable typed loosely.

The common tag map is a rubber stamp kept in one drawer: every resource presses the same stamp, and per-resource keys are what you write by hand beside it.

saying these in an interview costs you the question

  • Repeats the same tag literals in every resource block
  • Puts the shared tag map in a variable so any caller can replace it
  • Assumes a tag change forces resources to be recreated
  • Hardcodes the environment name inside the shared tag map
  • Expects merge to treat Environment and environment as one key

context