Locals, Outputs & Data Sources

The three constructs that stop a configuration repeating itself: named expressions, return values, and read-only lookups of infrastructure you do not manage — plus why you should never compute a value you could reference.

intermediate 18 min lesson hands-on task included

Variables bring values in. Outputs send values out. Locals name things in between, and data sources read the world you did not build. Together they keep a configuration from becoming a wall of repeated literals.


Topic 1: Locals

A local names an expression so it can be reused without repetition. Where a variable is an input the caller supplies, a local is a value the module computes for itself.

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

  common_tags = {
    Team        = "platform"
    Environment = var.environment
    ManagedBy   = "terraform"
  }

  subnet_names = [for cidr in var.private_cidrs : "${local.name_prefix}-${cidr}"]
}

Reference as local.<name>. Locals can refer to variables, resource attributes, data sources and other locals — but not to themselves, directly or through a cycle. That is an error, not an infinite loop.

The common_tags pattern is the one you will use most:

resource "aws_instance" "app" {
  tags = merge(local.common_tags, { Name = "${local.name_prefix}-app" })
}

Group related locals into one block, particularly when they depend on each other. Scattering them across files makes the dependency chain invisible.


Topic 2: Outputs

output "instance_public_ip" {
  value       = aws_instance.web.public_ip
  description = "Public IP address of the web instance"
}

output "db_endpoint" {
  value     = aws_db_instance.app.endpoint
  sensitive = true
}

Outputs do three jobs:

  • Echo values after apply. Provisioning a bastion and printing its IP saves a trip to the console.
  • Return values from a module to its caller.
  • Expose values to tooling via terraform output -json, which is how you feed a configuration-management inventory or a downstream pipeline step.
terraform output                  # everything
terraform output instance_ip      # one value
terraform output -json            # machine-readable

Terraform prints outputs in alphabetical order, not declaration order — a reminder that block ordering in a file means nothing.

Since 0.12 you can output an entire resource, returning every exported attribute:

output "bucket" {
  value = aws_s3_bucket.artifacts
}

Genuinely useful when debugging: rather than guessing which attribute you need, output the whole object once and read what is available.

The sensitive caveat, again:

Marking an output sensitive stops Terraform printing it. It does not remove it from state.


Topic 3: Data Sources

A data source is a read-only view of something Terraform does not manage. It creates nothing; it looks things up.

data "aws_ami" "base" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-2.0.*-x86_64-gp2"]
  }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.base.id
  instance_type = "t3.micro"
}

Reference form: data.<type>.<local_name>.<attribute>.


Topic 4: Never Compute What You Could Reference

The most consequential idea in this lesson, and easy to get wrong because the wrong version works.

Many resource identifiers follow a predictable format. You could build one with string interpolation instead of looking it up. Do not.

# Wrong — produces a string that is usually correct
Resource = "arn:aws:s3:::${var.bucket_name}/*"

# Right — creates a real dependency
data "aws_s3_bucket" "artifacts" {
  bucket = var.bucket_name
}

Resource = "${data.aws_s3_bucket.artifacts.arn}/*"

Three things the data source gives you that the computed string does not:

  1. It fails when the bucket does not exist. The computed version happily produces a policy pointing at nothing, and you discover the problem when something is denied at runtime.
  2. It tracks change. If the returned attribute moves, your configuration follows. A hardcoded format assumption breaks silently the day the provider changes it.
  3. It is a graph edge. Terraform knows there is a dependency and orders accordingly.

The same reasoning applies to resources you do manage — always reference aws_vpc.main.id, never a literal copied from the console.

The other reasons data sources earn their place:

  • Cross-project references. As a project grows you split it into separate state files. Data sources let one project read another’s resources without managing them.
  • Avoiding volatile literals. Machine image IDs change on every vendor release; a data source resolves the current one at plan time.
  • Adopting existing infrastructure gradually. When migrating a hand-built estate, data sources let new code reference the old world before you import it.

Topic 5: Choosing Between Them

You needUse
A value the caller setsvariable
A value computed inside the modulelocal
A value returned to the caller or to toolingoutput
A value read from infrastructure you do not managedata
A value read from infrastructure you do manageA direct resource reference

Try it yourself: Take a configuration with a repeated tag block and collapse it into a locals map applied with merge(). Change one tag value and count how many places you edited — one is the target.

Common mistake: Using a variable where a data source belongs. Passing a machine image ID in as a variable means somebody has to look it up and keep it current forever. A data source filtering for the newest matching image removes that job, and the stale-value bug with it.