Not everything Terraform needs to know about already lives in your configuration. Your VPC might be created by another team, your AMI IDs change every week, and your account ID is something you’d rather look up than hardcode. Data sources are how Terraform reads that existing, externally-managed information and pulls it into your configuration — and they show up on the HashiCorp Terraform Associate exam more often than most candidates expect.
This guide covers data sources from a practitioner’s perspective: what a data block actually does, how it differs from a resource, how filtering and dependencies work, and the specific behaviors — refresh timing, terraform_remote_state, depends_on — that the exam likes to test. If you’re still assembling the fundamentals, start with the Terraform Associate Exam Guide 2026 and the Terraform Workflow: init, plan, apply & destroy, then come back here.
What a Data Source Actually Is
A data source lets Terraform read information that it does not manage. Where a resource block tells Terraform “create and own this thing,” a data block says “go find this thing that already exists and give me its attributes.”
The distinction is the whole mental model:
| Aspect | resource block | data block |
|---|---|---|
| Purpose | Create, update, delete | Read only |
| Lifecycle | Terraform owns it | Terraform never modifies it |
| Appears in plan as | + create / ~ update / - destroy | Read (no changes to real infra) |
| State | Tracked and managed | Cached for reference |
| Keyword | resource "type" "name" | data "type" "name" |
Data sources are provided by the same providers that offer resources. The AWS provider offers aws_instance (resource) and aws_ami (data source); the terraform_remote_state data source ships with Terraform itself. If a provider isn’t configured, its data sources won’t work — see Terraform Providers & the Provider Block.
The Anatomy of a data Block
Here’s the canonical example the exam expects you to recognize — looking up the latest Ubuntu AMI instead of hardcoding an ID that will be stale next month:
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id # reference the data source
instance_type = "t3.micro"
}
Three things to lock in:
- The syntax is
data "<TYPE>" "<NAME>" { ... }. The arguments inside are query constraints — they describe what to look for, not what to create. - You reference it with the
data.prefix:data.aws_ami.ubuntu.id. Forgetting thedata.prefix is one of the most common exam-question traps —aws_ami.ubuntu.id(withoutdata.) refers to a resource that doesn’t exist. - The result is a set of read-only attributes (
id,arn,creation_date, etc.) you can use anywhere you’d use a value.
Data Sources in the plan and apply Lifecycle
A frequent exam question is: when does a data source get read? The behavior changed subtly across Terraform versions, and understanding it prevents “why is my plan showing (known after apply)?” confusion.
- If Terraform can resolve all of a data source’s arguments at plan time (they’re constants or already-known values), it reads the data during
terraform planand the results are known immediately. - If a data source’s arguments depend on values that won’t be known until apply (for example, an argument that references an attribute of a resource being created in the same run), Terraform defers the read until
apply. In the plan you’ll see the data source’s results shown as(known after apply).
# This data source depends on a resource created in the same apply,
# so Terraform defers reading it until apply time.
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
data "aws_subnets" "in_vpc" {
filter {
name = "vpc-id"
values = [aws_vpc.main.id] # not known until the VPC is created
}
}
The practical takeaway for the exam: data sources are refreshed on every plan/apply to keep their values current, and if a data source can’t be resolved until apply, that’s expected behavior, not an error.
Filtering and Narrowing Results
Many data sources can match multiple objects. The exam tests whether you know how to narrow a query to exactly one result — because a data source that expects a single object but matches many (or zero) will error out.
Common narrowing mechanisms:
filterblocks (AWS and others): match on tags, names, attributes.most_recent = true: when several match, pick the newest (e.g. AMIs).- Specific identifiers: passing an exact
id,name, ortagsmap.
# Look up an existing VPC by tag — must resolve to exactly one
data "aws_vpc" "selected" {
tags = {
Environment = "production"
}
}
# Then use its attributes
resource "aws_subnet" "app" {
vpc_id = data.aws_vpc.selected.id
cidr_block = "10.0.1.0/24"
}
If this query matched two production VPCs, Terraform would fail with a “multiple matching” error. Singular data sources (aws_vpc) expect one match; plural ones (aws_vpcs, aws_subnets) return a list of IDs and never error on count. Knowing which is which is a classic exam distinction.
Referencing Existing, Unmanaged Infrastructure
The most common real-world reason to reach for a data source is to consume infrastructure that another team, another Terraform configuration, or a click-ops process created. You don’t want to import it and take ownership — you just need to read a few attributes.
# Read the default AWS account details without managing anything
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
output "account_id" {
value = data.aws_caller_identity.current.account_id
}
output "region" {
value = data.aws_region.current.name
}
aws_caller_identity and aws_region are the two data sources you’ll see constantly — they let modules stay portable instead of hardcoding account IDs and regions. This connects directly to writing reusable modules; see Terraform Modules: The Complete Guide for how data sources keep modules environment-agnostic.
terraform_remote_state: Sharing Data Between Configurations
terraform_remote_state is the one data source the exam almost always touches, because it’s how separate Terraform configurations share information. Suppose a “network” configuration creates the VPC and subnets, and a separate “app” configuration needs those IDs. Rather than copy-pasting, the app config reads the network config’s outputs from its state:
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "network/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_instance" "app" {
# Reference an OUTPUT exported by the network configuration
subnet_id = data.terraform_remote_state.network.outputs.private_subnet_id
}
Critical exam facts about terraform_remote_state:
- It can only read values that the other configuration explicitly declared as outputs. Anything not output is invisible — it cannot reach arbitrary resource attributes in the remote state.
- The
backendandconfigdescribe where the other state lives, mirroring that configuration’s backend block. For a refresher on backends, see Terraform State Management: Remote Backends, Locking & State Commands. - On modern Terraform, the outputs are accessed via
.outputs.<name>.
This pattern enforces a clean boundary: the network team decides what to expose, the app team consumes only that.
Controlling When a Data Source Reads: depends_on
Usually Terraform infers ordering automatically — if a data source references aws_vpc.main.id, Terraform knows to create the VPC first. But sometimes a dependency is implicit and Terraform can’t see it. For example, a data source that queries objects created by a resource it doesn’t directly reference.
resource "aws_iam_role_policy" "example" {
# ... grants permission that the data source below needs
}
data "aws_iam_policy_document" "combined" {
# This read only succeeds AFTER the policy above is applied,
# but nothing links them — so declare it explicitly.
depends_on = [aws_iam_role_policy.example]
# ...
}
depends_on on a data source forces Terraform to wait until the named resources are created (and therefore reads the data at apply time rather than plan time). Use it sparingly — only when there’s a real ordering requirement Terraform can’t infer. Overusing depends_on pushes reads to apply and produces noisier plans. For the broader meta-argument picture, see Terraform Meta-Arguments, Lifecycle & Provisioners.
Data Sources with count and for_each
Data sources support count and for_each just like resources, which is how you look up many objects at once:
variable "azs" {
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
# One data lookup per availability zone
data "aws_subnet" "selected" {
for_each = toset(var.azs)
availability_zone = each.value
vpc_id = data.aws_vpc.selected.id
}
# Reference: data.aws_subnet.selected["us-east-1a"].id
With count, you index by number (data.aws_subnet.selected[0].id); with for_each, you index by key. Choosing between them follows the same rules as resources — see Terraform count vs for_each: Which to Use and When.
data vs resource: The Exam’s Favorite Comparison
Expect at least one question that hinges on knowing what a data source cannot do. Commit these to memory:
| Statement | True for data? |
|---|---|
| Creates infrastructure | No |
| Modifies or deletes infrastructure | No |
Appears in terraform destroy as something to remove | No (it’s only read) |
| Refreshed to current values on each plan | Yes |
| Can be referenced by resources | Yes |
| Can reference resource attributes | Yes |
| Requires a configured provider | Yes |
A data source never shows up as +, ~, or - in a plan’s change count — it’s purely a read. If an exam answer claims a data source “creates” or “manages” something, it’s wrong by definition.
Common Mistakes with Data Sources
Real-world (and exam) errors cluster around a handful of patterns:
- Dropping the
data.prefix when referencing —aws_ami.ubuntu.idinstead ofdata.aws_ami.ubuntu.id. - Ambiguous queries — a singular data source matching zero or multiple objects, which errors instead of returning a list.
- Expecting to read non-output values through
terraform_remote_state— only declared outputs are accessible. - Overusing
depends_on, forcing reads to apply time and muddying plans. - Assuming data is cached forever — it’s re-read on every run, so a value that changes upstream will change your plan.
Watching for these in practice questions is the fastest way to internalize the behavior. The Terraform Associate Practice Questions set includes data-source scenarios that drill exactly these distinctions.
Putting It Together
Data sources are Terraform’s read side: they let a configuration stay dynamic and decoupled by pulling in AMI IDs, VPCs, account details, and the outputs of other configurations instead of hardcoding them. For the Terraform Associate exam, focus on four things:
- The
data "type" "name"syntax and thedata.reference prefix. - Read-only semantics — no create, update, or destroy, refreshed every run.
terraform_remote_statereads outputs only from another configuration.depends_onand unresolved arguments defer the read to apply time.
Get comfortable writing these by hand, because on the exam you’ll need to spot a malformed reference or a “data sources can modify infrastructure” distractor in seconds.
Practice Data Sources Before Exam Day
Understanding data sources on paper is one thing; recognizing a broken data. reference or a terraform_remote_state question under a ticking clock is another. The most reliable way to build that instinct is repeated, exam-style practice with detailed explanations.
The HashiCorp Terraform Associate (004) Mock Exam Bundle covers data sources alongside every other exam objective — state management, modules, the configuration language, and the CLI workflow — with realistic questions and thorough explanations that reinforce the why behind each answer. Pair it with the Terraform Associate 30-Day Study Plan to move from “I’ve read about data sources” to “I can spot the trick in any question.” If you’re still deciding whether the cert is worth it, read Is the Terraform Associate Worth It in 2026? first.
Frequently Asked Questions
What is the difference between a resource and a data source in Terraform?
A resource block creates and manages real infrastructure — Terraform owns its full lifecycle (create, update, destroy). A data block only reads information about existing infrastructure or provider metadata; Terraform never modifies what a data source references. Data sources appear as reads in a plan, never as changes.
When does Terraform read a data source?
On every plan and apply, to keep the values current. If all of a data source’s arguments are known at plan time, it’s read during plan; if its arguments depend on resources being created in the same run, the read is deferred to apply and shown as (known after apply).
What is terraform_remote_state used for?
It lets one Terraform configuration read the outputs of another by pointing at that configuration’s state backend. It’s the standard way to share values (like VPC or subnet IDs) between separate configurations. Only values explicitly declared as outputs are accessible — arbitrary resource attributes are not.
Can a data source modify or create infrastructure?
No. Data sources are strictly read-only. They query existing objects and expose their attributes for reference. If an exam answer suggests a data source creates, updates, or destroys anything, it’s incorrect.
Why do I need depends_on on a data source?
When a data source depends on a resource but doesn’t directly reference any of its attributes, Terraform can’t infer the ordering. Adding depends_on forces the read to wait until those resources are created, which moves the read to apply time. Use it only when a real hidden dependency exists.
How do I filter a Terraform data source to a single result?
Use filter blocks, exact identifiers, tags, or most_recent = true to narrow the query. Singular data sources (like aws_vpc) must match exactly one object or they error; plural ones (like aws_vpcs) return a list of matches and don’t error on count.