Resource and data blocks
Managed resources and data sources
Two block types carry almost all of a Terraform configuration, and on the page they look nearly alike:
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
}
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
}
The difference is ownership. A resource block declares a managed resource, a real object Terraform creates, updates, and destroys[1] so infrastructure matches your configuration. A data block declares a data source, a read-only lookup that fetches data from the provider without creating or modifying anything[2]. In the example Terraform builds and owns the aws_instance, but only reads the aws_ami that someone else published.
Two labels form the address
Both blocks open with a keyword and two labels, a type and a local name. In resource "aws_instance" "web", the type aws_instance comes from the provider and fixes what the object is, while the local name web is yours to choose. Together they form the block's address, aws_instance.web, which Terraform uses to track the object in state[1] and which must be unique within the module. The local name is only an internal handle: it does not need to be globally unique, and it never has to match an id the provider assigns on create.
The data. prefix is the read-only tell
You wire blocks together by referencing their exported attributes. A managed resource's attribute reads as aws_instance.web.id; a data source's has the same shape with a leading data. marker, data.aws_ami.ubuntu.id. That data. prefix is the single syntactic signal that a reference points at a read-only lookup rather than a managed object. Drop it and Terraform looks for a managed resource of that name instead, so the prefix, not the block's position in the file, is what tells configuration and reader alike which kind of block owns a value.
Meta-arguments: count, for_each, and the rest
Every resource and data block accepts a fixed set of meta-arguments that Terraform interprets itself, independent of the provider. Learn them once and they behave the same on any resource type.
| Meta-argument | What it does |
|---|---|
depends_on | Forces an explicit ordering when Terraform cannot infer it from references |
count | Creates a fixed number of instances, indexed from 0 |
for_each | Creates one instance per map entry or set member, keyed by that element |
provider | Selects a non-default (aliased) provider configuration |
lifecycle | Tunes create, replace, and destroy behavior (create_before_destroy, prevent_destroy, ignore_changes) |
The reference lists exactly these five for a resource block[1]; a data block accepts the same set, plus precondition and postcondition checks inside its lifecycle.
count indexes; for_each keys
count and for_each both stamp out multiple instances from one block, but they identify those instances differently, and you cannot use both in the same block[3].
counttakes a whole number[3] and creates that many instances. Inside the blockcount.indexis the position starting at 0, and you address an instance by number:aws_instance.web[0].for_eachtakes a map or a set of strings[4] and creates one instance per element. Inside the blockeach.keyandeach.valuedescribe the current element, and you address an instance by its key:aws_instance.web["blue"].
count.index is available only under count, and each.key / each.value only under for_each; reaching for the wrong one is a common configuration error.
Why for_each survives edits that count churns
Prefer for_each when instances have their own identity. Because for_each tracks instances by key, deleting the middle entry of a three-item map removes exactly that one instance. Under count, deleting the middle of a three-item list shifts every later instance down one index, and since state is keyed by that index, Terraform plans to destroy and recreate each shifted instance[3]. Switching an existing resource from count to for_each re-addresses every instance the same way, so pair the switch with a moved block to keep the objects instead of replacing them.
How and when data sources are read
A data source's value is only as fresh as its last read, so knowing when Terraform reads it explains most data-source surprises. The rule has two branches.
Terraform reads a data source during the refresh phase[2], the read-back step at the start of plan and apply where Terraform checks real objects against its records, when all of the data source's arguments are already known, for example a hard-coded owner id or a plain variable. The result is available during plan, so the plan can show what the lookup returned.
Terraform defers the read to the apply phase[2] when at least one argument refers to a value it cannot predict during planning, such as an attribute of a resource being created in the same run. Until apply produces that value, the data source and everything downstream of it read as (known after apply).
Reads repeat on every run
A data source is not read once and cached forever. Terraform re-reads each data source on the refresh of every plan and apply, so it always reflects the current real-world value. A managed resource behaves differently: Terraform changes it only when your configuration or detected drift calls for a change. That is the practical run-time split between the two block types. The data source re-observes reality each run, while the resource is reconciled toward your configuration.
Provisioners and terraform_data: the last resort
Reach for a provisioner only after every declarative option is exhausted. HashiCorp designs Terraform for immutable infrastructure and recommends purpose-built solutions for post-apply steps[5], because a provisioner runs an opaque imperative command that Terraform cannot model, plan, or reconcile. When you do need one, the type you choose decides where the command runs.
Three provisioner types, two locations
local-execinvokes an executable on the machine running Terraform[8] after the resource is created; it needs no network connection to the resource.remote-execruns commands on the remote resource itself[6] and therefore requires aconnectionblock giving Terraform SSH or WinRM access.filecopies files or directories from the Terraform machine to the new resource[9]; likeremote-execit reaches the target through aconnectionblock. It rounds out the built-in triad, and despite the plain name it acts on the remote host, not on local disk.
Creation-time by default, destroy-time on request
Provisioners run at creation by default. Set when = destroy to run one during terraform destroy instead. Failure handling differs by phase, and the default catches people out:
- A creation-time provisioner that fails marks the resource as tainted[5], so the next
terraform applydestroys and recreates it rather than leaving a half-configured object. The defaulton_failureisfail, which aborts the apply;on_failure = continueinstead tells Terraform to ignore the error and proceed. - A destroy-time provisioner that fails does not taint anything: Terraform returns an error and reruns the provisioner on the next
terraform apply[5].
terraform_data: a resource to hang provisioners on
A provisioner must live inside a resource, so what do you do when no real resource fits? Use terraform_data, a built-in managed resource that follows the standard lifecycle without managing any real object[7]. It is always available through the built-in provider terraform.io/builtin/terraform, with no required_providers entry or provider block[7], and it is the modern replacement for the null provider's null_resource. Its optional input argument stores a value that Terraform echoes to the computed output attribute (unknown during plan, equal to input after apply), and setting triggers_replace forces the instance to be replaced when that value changes, which you can chain into another resource's lifecycle { replace_triggered_by = [...] }.
Exam-pattern recognition
The exam rarely asks for a definition outright. It shows a snippet or a scenario and asks which block, which meta-argument, or which command fits. The recurring patterns:
- A stem shows configuration that reads an object that already exists (an AMI, a VPC, the current account) and asks how to reference it. The answer is a
datablock; distractors propose aresourceblock, which would try to create or take over the object. A data source cannot create or destroy anything. - A stem references
aws_instance.web.idon a resource that setscountand asks why the plan errors. Withcountthe name is a list, so you must index it:aws_instance.web[0].id. Thefor_eachcounterpart uses a string key,aws_instance.web["blue"].id, never a numeric index. - A stem sets both
countandfor_eachon one block. That is always invalid: the two are mutually exclusive. - A stem omits the
data.prefix when referencing a data source, or asks which prefix distinguishes a data source from a managed resource. Thedata.prefix is the answer. - A stem needs to run a shell command or copy a file onto a new server. The answer is a provisioner, but the correct framing is that it is a last resort:
local-execfor the Terraform host,remote-execorfilefor the server through aconnectionblock. An option offering a provider resource or instance user data is often the better real-world choice the question is steering you toward. - A stem wants to run provisioners but has no natural resource to attach them to, or mentions replacing
null_resource. The answer isterraform_data, which needs no provider configured. - A stem describes a provisioner that failed during creation and asks about the resource's state. It is tainted, so the next
terraform applydestroys and recreates it;on_failure = continueis the only setting that would have let the apply proceed.
resource block vs data block
| Aspect | resource block | data block |
|---|---|---|
| Purpose | Manage a real object | Read an existing object |
| Lifecycle actions | Create, update, destroy | Read only, never writes |
| When it runs | Planned, then applied | Refresh phase; deferred to apply if inputs are unknown |
| Reference form | aws_instance.web.id | data.aws_ami.ubuntu.id |
| Meta-arguments | depends_on, count, for_each, provider, lifecycle | Same set, plus precondition and postcondition checks |
Decision tree
Sharp facts the exam loves — give these one last read before exam day.
Cheat sheet
Sharp facts the exam loves — scan these before test day.
- Resource block has two labels
A
resourceblock requires two labels: the resource type (e.g.aws_instance) and a local name (e.g.web). Together they form the resource addressaws_instance.web, which must be unique within the module.Trap Believing the local name must be globally unique or must match a provider-assigned identifier.
3 questions test this
- In Terraform's configuration language a resource is declared with the keyword `resource` followed by two quoted labels, as in `resource "aws_instance" "web" { ... }`. What do these two labels specify,
- Within one module, an engineer writes `resource "aws_instance" "web" { ... }` and, in the same configuration, `resource "aws_s3_bucket" "web" { ... }`, reusing the local name `web` for both blocks. A
- An engineer declares a single compute instance with the block `resource "aws_instance" "web" { ... }` and later needs to reference that instance's exported attributes from an `output` value elsewhere
- Resource meta-arguments
In addition to provider-defined arguments, a
resourceblock accepts the meta-argumentsdepends_on,count,for_each,provider, andlifecycle.countandfor_eachare mutually exclusive within the same block.Trap Using both
countandfor_eachin one resource block (Terraform errors).3 questions test this
- To create several storage buckets, an engineer adds both `count = 3` and `for_each = var.bucket_names` to the same `resource` block, expecting Terraform to combine the two. When the engineer runs `ter
- Two resources in a module have a hidden relationship: an application server must be created only after a monitoring agent's IAM role finishes provisioning, yet the server's configuration never referen
- A platform engineer is refactoring a resource block so that, during future updates, Terraform provisions a replacement instance before tearing down the current one, and so that no plan can ever destro
- Managed resource lifecycle
A managed
resourceblock directs Terraform to create, update, or destroy real infrastructure so it matches the configuration.3 questions test this
- A new team member asks what a managed `resource` block actually instructs Terraform to do when they run `terraform apply` against it. Which statement best describes the purpose of a managed resource b
- An engineer deletes an `aws_instance` resource block that was previously applied and whose real compute instance still exists and is recorded in state. Leaving the rest of the configuration unchanged,
- A platform engineer is refactoring a resource block so that, during future updates, Terraform provisions a replacement instance before tearing down the current one, and so that no plan can ever destro
- count vs. for_each resource meta-arguments
The count meta-argument takes a whole number and creates that many instances addressed by count.index (starting at 0), while for_each takes a map or a set of strings and creates one instance per element keyed by each.key/each.value; you cannot set both count and for_each on the same block. for_each is preferred when instances need stable identity, because instances are tracked by map/set key rather than by positional index, so adding or removing a middle element does not force the shift-and-replace churn that count produces.
Trap count.index is unavailable inside a for_each block (and each.key/each.value are unavailable with count); switching a resource from count to for_each changes every instance address and forces replacement unless you add a moved block.
3 questions test this
- To create several storage buckets, an engineer adds both `count = 3` and `for_each = var.bucket_names` to the same `resource` block, expecting Terraform to combine the two. When the engineer runs `ter
- An engineer manages five virtual machines with `count = 5` and wants to switch to `for_each` keyed by hostname so that removing one machine from the middle of the list no longer reshuffles the others.
- An engineer wants one storage bucket per name in a variable `bucket_names` that is typed as `list(string)`, so they write `for_each = var.bucket_names` directly on the resource block. Terraform return
- Data blocks are read-only
A data source is declared
data "<TYPE>" "<NAME>" {}; Terraform performs only read operations on it, fetching information about existing objects without creating or modifying infrastructure.4 questions test this
- A team needs their Terraform configuration to look up the ID of an existing, externally managed VPC and feed it into a new security group, without provisioning or taking ownership of the VPC itself. W
- A root configuration contains one `resource "aws_instance" "app"` block and one `data "aws_ami" "base"` block that looks up the AMI the instance uses. The engineer runs `terraform destroy` and confirm
- During a design review, a colleague asks why the team uses a `data` block rather than a `resource` block to obtain the ID of a pre-existing VPC that a different team already provisions and owns. Which
- A platform engineer adds a `data "aws_ami" "ubuntu" {}` block to a configuration that already manages several EC2 instances. During code review, a teammate worries that running `terraform apply` will
- When data sources are read
Terraform reads a data source during the plan/refresh phase when its arguments are known, but defers the read until apply when the data block depends on values not yet known (for example attributes of resources changing in the same plan).
Trap Assuming data sources are always read at apply time, or always fully resolved before plan.
4 questions test this
- A `data "aws_ami" "latest"` block selects the most recent AMI matching a fixed filter. Between two runs of `terraform plan`, a newer matching AMI is published in the cloud account. With every argument
- A platform engineer declares `data "aws_vpc" "primary" {}` that looks up an existing VPC using only fixed literal filter values, with no reference to any managed resource in the configuration. A teamm
- An engineer's `data "aws_ami" "ubuntu"` block filters solely on hard-coded literal values - a fixed name pattern and a fixed owner ID - with no `depends_on` set. All of the block's arguments are there
- An engineer writes a `data "aws_subnet" "selected"` block whose lookup argument references `aws_instance.app.id` - an instance being created in the very same `terraform plan`. Because that argument is
- Data source reference syntax
Data source attributes are referenced as
data.<TYPE>.<NAME>.<ATTRIBUTE>; the leadingdata.prefix distinguishes them from managed resource references.Trap Omitting the
data.prefix and referencing the source like a managed resource.2 questions test this
- A child module declares `data "aws_subnet" "selected" {}` and must expose that subnet's `cidr_block` attribute to its parent module through an `output` block. Which reference belongs in the output blo
- In one configuration, an engineer has both a managed resource `aws_vpc.main` and a data source declared `data "aws_vpc" "main"` - the same type and the same name label. Which pair of expressions refer
- CRUD ownership differs
A managed
resourceblock owns the full create/update/delete lifecycle of the object, while adatablock only reads an existing object and never manages or destroys it.Trap Thinking a data source can provision or destroy infrastructure.
7 questions test this
- An engineer needs Terraform to create a brand-new load balancer, keep it updated in place as the configuration evolves, and destroy it when it is removed from the configuration. Which block type decla
- A configuration contains a `data` block that looks up an existing, externally managed database instance so its endpoint can be passed to an application resource. A colleague worries that running `terr
- A colleague created a storage account by hand in the cloud console and now wants Terraform to fully own it, updating it when the configuration changes and destroying it when it is removed. Another eng
- A teammate claims that Terraform reconciles a `data` block and a managed `resource` block on exactly the same schedule during a run. How does the update cadence of a data source actually differ from t
- In Terraform, a `resource` block and a `data` block both describe objects that live in a provider, yet they play fundamentally different roles in a configuration. Which statement correctly describes h
- A platform team already has a production VPC that was created by hand in the cloud console. They want their new Terraform configuration to reference that VPC's ID when defining subnets, but Terraform
- New to Terraform, an engineer writes a `data` block for a cloud storage bucket, expecting that the next `terraform apply` will provision a brand-new bucket for the team. When the apply runs, what does
- Data sources re-read each run
Terraform re-reads each data source during the refresh step of every plan/apply so it reflects the current real-world value, whereas a managed resource is only changed when configuration or state drift requires it.
5 questions test this
- A `data` block reads a value from a shared platform that a different team updates several times a week. Between two runs nobody edits the Terraform files or the data block's arguments. During the next
- A `data` block reads an existing DNS record whose value is maintained outside Terraform, and a managed resource references that data source's output. Someone edits the DNS record directly in the provi
- A teammate claims that Terraform reconciles a `data` block and a managed `resource` block on exactly the same schedule during a run. How does the update cadence of a data source actually differ from t
- During a normal `terraform plan` in which a `data` block's arguments are all already known, at what point does Terraform query that data source, and why does the timing matter?
- A `data` block looks up the most recent machine image published by a platform team. Weeks after the last apply, that team publishes a newer image. Without changing any Terraform files, the engineer no
- provisioners: local-exec vs. remote-exec
A local-exec provisioner runs a command on the machine running Terraform, whereas a remote-exec provisioner runs commands on the newly created remote resource and therefore requires a connection block (SSH or WinRM). Provisioners run at creation time by default, or during destroy when when = destroy is set, and HashiCorp guidance is to use them only as a last resort after purpose-built alternatives.
Trap A failed creation-time provisioner marks the resource as tainted so it is recreated on the next apply; destroy-time provisioners still require a valid connection at destroy time.
8 questions test this
- A new team is deciding how heavily to rely on provisioners for post-apply configuration of their infrastructure. According to HashiCorp's official guidance, what posture should they take toward provis
- A platform engineer is choosing between the local-exec and remote-exec provisioners for a post-apply step attached to a single resource, and needs to recall the essential difference between the two. W
- A team configures a remote-exec provisioner with when = destroy on a server resource so that a graceful-shutdown script runs on the host just before Terraform tears it down. For this destroy-time prov
- A team adds a provisioner to a resource and wants it to run automatically the first time that resource is created, and they include no when argument at all. At what point in the resource's lifecycle d
- A platform engineer adds a provisioner to a resource so that, immediately after the resource is created, a shell command runs on the same CI runner that is executing Terraform to update a local invent
- An engineer must run a CLI command on the very machine executing Terraform right after a load balancer is created, to register the load balancer's DNS name with an external monitoring service. Which p
- During terraform apply, a destroy-time provisioner (when = destroy) attached to a resource being removed exits non-zero while running its cleanup script. Unlike a failed creation-time provisioner, how
- An engineer adds a remote-exec provisioner to a compute-instance resource to run a package-install command on the new instance, but terraform apply fails because Terraform has no way to reach the host
- Provisioner failure taints the resource
A creation-time provisioner that exits non-zero fails the apply and marks the resource as tainted, so Terraform destroys and recreates it on the next terraform apply; because Terraform cannot predictably model the changes a provisioner makes, recreation is used instead of a partial repair. The on_failure argument overrides this: the default (fail) aborts the apply, while on_failure = continue makes Terraform ignore the error and proceed.
Trap Assuming a failed provisioner leaves a usable resource, or that continue is the default (it is not — fail is). Destroy-time provisioners behave differently: a failure returns an error and Terraform re-runs the provisioner on the next terraform apply rather than tainting.
5 questions test this
- During terraform apply, a remote-exec provisioner attached to a newly created server exits with a non-zero status while running a bootstrap script, and no on_failure argument is set on the provisioner
- A one-off local-exec provisioner runs a best-effort notification command that occasionally exits non-zero, and the engineer does not want that failure to abort an otherwise-successful terraform apply.
- During terraform apply, a destroy-time provisioner (when = destroy) attached to a resource being removed exits non-zero while running its cleanup script. Unlike a failed creation-time provisioner, how
- An engineer's remote-exec provisioner exits non-zero midway through configuring a freshly created VM, and the provisioner has no on_failure argument set. Focusing only on the current terraform apply t
- A junior engineer asks why, after a creation-time provisioner fails and taints a resource, Terraform destroys and recreates the whole resource on the next apply instead of simply re-running the provis
- The file provisioner
The file provisioner copies files or directories from the machine where Terraform is running to the newly created resource, using either a source path or inline content plus a destination path on the target. Like remote-exec, it reaches the resource through a connection block over SSH or WinRM.
Trap file completes the provisioner-type triad (local-exec, remote-exec, file); a common misconception is treating file as a generic data/config feature or forgetting it still requires a connection block — unlike local-exec it acts on the remote host, not the Terraform machine.
- terraform_data built-in resource (null_resource replacement)
The built-in terraform_data managed resource (added in Terraform 1.4, provider source terraform.io/builtin/terraform, requiring no provider block or required_providers entry) is the modern replacement for the null provider's null_resource: it implements the standard resource lifecycle without managing any real infrastructure object. Its optional input argument stores an arbitrary value that is echoed to the computed output attribute (unknown during plan, equal to input after apply), and it can host standalone provisioners that have no other logical managed resource to attach to. Setting its triggers_replace argument forces the terraform_data instance to be replaced whenever that value changes, so referencing the instance from another resource's lifecycle { replace_triggered_by = [terraform_data.] } propagates a forced replacement onto that dependent resource.
Trap Assuming terraform_data still needs the hashicorp/null provider (a required_providers entry or provider block) the way null_resource did — it does not; terraform_data is always available through Terraform's built-in provider, so no provider is declared or configured.
References
- Resource blocks (Terraform language)
- Data sources (Terraform language)
- The count meta-argument (Terraform language)
- The for_each meta-argument (Terraform language)
- Provisioners (Terraform language)
- remote-exec provisioner (Terraform language)
- The terraform_data managed resource (Terraform language)
- local-exec provisioner (Terraform language)
- file provisioner (Terraform language)