What is a Resource? #

In the Concept section, you were introduced to resources as the basic unit of Terraform configuration. In this section, we go deeper — how resources interact with each other, how Terraform manages their lifecycle, and how to write robust resources for production infrastructure. This article builds the foundation for the deeper topics in this section: lifecycle, dependency, and operation.

Resource vs Data Source #

One of the most common confusions in Terraform is the difference between resource and data. Both are configuration blocks, but their roles are very different.

flowchart LR
    subgraph "resource (Manage)"
        R1["aws_vpc.main"] --> R2["Terraform CREATES\nthis resource"]
        R2 --> R3["Terraform UPDATES\nif the config changes"]
        R3 --> R4["Terraform DELETES\nif destroyed"]
    end

    subgraph "data (Read)"
        D1["aws_ami.ubuntu"] --> D2["Terraform READS\nsomething that exists"]
        D2 --> D3["Cannot be\ncreated/deleted"]
        D3 --> D4["Only a reference\nfor other resources"]
    end

    style R1 fill:#e3f2fd,stroke:#1565c0
    style D1 fill:#e8f5e9,stroke:#2e7d32
# resource — Terraform CREATES and MANAGES this
# Terraform is responsible for its lifecycle: create, update, destroy
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "main-vpc"
  }
}

# data — Terraform READS this, doesn't create it
# The resource already exists outside Terraform, you just want to reference it
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]  # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"]
  }
}

# Using both together
resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id  # AMI from the data source
  instance_type = "t3.micro"
  vpc_id        = aws_vpc.main.id         # VPC from the resource
}
Aspectresourcedata
PurposeCreate and manage new resourcesRead existing resources
LifecycleCreate, update, destroyNone
In state?✅ Yes, stored and tracked✅ Yes, but only as a cache
Created by Terraform?✅ Yes❌ No, already exists outside
ExamplesCreate VPC, EC2, S3Read an AMI, read an existing VPC

Complete Resource Block Anatomy #

flowchart TD
    A["Resource Block"] --> B["Required Arguments\n(ami, instance_type)"]
    A --> C["Optional Arguments\n(subnet_id, tags)"]
    A --> D["Nested Blocks\n(tags, root_block_device)"]
    A --> E["Meta-Arguments\n(depends_on, count,\nfor_each, provider,\nlifecycle)"]

    B --> B1["Mandatory\nProvided by the provider"]
    C --> C1["Optional\nProvided by the provider"]
    D --> D1["Nested structure\nProvided by the provider"]
    E --> E1["Provided by Terraform\nApplies to ALL\nresources"]

    style A fill:#e3f2fd,stroke:#1565c0
    style E fill:#fff3e0,stroke:#e65100
    style E1 fill:#fff3e0,stroke:#e65100
# Complete resource block structure with all its parts

resource "<PROVIDER>_<TYPE>" "<NAME>" {
  # ─────────────────────────────────────────
  # ARGUMENTS — configuration specific to this resource
  # ─────────────────────────────────────────
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.micro"

  # Nested blocks
  tags = {
    Name        = "web-server"
    Environment = var.environment
  }

  # ─────────────────────────────────────────
  # META-ARGUMENTS — apply to all resources
  # ─────────────────────────────────────────

  # Explicit dependency
  depends_on = [aws_iam_role_policy_attachment.node]

  # Create multiple instances
  count = 3
  # or: for_each = var.instance_map

  # Use a non-default provider
  provider = aws.us_east

  # Customize the lifecycle
  lifecycle {
    create_before_destroy = true
    prevent_destroy       = false
    ignore_changes        = [tags["LastModified"]]
  }
}

Meta-arguments (depends_on, count, for_each, provider, lifecycle) are special arguments recognized by Terraform itself — not provider arguments. They apply to all resources, regardless of type or provider.


How Resources Are Represented in State #

Every resource created by Terraform is stored in state with its unique identity. Understanding this structure helps when you need to interact with state directly.

flowchart TD
    A["State File"] --> B["Resource Type\naws_instance"]
    B --> C["Resource Name\nweb"]
    C --> D["Resource Address\naws_instance.web"]
    D --> E["Attributes\nid, ami, ip, tags"]

    F["Resource + count"] --> G["&quot;aws_instance.workers[0&quot;"]
    F --> H["&quot;aws_instance.workers[1&quot;"]
    F --> I["&quot;aws_instance.workers[2&quot;"]

    J["Resource + for_each"] --> K["aws_subnet.public[&quot;a&quot;]"]
    J --> L["aws_subnet.public[&quot;b&quot;]"]
    J --> M["aws_subnet.public[&quot;c&quot;]"]

    style D fill:#e3f2fd,stroke:#1565c0
    style G fill:#e8f5e9,stroke:#2e7d32
    style K fill:#e8f5e9,stroke:#2e7d32
# View all resources in state
terraform state list

# Output:
# aws_instance.web
# aws_instance.workers[0]
# aws_instance.workers[1]
# aws_instance.workers[2]
# module.vpc.aws_vpc.main
# module.vpc.aws_subnet.public["ap-southeast-1a"]
# View details of one resource in state
terraform state show aws_instance.web

# Output:
# # aws_instance.web:
# resource "aws_instance" "web" {
#     ami                    = "ami-0abcdef1234567890"
#     id                     = "i-0abcdef1234567890"
#     instance_type          = "t3.micro"
#     private_ip             = "10.0.1.42"
#     public_ip              = "54.123.45.67"
#     ... (all attributes, including the ones AWS generated)
# }

Resource addresses in state follow the format: <type>.<name> for regular resources, <type>.<name>[index] for resources with count, and <type>.<name>["key"] for resources with for_each.

Resource PatternAddress FormatExample
Regular resource<type>.<name>aws_instance.web
With count<type>.<name>[index]aws_instance.workers[0]
With for_each<type>.<name>["key"]aws_subnet.public["a"]
Inside a modulemodule.<name>.<type>.<name>module.vpc.aws_vpc.main

Resource Addresses and How to Reference Them #

Every resource has an address that can be referenced from elsewhere in the configuration.

flowchart LR
    A["Cross-resource\nreferences"] --> B["Format:\ntype.name.attribute"]

    B --> C["aws_vpc.main.id\n(id of the VPC)"]
    B --> D["aws_subnet.public.id\n(id of the subnet)"]
    B --> E["aws_instance.web.public_ip\n(IP of the instance)"]

    F["Splat expressions"] --> G["&quot;aws_subnet.private[*&quot;].id\nAll subnet IDs at once"]

    style B fill:#e3f2fd,stroke:#1565c0
# Reference format: <TYPE>.<NAME>.<ATTRIBUTE>

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "public" {
  vpc_id = aws_vpc.main.id  # Reference to the "id" attribute of aws_vpc.main
}

# For resources with count:
resource "aws_subnet" "private" {
  count  = 3
  vpc_id = aws_vpc.main.id
  cidr_block = "10.0.${count.index + 10}.0/24"
}

resource "aws_route_table_association" "private" {
  count          = 3
  subnet_id      = aws_subnet.private[count.index].id  # Access per index
  route_table_id = aws_route_table.private.id
}

# All subnets at once using a splat expression:
output "private_subnet_ids" {
  value = aws_subnet.private[*].id  # ["subnet-001", "subnet-002", "subnet-003"]
}

Consistent Writing Patterns #

Consistency in how you write resources makes configurations easier to read and review.

# RECOMMENDED PATTERNS:

resource "aws_instance" "web" {
  # 1. Required arguments (mandatory) at the top
  ami           = var.ami_id
  instance_type = var.instance_type

  # 2. Important optional arguments
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.web.id]
  iam_instance_profile   = aws_iam_instance_profile.web.name

  # 3. Nested blocks
  root_block_device {
    volume_size = 20
    volume_type = "gp3"
    encrypted   = true
  }

  # 4. Tags always at the end
  tags = merge(
    var.common_tags,
    {
      Name = "web-server"
      Role = "web"
    }
  )

  # 5. Lifecycle at the very end (if any)
  lifecycle {
    create_before_destroy = true
  }
}


Resource Addressing #

Resource addressing enables specific references to particular resources.

# Format: module.module_name.resource_type.resource_name[index]

# Root level resource
aws_instance.web

# Resource in a module
module.networking.aws_vpc.main

# Resource in a count
aws_instance.web[0]
aws_instance.web[1]

# Resource in a for_each
aws_instance.web["app-server"]
aws_instance.web["db-server"]

# Nested module
module.networking.module.subnets.aws_subnet.private[0]
# Resource reference for dependencies
resource "aws_instance" "app" {
  subnet_id = module.networking.private_subnet_ids[0]
  # Implicit dependency on module.networking
}

Resource Metadata #

# Every resource has metadata you can access

resource "aws_instance" "web" {
  ami           = "ami-12345"
  instance_type = "t3.micro"
  
  tags = {
    Name = "web-server"
  }
}

# Available metadata:
# aws_instance.web.id           → Instance ID
# aws_instance.web.arn          → ARN
# aws_instance.web.private_ip   → Private IP
# aws_instance.web.public_ip    → Public IP (if any)
# aws_instance.web.availability_zone → AZ
# aws_instance.web.instance_state → State (running, stopped)
# View all attributes of a resource
terraform state show aws_instance.web
# Shows ALL attributes stored in state

Resource Lifecycle States #

RESOURCE STATES:

1. pending_create: Will be created on apply
2. creating: Currently being created
3. created: Already created (active)
4. pending_update: Will be updated on apply
5. updating: Currently being updated
6. pending_destroy: Will be deleted on apply
7. destroying: Currently being deleted
8. destroyed: Already deleted

TERRAFORM PLAN ICONS:
+  Create (new resource)
~  Update (changed resource)
-  Destroy (deleted resource)
-/+ Replace (destroy + create)
<= Read (data source)
# View a resource's state
terraform state show aws_instance.web

# List all resources in state
terraform state list

# List with a filter
terraform state list | grep aws_instance

Summary #

  • resource creates, data reads — resources are managed by Terraform, data sources are only read from what already exists.
  • Meta-arguments (depends_on, count, for_each, provider, lifecycle) apply to all resources, not provider arguments.
  • Resource addresses in state follow the <type>.<name> format — important to know for terraform state operations and cross-resource references.
  • References use the <type>.<name>.<attribute> format — this is what builds Terraform’s automatic dependency graph.
  • Splat expressions ([*]) access all instances of a count-based resource at once.
  • Writing consistency — required args first, then optional, nested blocks, tags, lifecycle — makes reviews easier.

← Previous: Drift Detection   Next: Operation →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact