resource block is the beating heart of Terraform. It is where you declare the physical components you want to exist in the real world—whether that is an AWS EC2 instance, a Google Cloud SQL database, or a GitHub Repository. Let’s build a foundation.1. Anatomy of a Resource Block#
Before we write code, you must understand the anatomical structure of a Terraform resource block. It always follows this strict pattern:
resource "provider_resource_type" "local_logical_name" {
argument_1 = "value_1"
argument_2 = "value_2"
}resource: The keyword instructing Terraform that you want to create a physical object.provider_resource_type: The specific type of object you are creating (e.g.,aws_vpc,aws_instance,google_storage_bucket). This dictates which Provider plugin is used.local_logical_name: An arbitrary name you choose (e.g.,main,web_server). This name is only used internally by Terraform to refer to this block elsewhere in your code. It is not the physical name of the resource in the cloud.- Arguments: The configuration settings specific to that resource type (e.g.,
cidr_block,instance_type).
2. Practice: Building a VPC and Subnet#
Let’s build the foundational networking layer for any AWS architecture: A Virtual Private Cloud (VPC) and a Subnet inside it.
Create a new directory named terraform-resources and open a main.tf file:
mkdir terraform-resources
cd terraform-resourcesPaste the following HCL configuration:
# main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
# 1. Declare the VPC
resource "aws_vpc" "main_network" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "Production-VPC"
Environment = "Prod"
}
}
# 2. Declare a Subnet INSIDE the VPC
resource "aws_subnet" "web_subnet" {
# We reference the ID of the VPC created above dynamically!
vpc_id = aws_vpc.main_network.id
cidr_block = "10.0.1.0/24"
tags = {
Name = "Web-Tier-Subnet"
}
}The Magic of the Implicit Dependency Graph#
Look closely at the aws_subnet block, specifically this line:
vpc_id = aws_vpc.main_network.id
If you were writing a Bash script, you would have to:
- Run the AWS CLI to create the VPC.
- Extract the resulting VPC ID using
jqorgrep. - Pass that extracted ID into the AWS CLI command to create the Subnet.
In Terraform, you simply reference the attribute (.id) of the other resource block (aws_vpc.main_network).
By doing this, you create an Implicit Dependency. Terraform builds a mathematical Directed Acyclic Graph (DAG) in memory. It immediately knows: “I cannot create the Subnet until the VPC exists, because the Subnet requires the VPC’s ID.”
Therefore, even if you put the Subnet block at the very top of the file and the VPC block at the bottom, Terraform will always create the VPC first.
3. Execution and Observation#
Let’s execute this code to see the Dependency Graph in action.
Step 3.1: Initialization#
terraform initStep 3.2: Planning#
terraform planExpected Terminal Output:
Terraform will perform the following actions:
# aws_subnet.web_subnet will be created
+ resource "aws_subnet" "web_subnet" {
+ arn = (known after apply)
+ cidr_block = "10.0.1.0/24"
+ id = (known after apply)
+ vpc_id = (known after apply)
# ... other attributes omitted for brevity
}
# aws_vpc.main_network will be created
+ resource "aws_vpc" "main_network" {
+ arn = (known after apply)
+ cidr_block = "10.0.0.0/16"
+ id = (known after apply)
# ... other attributes omitted for brevity
}
Plan: 2 to add, 0 to change, 0 to destroy.Notice the (known after apply) markers. Because the VPC has not been created yet, AWS has not generated the physical ID (e.g., vpc-01234abcd). Therefore, the Subnet’s vpc_id is also unknown until execution time.
Step 3.3: Applying#
terraform apply -auto-approveExpected Terminal Output:
aws_vpc.main_network: Creating...
aws_vpc.main_network: Creation complete after 2s [id=vpc-0abcdef1234567890]
aws_subnet.web_subnet: Creating...
aws_subnet.web_subnet: Creation complete after 1s [id=subnet-0abcdef1234567890]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.Observe the order of operations in the logs! Terraform created the VPC first, waited 2 seconds for it to finish, captured the ID (vpc-0abcdef1234567890), and instantly injected it into the Subnet creation process.
4. Explicit Dependencies (depends_on)#
What if you have two resources that do not share any data (no attribute references), but Resource B absolutely must wait for Resource A to finish booting up before it can start?
This is where the Meta-Argument depends_on is used to create an Explicit Dependency.
resource "aws_instance" "database_server" {
ami = "ami-12345"
instance_type = "t3.medium"
}
resource "aws_instance" "web_server" {
ami = "ami-12345"
instance_type = "t3.micro"
# Explicitly tell Terraform to wait for the database server
# even though we aren't referencing any of its attributes.
depends_on = [
aws_instance.database_server
]
}Best Practice: Always rely on Implicit Dependencies (attribute referencing) whenever possible. Only use depends_on as a last resort when Terraform cannot organically deduce the relationship.
Troubleshooting & Common Errors#
Reference to undeclared resource- Root Cause: You misspelled the resource type or logical name when trying to reference it. For example, typing
aws_vpc.main_net.idwhen the block is namedmain_network. - Solution: Double-check the exact spelling of the
provider_resource_typeandlocal_logical_namein your reference.
- Root Cause: You misspelled the resource type or logical name when trying to reference it. For example, typing
Cycle: aws_subnet.a depends on aws_vpc.b, aws_vpc.b depends on aws_subnet.a- Root Cause: You accidentally created a circular dependency (an infinite loop). Resource A needs B’s ID, but Resource B needs A’s ID.
- Solution: Break the loop. A Terraform architecture must always resolve to a Directed Acyclic Graph (DAG) with a clear beginning and end.
Conclusion & Next Steps#
You now understand how to declare physical infrastructure and how Terraform organically manages the order of creation via the Implicit Dependency Graph.
However, our current main.tf file has a fatal flaw: The CIDR blocks (10.0.0.0/16) and Tags are hardcoded directly into the resource blocks. If we want to reuse this code for a Staging environment, we would have to manually rewrite the code.
In Episode 4: Input Variables, Outputs, and Data Types, we will parameterize our code to make it dynamic and highly reusable!

