Study guide

6.1 Resource Blocks vs Data Blocks

HashiCorp Certified: Terraform Associate 004

Track your progress as you read

Finished sections get a check mark.

1 Introduction & Exam Overview

1.1 Current Exam Facts 1.2 Format, Delivery, Retakes, and Recertification 1.3 004 Changes, Terraform 1.12, and Study Plan

2 Infrastructure as Code with Terraform

2.1 What Infrastructure as Code Is 2.2 Advantages of IaC Patterns 2.3 Multi-Cloud, Hybrid, and Service-Agnostic Workflows

3 Terraform Fundamentals: Providers & State

3.1 Install and Version Terraform Providers 3.2 How Terraform Uses Providers 3.3 Configuration with Multiple Providers 3.4 How Terraform Uses and Manages State

4 Core Terraform Workflow: Init & Validate

4.1 The Terraform Write–Plan–Apply Workflow 4.2 Initialize a Working Directory 4.3 Validate a Configuration

5 Plan, Apply, Destroy, and Formatting

5.1 Generate and Review an Execution Plan 5.2 Apply Changes to Infrastructure 5.3 Destroy Terraform-Managed Infrastructure 5.4 Formatting and Style Adjustments

6 Resources, Data Sources, and References

6.1 Resource Blocks vs Data Blocks 6.2 Resource Attributes and Cross-Resource References 6.3 Resource Addressing and Interpolation Patterns

7 Variables, Outputs, and Complex Types

7.1 Input Variables, tfvars, and Precedence 7.2 Output Values 7.3 Complex Types: list, map, object, tuple, set

8 Dynamic Configuration and Dependencies

8.1 Expressions, count, for_each, and dynamic Blocks 8.2 Built-in Functions 8.3 Dependencies, depends_on, and Lifecycle Rules

9 Conditions, Secrets, and Sensitive Data

9.1 Custom Conditions: validation, precondition, postcondition, check 9.2 Sensitive Data Handling 9.3 Vault, Ephemeral Values, and Write-Only Arguments

10 Terraform Modules

10.1 How Terraform Sources Modules 10.2 Variable Scope within Modules 10.3 Using Modules in Configuration 10.4 Managing Module Versions

11 Terraform State Management

11.1 The Local Backend 11.2 State Locking 11.3 Remote State and the backend Block 11.4 Resource Drift and State Operations

12 Maintain Infrastructure with Terraform

12.1 Import Existing Infrastructure 12.2 Inspect State with the CLI 12.3 Verbose Logging and Diagnostics

13 HCP Terraform

13.1 Create Infrastructure with HCP Terraform 13.2 Collaboration and Governance Features 13.3 Workspaces and Projects 13.4 HCP Terraform Integrations

6.1 Resource Blocks vs Data Blocks

Key Takeaways Explain This Topic AI

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_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" takes ami = 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

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