Terraform Resources, Data Sources, Variables, and Outputs
Resource blocks create and manage infrastructure; data blocks only read infrastructure that already exists. That single distinction anchors most of objective 4 on the Terraform Associate (004) exam. Around those two block types sit the plumbing that makes a configuration reusable: input variables parameterize values coming in, output values expose results going out, and attribute references such as aws_instance.web.id wire resources together while silently building Terraform's dependency graph. The exam tests whether you can declare each block correctly, reference it with the right address prefix (resources get none, data sources are prefixed with data., variables with var.), supply variable values through files, flags, and environment variables in the correct precedence order, and choose the right type constraint, from a simple string up to a map of objects. This lesson covers all of it with short, real HCL you can read in one pass, including the variable-definition precedence list the exam loves to scramble.
On this page7 sections
- Resource blocks create and manage infrastructure
- Data sources read infrastructure you do not manage
- Attribute references create implicit dependencies
- Input variables parameterize a configuration
- The variable-definition precedence order
- Output values expose results
- Complex types: collections and structural types
- Differentiate resource and data blocks and state what each one does to real infrastructure
- Reference resource and data source attributes to create implicit cross-resource dependencies
- Declare input variables with type constraints, defaults, sensitive flags, and custom validation
- Recite the variable-definition precedence order Terraform uses when the same variable is set in multiple places
- Declare output values, mark them sensitive, and consume them from the root module and child modules
- Choose the correct complex type: list, set, map, object, or tuple
Resource blocks create and manage infrastructure
A resource block declares a piece of infrastructure that Terraform creates, updates, and destroys on your behalf. It is the most important block type in the language: everything Terraform manages in state corresponds to a resource. Each block has two labels, the resource type and a local name, and the pair must be unique within a module.
resource "aws_instance" "web" {
ami = "ami-0c02fb55956c7d316"
instance_type = "t3.micro"
tags = {
Name = "web-server"
}
}The resource type's prefix tells Terraform which provider manages it: aws_instance belongs to the aws provider, google_compute_instance to google, and random_pet to random. Elsewhere in the configuration you refer to this resource by the address aws_instance.web — the word resource never appears in the reference.
Arguments inside the block configure the resource; which arguments exist is defined by the provider's schema, not by Terraform core. After an apply, the resource also exposes attributes that the provider computes, such as aws_instance.web.public_ip, which you can read even though you never set them.
Data sources read infrastructure you do not manage
A data block reads information from outside the current configuration — an object managed by another Terraform workspace, created by hand, or published by the provider itself. Data sources never create, modify, or destroy anything; Terraform refreshes them during planning and exposes the result as read-only attributes.
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
}References to data sources are prefixed with data. — here data.aws_ami.ubuntu.id. Forgetting that prefix (or adding it to a managed resource) is a classic exam trap.
| resource block | data block | |
|---|---|---|
| Effect on infrastructure | Creates, updates, destroys | Read-only lookup |
| Tracked in state | Yes, as a managed object | Yes, but only the cached read result |
| Reference prefix | None (type.name) | data.type.name |
| Typical use | Infrastructure you own | Existing AMIs, VPCs, secrets, account IDs |
Attribute references create implicit dependencies
Referencing one resource's attribute inside another resource is how you wire infrastructure together, and it does double duty: the reference itself tells Terraform the order in which to create things. This is called an implicit dependency, and it is the preferred way to express ordering.
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "app" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
subnet_id = aws_subnet.app.id
}Because aws_subnet.app references aws_vpc.main.id, Terraform builds the VPC first, then the subnet, then the instance — with no explicit ordering configuration at all. During a destroy, the graph runs in reverse. The general reference form is <resource_type>.<name>.<attribute>, and you can drill into nested values with further dots and index brackets.
Values that a provider only knows after creation (like an instance ID) are shown as (known after apply) in the plan. Terraform still resolves the graph correctly; it simply defers the concrete value until apply time.
Input variables parameterize a configuration
An input variable is declared with a variable block and referenced as var.<name>. Variables are how the same configuration serves multiple environments without editing the source.
variable "instance_type" {
type = string
description = "EC2 instance type for the web tier"
default = "t3.micro"
}
variable "db_password" {
type = string
sensitive = true
}
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}Key arguments to know for the exam: type constrains what values are accepted; default makes the variable optional; description documents it; sensitive = true redacts the value from plan and apply output; and one or more validation blocks reject bad values at plan time with your own error_message. Since Terraform 1.9, a validation condition may also reference other variables in the module, not just the one being validated.
A variable with no default and no supplied value causes Terraform to prompt interactively — or fail in automation — so required inputs are simply variables without defaults.
The variable-definition precedence order
Terraform loads variable values from several sources, and later sources override earlier ones. From lowest to highest precedence:
- Environment variables named
TF_VAR_<name>(for exampleTF_VAR_instance_type=t3.small) - The
terraform.tfvarsfile, if present - The
terraform.tfvars.jsonfile, if present - Any
*.auto.tfvarsor*.auto.tfvars.jsonfiles, processed in lexical order of their filenames -varand-var-fileoptions on the command line, in the order they are provided (highest precedence)
A default in the variable block sits beneath all of these — it applies only when no source supplies a value. Note the direction: the environment variable is the weakest definition source, and a -var flag on the CLI beats everything. Exam questions frequently set the same variable in two or three places and ask which value wins.
terraform apply -var="instance_type=m5.large" -var-file="prod.tfvars"
One more file-loading rule worth memorizing: terraform.tfvars and *.auto.tfvars files load automatically, but any other .tfvars file (like prod.tfvars above) is only used when passed explicitly with -var-file.
Output values expose results
An output block exposes a value from a configuration after apply. In the root module, outputs print at the end of terraform apply and are queryable with terraform output; in a child module, outputs are the only values a parent can read, via module.<name>.<output>.
output "instance_ip" {
description = "Public IP of the web server"
value = aws_instance.web.public_ip
}
output "db_connection_string" {
value = "postgres://app:${var.db_password}@${aws_instance.web.private_ip}/app"
sensitive = true
}Marking an output sensitive = true redacts it from CLI output — Terraform prints <sensitive> instead of the value. If an output's value derives from a sensitive variable, Terraform forces you to mark the output sensitive too, or the plan errors. Redaction is display-only: the real value still lives in the state file, and terraform output -raw db_connection_string or terraform output -json will reveal it to anyone with state access.
Outputs commonly feed other systems: a parent module composing child modules, a script parsing terraform output -json, or another Terraform configuration reading them through remote state.
Complex types: collections and structural types
Terraform's type system starts with three primitives — string, number, and bool — and builds two families of complex types on top of them.
Collection types hold many values of one element type:
list(string)— ordered, indexed from zero, duplicates allowed:var.zones[0]set(string)— unordered, unique values, no index access (convert withtolistif you need one)map(string)— key/value pairs accessed asvar.tags["team"]
Structural types hold a fixed shape of possibly different types:
object({...})— named attributes, each with its own typetuple([...])— a fixed-length sequence where each position has its own type
variable "servers" {
type = map(object({
instance_type = string
monitoring = bool
disk_gb = optional(number, 20)
}))
}
variable "endpoint" {
type = tuple([string, number])
default = ["api.example.com", 443]
}The optional() modifier (with an optional default) lets object attributes be omitted by callers. There is also any, a placeholder that tells Terraform to infer the type — convenient, but it gives up the plan-time checking that explicit constraints provide. On the exam, expect to distinguish map from object (uniform element type vs named mixed attributes) and list from tuple (uniform vs per-position types), and to remember that sets are unordered.
Tip. Expect questions that make you differentiate resource and data blocks, including the data. reference prefix and the fact that data sources never modify infrastructure. The variable-definition precedence order is heavily tested, usually as a scenario where the same variable is set by an environment variable, a tfvars file, and a -var flag. You should also be able to spot valid variable and output syntax, know what sensitive = true does and does not protect, and match a described value shape to the correct complex type.
- resource blocks create and manage infrastructure; data blocks only read existing infrastructure and never change it.
- Resources are referenced as type.name (aws_instance.web); data sources take the data. prefix (data.aws_ami.ubuntu).
- Referencing another resource's attribute, such as aws_subnet.app.id, creates an implicit dependency that orders operations automatically.
- Variable blocks support type, default, description, sensitive, and validation; a variable with no default is required input.
- Variable precedence from lowest to highest: TF_VAR_ environment variables, terraform.tfvars, terraform.tfvars.json, *.auto.tfvars in lexical order, then -var and -var-file flags.
- Outputs are the only way a parent module reads values from a child module, accessed as module.name.output_name.
- sensitive = true redacts values from CLI output only — variables and outputs are still stored in plaintext in the state file.
- Know the type families: list/set/map are collections of one element type; object and tuple are structural types with per-attribute or per-position types.
Frequently asked questions
What is the difference between a resource block and a data block in Terraform?
A resource block declares infrastructure that Terraform creates, updates, and destroys, and it is fully managed in state. A data block performs a read-only lookup of infrastructure that already exists outside the configuration, such as an AMI, a VPC, or an account ID. Data sources are refreshed during planning and can never modify real infrastructure; they are referenced with a data. prefix, while managed resources are referenced with no prefix.
Which value wins when a Terraform variable is set in multiple places?
The last-loaded source wins. From lowest to highest precedence: TF_VAR_ environment variables, then terraform.tfvars, then terraform.tfvars.json, then any *.auto.tfvars files in lexical filename order, and finally -var and -var-file command-line options in the order given. A default in the variable block is only used when no source provides a value at all.
Are sensitive variables and outputs encrypted in the Terraform state file?
No. The sensitive flag only redacts values from plan and apply output in the CLI. Inside the state file, and inside saved plan files, sensitive values are stored in plaintext. That is why protecting state — with an access-controlled, encrypted remote backend — is the real security control, and why sensitive is a display safeguard rather than an encryption feature.
How does a parent module read values from a child module?
Only through the child module's declared outputs. If a child module defines output "vpc_id", the parent references it as module.network.vpc_id. Resources inside a child module are not directly addressable from the parent, so anything the parent needs must be deliberately exposed as an output.
When should I use an object type instead of a map?
Use map(type) when every value has the same type and the keys are arbitrary, like a set of tags. Use object({...}) when you need a fixed set of named attributes that can each have a different type, like an instance_type string alongside a monitoring bool. The same logic separates list (uniform element type, any length) from tuple (fixed length, per-position types).
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.