If you've written even a small Terraform configuration, you've already worked with resources—they’re the core building blocks of everything Terraform does. No resources, no infrastructure. Simple as that.
But once you move beyond basic examples, things get more nuanced: dependencies, lifecycle rules, dynamic creation, and managing drift. That’s where understanding Terraform resources properly starts to pay off.
What is a Terraform Resource?
A Terraform resource represents a piece of infrastructure. It could be a virtual machine, a database, a DNS record, or even something abstract like an IAM policy.
At its simplest, a resource block looks like this:
1resource "aws_instance" "web_server" {
2 ami = "ami-0c55b159cbfafe1f0"
3 instance_type = "t2.micro"
4}
5There are three parts to pay attention to:
- Type:
aws_instance(defined by the provider) - Name:
web_server(your local identifier) - Configuration: the attributes inside the block
Terraform combines the type and name into a unique identifier: aws_instance.web_server.
Starting with a Real Example
Let’s say you want to spin up an EC2 instance and attach a security group.
1resource "aws_security_group" "web_sg" {
2 name = "web-sg"
3 description = "Allow HTTP traffic"
4
5 ingress {
6 from_port = 80
7 to_port = 80
8 protocol = "tcp"
9 cidr_blocks = ["0.0.0.0/0"]
10 }
11}
12
13resource "aws_instance" "web" {
14 ami = "ami-0c55b159cbfafe1f0"
15 instance_type = "t2.micro"
16
17 vpc_security_group_ids = [aws_security_group.web_sg.id]
18}
19Here’s where things get interesting: Terraform automatically understands that the EC2 instance depends on the security group because of the reference aws_security_group.web_sg.id.
No need to explicitly define dependencies in most cases.
Resource Arguments vs Attributes
A common point of confusion: not everything inside a resource block behaves the same way.
- Arguments: values you define (like
instance_type) - Attributes: values Terraform exposes after creation (like
id,public_ip)
You can reference attributes like this:
1output "instance_ip" {
2 value = aws_instance.web.public_ip
3}
4Creating Multiple Resources Dynamically
Hardcoding multiple resource blocks gets messy quickly. Terraform gives you two main tools: count and for_each.
Using count
1resource "aws_instance" "app" {
2 count = 3
3 ami = "ami-0c55b159cbfafe1f0"
4 instance_type = "t2.micro"
5}
6This creates three identical instances. You can reference them using indexes:
1aws_instance.app[0].idUsing for_each
More flexible and safer when dealing with dynamic sets:
1variable "instance_names" {
2 default = ["api", "worker", "frontend"]
3}
4
5resource "aws_instance" "app" {
6 for_each = toset(var.instance_names)
7
8 ami = "ami-0c55b159cbfafe1f0"
9 instance_type = "t2.micro"
10
11 tags = {
12 Name = each.value
13 }
14}
15This avoids index-shifting issues that can happen with count.
Lifecycle Rules: Controlling Resource Behavior
Sometimes the default create/update/destroy behavior isn’t enough. That’s where lifecycle comes in.
1resource "aws_instance" "example" {
2 ami = "ami-123456"
3 instance_type = "t2.micro"
4
5 lifecycle {
6 create_before_destroy = true
7 prevent_destroy = true
8 ignore_changes = [tags]
9 }
10}
11What these do:
- create_before_destroy: avoids downtime during replacement
- prevent_destroy: protects critical resources
- ignore_changes: ignores external modifications
A common mistake developers make is skipping lifecycle rules for production systems—this can lead to accidental downtime or data loss.
Implicit vs Explicit Dependencies
Terraform usually infers dependencies automatically, but sometimes you need to be explicit.
1resource "aws_instance" "web" {
2 ami = "ami-123456"
3 instance_type = "t2.micro"
4
5 depends_on = [aws_s3_bucket.logs]
6}
7Use depends_on sparingly—overusing it can make your configuration harder to reason about.
Handling Resource Drift
Infrastructure changes outside Terraform (like manual console edits) create drift.
Terraform detects this during plan and tries to reconcile it. But depending on your setup, this can result in:
- Unexpected updates
- Resource replacement
- Failed applies
To stay safe:
- Run
terraform planfrequently - Avoid manual changes in production
- Use
ignore_changesselectively
Resource Naming and Organization
Readable Terraform code matters more than people think—especially in teams.
Instead of this:
1resource "aws_instance" "x1" {}Prefer something meaningful:
1resource "aws_instance" "backend_api_server" {}Also consider grouping related resources into modules once things grow beyond a single file.
When Resources Become Complex
As your infrastructure scales, resource definitions often grow with:
- Nested blocks (e.g., networking rules)
- Dynamic blocks
- Conditional expressions
At that point, splitting configurations into modules isn’t just cleaner—it becomes necessary.
A Quick Mental Model
Think of Terraform resources as:
"Declarative blueprints that describe the desired state, not the steps to get there."
You don’t tell Terraform how to create an EC2 instance—you describe what it should look like, and Terraform figures out the execution plan.
Wrapping It Up
Terraform resources are deceptively simple at first glance, but they carry a lot of power once you start layering dependencies, lifecycle rules, and dynamic configurations.
If you get comfortable with:
- Writing clean resource blocks
- Managing dependencies correctly
- Using lifecycle rules wisely
…you’ll avoid most of the common pitfalls teams run into when scaling Terraform.
And honestly, that’s where Terraform starts to feel less like a tool—and more like a reliable system for managing infrastructure at scale.