6.1 Resource Blocks vs Data Blocks
Key Takeaways
- A resource block manages an object — Terraform creates, updates, and destroys it; a data block only reads an object that already exists
- Syntax is resource "TYPE" "NAME" versus data "TYPE" "NAME"; addresses are TYPE.NAME and data.TYPE.NAME
- Data sources still use a provider and usually run their reads during plan or refresh, not as a destroy action
- You cannot create or destroy a remote object through a data source; terraform destroy leaves queried objects alone
- Locals are named expressions inside the module; they are not data sources and do not query a provider
6.1 Resource Blocks vs Data Blocks
Quick Answer: A
resourceblock tells Terraform to manage an object — create it, update it, and destroy it. Adatablock only reads an object that already exists. Writeresource "TYPE" "NAME"versusdata "TYPE" "NAME". Data sources still use a provider and usually run those reads during plan or refresh. You cannot destroy anything through a data source. Locals are not data sources.
Objective 4a on Terraform Associate (004) is: use and differentiate resource and data blocks. The product version on the exam is Terraform 1.12. HashiCorp's 004 content list points at the 1.12 Resources and Data Sources pages. The language references you should be able to quote are the resource block and Query infrastructure data.
Why 004 splits these two blocks
Both blocks talk to a provider. Both export attributes you can reference. Both accept count, for_each, depends_on, and the provider meta-argument. The exam still treats them as different tools because their lifecycle is different:
- A managed resource is an object Terraform is responsible for. Apply creates it if it is missing, updates it when configuration no longer matches, and destroys it if you remove the block or run destroy.
- A data source is a read. Terraform asks the provider for current information and exposes the result. It does not create the remote object, does not update it, and cannot destroy it.
If you can say that pair of sentences without hedging, you have the 4a answer. The rest of this section is how that difference shows up in syntax, plan output, and the classic aws_ami → aws_instance pairing.
Syntax you must recognize on sight
data "aws_ami" "al2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.al2023.id
instance_type = "t3.micro"
}
| Piece | Resource | Data source |
|---|---|---|
| Keyword | resource | data |
| Type string | Provider resource type, such as aws_instance | Provider data source type, such as aws_ami |
| Label (name) | Local name in this module, such as web | Local name in this module, such as al2023 |
| Address | aws_instance.web | data.aws_ami.al2023 |
| What apply does | Create, update, or destroy the object | Nothing to the remote object; attributes already came from a read |
| What destroy does | Destroys the managed object | Leaves the remote object alone |
The type plus the label must be unique among resource blocks, and separately unique among data blocks. You can legally write resource "aws_instance" "web" and data "aws_instance" "web" in the same module because the data. prefix keeps the addresses distinct. That pair is a favorite 004 distractor: the data block is reading some other instance that already exists. It is not a second way to manage aws_instance.web.
The label is not the cloud name. Terraform uses the label to track the object in state. The AWS Name tag, the Azure resource name, or the DNS record name is a provider argument you set in the body. Official resource-block language: the label does not affect settings on the actual infrastructure object.
Data sources still use a provider
A data source is not a Terraform-core lookup table. The provider implements the read. data "aws_ami" uses whatever AWS provider configuration applies — the default unaliased provider "aws" block, or an alias you select with provider = aws.west. If that provider is not initialized, the data source cannot run. terraform init is required for data sources just as it is for managed resources.
Terraform 1.12 also ships a built-in data source, terraform_remote_state, that reads another workspace's root outputs. It is still a data block (data "terraform_remote_state" "net"). It is not a locals block and it is not a managed resource.
Some data sources are specialized utilities that generate a value for the current run: data "local_file" reads a file on disk, and data "aws_iam_policy_document" renders a JSON policy. They still do not create the thing you are describing. HashiCorp's wording is blunt: Terraform can only perform read operations on data sources.
When the read happens
HashiCorp's data-source documentation for the 1.12 line is explicit about timing:
- Terraform tries to query data sources during planning.
- When every argument is already known — literals, input variables, or locals that do not depend on a resource this plan is about to create — Terraform reads during the refresh that precedes plan by default. The plan then shows the real AMI ID, the real VPC CIDR, the real hosted-zone ID.
- When an argument refers to a value that is unknown until apply — typically a managed resource this plan will create or change — Terraform defers the read until apply. The data source's attributes then also appear as
(known after apply), and anything that consumes them cannot be fully planned.
That deferral is why a data source that looks up a subnet in a VPC you are creating in the same run is legal but noisy: the instance that needs the looked-up ID cannot show a concrete value until apply. Prefer passing a managed resource's own exported attributes when you created the object in this configuration. Use a data source when the object already exists outside this configuration's management.
You cannot force a destroy through a data source. Removing a data block drops the read from configuration and from state. The AMI, the existing VPC, and the remote state you queried are still there.
The exam pair: aws_ami into aws_instance
004 loves this pairing because it is the smallest complete story:
data "aws_ami" "al2023"asks the AWS provider for an image that already exists in the account or in Amazon's catalog.resource "aws_instance" "web"takesami = data.aws_ami.al2023.id.- Apply creates the instance. Destroy terminates the instance. Neither operation deletes the AMI.
Hardcoding ami = "ami-0c55b159cbfafe1f0" works until that image is deprecated. The data source keeps the configuration dynamic without making Terraform manage the AMI. The same pattern appears on every provider: data.azurerm_client_config.current.tenant_id, data.google_compute_image.debian.self_link, or data.aws_vpc.existing.id feeding resource "aws_subnet" "app".
Locals are not data sources
locals {
instance_type = "t3.micro"
name_prefix = "${var.project}-${var.environment}"
}
A locals block names an expression so you can reuse it. It does not query a provider, it does not appear as data.local..., and terraform destroy has nothing to destroy. You reference it as local.instance_type — singular local, not locals. Teams sometimes stash a looked-up ID in a local (local.ami_id = data.aws_ami.al2023.id) for reuse. That local is still just an alias. The read happened in the data source.
| Construct | Queries a provider? | Creates or destroys? | Reference |
|---|---|---|---|
resource | Yes — write and follow-up read | Yes | TYPE.NAME.ATTR |
data | Yes — read only | No | data.TYPE.NAME.ATTR |
locals | No | No | local.NAME |
variable | No | No | var.NAME |
Meta-arguments both blocks share — and the ones they do not
Both blocks accept count, for_each (those two are mutually exclusive), depends_on, provider, and lifecycle preconditions or postconditions. Data blocks do not take provisioners, connection, create_before_destroy, prevent_destroy, or ignore_changes. Those exist to control a managed object's lifecycle. A read has nothing to prevent-destroy.
depends_on on a data source is a real 004 edge: it forces Terraform to finish the listed resource operations before the read. Use it when the thing you are querying is created in the same configuration but the data source arguments do not reference it directly. Prefer a real attribute reference when you can; implicit dependencies are clearer.
004 traps for objective 4a
datais not a read-only resource you can later convert withmoved. HashiCorp documents that you cannot move a managed resource into a data resource.- Writing
resource "aws_ami"does not look up an AMI. If the provider exposes a managedaws_amitype, that block would try to create an AMI. terraform destroydoes not delete objects you only read.- A successful plan that prints an AMI ID is not proof Terraform created that AMI.
localvalues are not cached data sources and do not survive as remote objects.
On Terraform 1.12, what is the primary difference between a resource block and a data block?
When does Terraform typically read a data source whose arguments are all already known?
A module looks up Amazon Linux 2023 with data "aws_ami" "al2023" and sets ami = data.aws_ami.al2023.id on resource "aws_instance" "web". Which statement is true?