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.
resource block creates and owns something; a data block only looks something up. Confusing the two is one of the more common newcomer mistakes.
Creates, owns, and can destroy the object it describes.
Reads information about something that already exists. Never creates, changes, or destroys it.
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.
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.
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.
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.