Skip to content
Infrastructure as Code iac concepts 4 min read

Resources

A resource is the most important element in the Terraform language. Every resource block describes exactly one infrastructure object — a virtual machine, a DNS record, an S3 bucket, an IAM policy. When you run Terraform, its single job is to make the real world match the set of resources you declared: creating what’s missing, updating what has changed, and destroying what you removed. Understanding resources is understanding Terraform itself.

The anatomy of a resource block

Every resource is declared with the resource keyword followed by two labels — the type and the name — and a body of arguments enclosed in braces.

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"

  tags = {
    Name = "web-server"
  }
}
  • Resource type (aws_instance) — the kind of object to manage. The prefix (aws_) identifies the provider; the rest names the object. The provider’s schema decides which types exist and which arguments they accept.
  • Local name (web) — your label for this resource within the module. It must be unique per type and is used to reference the resource elsewhere. It has no meaning to the cloud API.
  • Arguments — the key/value pairs that configure the object. Some are required, some optional, and many have provider-set defaults.

This syntax is identical in Terraform 1.5+ and OpenTofu — both consume the same HCL2 configuration and provider plugins, so resource blocks are fully portable between them.

Resource addresses

Terraform identifies each resource instance by a unique address. For a single resource the address is simply type.name:

aws_instance.web

When a resource is expanded with count, instances are addressed by integer index; with for_each, by map key. Addresses become the handle you use on the CLI to target or inspect a specific object.

resource "aws_instance" "worker" {
  count         = 3
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"
}
ConstructExample addressRefers to
Single resourceaws_instance.webThe one instance
countaws_instance.worker[0]First of the indexed instances
for_eachaws_instance.worker["api"]Instance keyed "api"
In a modulemodule.network.aws_vpc.mainA resource inside a child module

You can inspect any address directly from state:

terraform state show aws_instance.web

Referencing attributes between resources

Resources rarely stand alone. You wire them together by referencing one resource’s exported attributes from another’s arguments, using type.name.attribute. These references are how Terraform infers dependencies and the correct order of operations.

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

resource "aws_subnet" "public" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
}

Because aws_subnet.public reads aws_vpc.main.id, Terraform knows the VPC must be created before the subnet. You almost never write explicit ordering — the references express it for you.

Tip: Prefer attribute references over hard-coded IDs. aws_vpc.main.id keeps your config correct even when the underlying resource is recreated and its real ID changes.

How Terraform manages a resource’s lifecycle

For every resource, Terraform compares three things: your configuration, the recorded state, and the real infrastructure. From that comparison it decides on one of four actions.

SymbolActionWhen it happens
+CreateResource is in config but not in state
~Update in placeAn argument changed and the provider can patch it
-/+ReplaceA changed argument forces destruction and recreation
-DestroyResource removed from config but still in state

Running a plan shows exactly what Terraform intends to do:

terraform plan

Output:

Terraform will perform the following actions:

  # aws_instance.web will be created
  + resource "aws_instance" "web" {
      + ami           = "ami-0c55b159cbfafe1f0"
      + instance_type = "t3.micro"
      + id            = (known after apply)
      + private_ip    = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Values marked (known after apply) are attributes the provider only learns once the object actually exists. Applying the plan reconciles reality with your config and writes the result to state:

terraform apply

Output:

aws_instance.web: Creating...
aws_instance.web: Creation complete after 32s [id=i-0abc123def4567890]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Controlling lifecycle behaviour

The nested lifecycle block lets you override Terraform’s default create/replace logic per resource — useful for protecting stateful objects or avoiding downtime.

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"

  lifecycle {
    create_before_destroy = true
    prevent_destroy       = true
    ignore_changes        = [tags["LastScanned"]]
  }
}
  • create_before_destroy — provision the replacement before tearing down the old object (zero-downtime replacement).
  • prevent_destroy — make Terraform refuse any plan that would delete this resource.
  • ignore_changes — treat the listed attributes as managed outside Terraform, suppressing drift on them.

Best practices

  • Give resources clear, role-based local names (web, primary_db) rather than encoding the provider or type, which is already in the labels.
  • Reference attributes (aws_vpc.main.id) instead of hard-coding IDs so dependencies and recreations stay correct automatically.
  • Use for_each over count when managing a set of similar resources — map keys keep addresses stable when the collection changes.
  • Reach for lifecycle blocks sparingly and deliberately; prevent_destroy on databases and create_before_destroy for stateless compute are the common, justified cases.
  • Always review the plan output and the + / ~ / -/+ / - symbols before applying — a -/+ replacement on a stateful resource can mean data loss.
  • Keep every managed object in configuration; resources created by hand outside Terraform won’t appear in state and will drift from your declared intent.
Last updated June 14, 2026
Was this helpful?