Terraform · Part 8

Terraform — Part 8 — Data Sources vs Resources

Every resource block so far has created something new. This part covers the other half of reading infrastructure: data sources, which look up information about things Terraform doesn’t manage and never will.

Quick idea: a resource block creates and owns something; a data block only looks something up. Confusing the two is one of the more common newcomer mistakes.
resource

Creates, owns, and can destroy the object it describes.

data

Reads information about something that already exists. Never creates, changes, or destroys it.

Refresh

The step where Terraform queries data sources, normally as part of building a plan.

Introduction

Every resource block from Part 4 onward has created something Terraform then owns and tracks in state. Real infrastructure is rarely that self-contained — a new server usually needs to reference an existing base image, an existing network, or an existing DNS zone that some other process created and manages. Terraform needs a way to read that information without pretending it created it.

What a Data Source Is

Think of a resource block as placing an order — Terraform is responsible for what comes back and for cancelling it later if needed. A data block is more like looking something up in a catalogue you don’t control: Terraform reads the current value, but has no ownership over it, no ability to change it, and no responsibility to clean it up.

A data source fetches information about an existing object — often something outside Terraform’s management entirely, like an AMI published by a cloud provider, or an existing resource managed by a completely separate Terraform configuration.

The data Block

Syntactically, a data block looks almost identical to a resource block — same two labels, type and local name — with a different keyword:

data "<TYPE>" "<LABEL>" {
  <PROVIDER-SPECIFIC ARGUMENTS>
}

Reference its result elsewhere with data.<TYPE>.<LABEL>.<ATTRIBUTE> — the same reference shape from Part 4, with a leading data. to distinguish it from a resource reference.

A Worked Example

A common real pattern: looking up the latest Ubuntu AMI published by Canonical, then using it to launch an instance, instead of hard-coding an AMI ID that will eventually go stale:

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]
  }

  owners = ["099720109477"]
}

resource "aws_instance" "app_server" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
}

most_recent = true tells the data source to resolve to the newest AMI matching the filter rather than an arbitrary one. owners restricts the search to a specific publisher’s account — here, Canonical’s well-known AWS account ID — so the lookup can’t accidentally resolve to a similarly-named image from an untrusted source. The resource block then references data.aws_ami.ubuntu.id exactly the way it would reference another resource’s attribute.

Key point: Terraform never creates, modifies, or deletes the AMI here. If Canonical publishes a newer image tomorrow, the next plan simply picks it up — there’s nothing in state tying this configuration to a specific AMI the way there would be for a resource it actually owns.

When Data Is Read

By default, Terraform reads data sources during the refresh step that runs automatically before building a plan — so the values are already resolved by the time you see the plan’s proposed changes.

If a data source’s arguments depend on a value Terraform can’t know until something else is actually applied — a resource attribute that doesn’t exist yet, for instance — Terraform defers reading that data source until the apply phase instead, and says so explicitly in the plan output.

Data Source vs Resource

Aspect resource data
Ownership Terraform creates, tracks, and can destroy it. Terraform only reads it. Never creates or destroys.
In state Tracked with full lifecycle metadata. Cached value only, re-checked on each refresh.
On terraform destroy Removed. Unaffected — nothing to destroy.
Typical use Anything this configuration is responsible for. Anything owned elsewhere that this configuration needs to reference.

The Pitfall Worth Knowing

Because a data source is re-read on every refresh, its value can change between one apply and the next without a single line of your configuration changing — the underlying object simply changed outside Terraform’s control. An AMI lookup with no owners filter, or too loose a name filter, can silently resolve to a different image next month and rebuild a resource that depended on it.

Practical rule: scope data source filters as tightly as the AMI example above — specific owner, specific name pattern — so a lookup resolves predictably instead of drifting underneath you.

Quick Reference Summary

Term Meaning
data block Reads information about an existing object without owning it.
data.TYPE.LABEL.ATTRIBUTE Reference syntax for a data source’s result.
Refresh The step where data sources are normally read, before a plan is built.
Deferred read When a data source depends on an unknown value, Terraform reads it during apply instead.

Final Thoughts

Data sources are what let a configuration stay honest about what it actually manages. Reaching for a resource block when you only need to read something invites Terraform to try to own infrastructure it has no business owning; reaching for a data source keeps the boundary clean.

Key takeaway: if a configuration needs to create, change, or eventually destroy something, that’s a resource. If it just needs to know a value about something that already exists elsewhere, that’s a data source.
Next in this series

Terraform — Part 9 — Meta-Arguments: count, for_each, depends_on, lifecycle. The four features Part 4 previewed, covered properly, including the trade-offs between count and for_each newcomers most often get wrong.