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
Last updated: August 2026

6.1 Resource Blocks vs Data Blocks

Quick Answer: A resource block tells Terraform to manage an object — create it, update it, and destroy it. A data block only reads an object that already exists. Write resource "TYPE" "NAME" versus data "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_amiaws_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"
}
PieceResourceData source
Keywordresourcedata
Type stringProvider resource type, such as aws_instanceProvider data source type, such as aws_ami
Label (name)Local name in this module, such as webLocal name in this module, such as al2023
Addressaws_instance.webdata.aws_ami.al2023
What apply doesCreate, update, or destroy the objectNothing to the remote object; attributes already came from a read
What destroy doesDestroys the managed objectLeaves 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:

  1. Terraform tries to query data sources during planning.
  2. 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.
  3. 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:

  1. data "aws_ami" "al2023" asks the AWS provider for an image that already exists in the account or in Amazon's catalog.
  2. resource "aws_instance" "web" takes ami = data.aws_ami.al2023.id.
  3. 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.

ConstructQueries a provider?Creates or destroys?Reference
resourceYes — write and follow-up readYesTYPE.NAME.ATTR
dataYes — read onlyNodata.TYPE.NAME.ATTR
localsNoNolocal.NAME
variableNoNovar.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

  • data is not a read-only resource you can later convert with moved. 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 managed aws_ami type, that block would try to create an AMI.
  • terraform destroy does not delete objects you only read.
  • A successful plan that prints an AMI ID is not proof Terraform created that AMI.
  • local values are not cached data sources and do not survive as remote objects.
Loading diagram...
Resource blocks manage; data blocks only read
Test Your Knowledge

On Terraform 1.12, what is the primary difference between a resource block and a data block?

A
B
C
D
Test Your Knowledge

When does Terraform typically read a data source whose arguments are all already known?

A
B
C
D
Test Your Knowledge

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?

A
B
C
D