Anatomy of a resource block
resource "aws_s3_bucket" "demo" {bucket = "my-bucket-12345"
tags = {Owner = "student"
}
}
There are four things to notice here:
- The
resourcekeyword tells Terraform "this is an entity to create." - The resource type
"aws_s3_bucket"is what you are creating. The type comes from the provider. The AWS provider has 800+ types:aws_s3_bucket,aws_instance,aws_iam_role, and so on. - The resource name
"demo"is your internal label. You use it to reference this resource from other parts of HCL:aws_s3_bucket.demo.id. - The block body holds the arguments. Some are required (
bucket), some are optional (tags).
The name exists only inside Terraform. In AWS the bucket is named my-bucket-12345 (the value of the bucket argument). demo is just like a variable in code.
Arguments vs attributes
These two words are easy to mix up.
- Arguments are what you write in HCL. You supply them:
bucket = "...",region = "...". - Attributes are what the cloud returns after creation. You read them:
aws_s3_bucket.demo.id,aws_s3_bucket.demo.arn.
Some attributes are known immediately (the values you wrote as arguments). Others are available only after apply (for example, id, arn, creation_date). Before apply, the plan shows them as (known after apply).
Resource address
Every resource has a unique address within the project: type.name.
aws_s3_bucket.demo # the resource itself
aws_s3_bucket.demo.bucket # its "bucket" attribute
aws_s3_bucket.demo.arn # its "arn" attribute
The address is stable across runs. If you rename "demo" to "main", Terraform treats that as "delete the old one, create a new one," even if nothing changed in the cloud. To avoid recreation on rename, use the moved {} block (advanced).
References between resources
The most useful pattern is linking resources together:
resource "aws_s3_bucket" "demo" {bucket = "my-bucket-12345"
}
resource "aws_s3_bucket_versioning" "demo" {bucket = aws_s3_bucket.demo.id # ← reference to the bucket above
versioning_configuration {status = "Enabled"
}
}
When Terraform sees aws_s3_bucket.demo.id, it understands: "create that bucket first, then this resource." This is the automatic dependency graph. See tf-depends-on.
Multiple instances of the same resource
If you need three similar buckets, there are two ways:
# Method 1: count, indexed by number
resource "aws_s3_bucket" "many" {count = 3
bucket = "bucket-${count.index}"}
# Method 2: for_each, indexed by key
resource "aws_s3_bucket" "regional" {for_each = toset(["us", "eu", "ap"])
bucket = "bucket-${each.key}"}
The address of each instance:
aws_s3_bucket.many[0],aws_s3_bucket.many[1],aws_s3_bucket.many[2]aws_s3_bucket.regional["us"],aws_s3_bucket.regional["eu"], ...
See tf-count-for-each for details on when to use which.
Pitfalls
-
The resource name is not the name in the cloud. If you rename the HCL label from
"demo"to"main", Terraform will drop and recreate the resource, even if the content is identical. Use themoved {}block if you need to rename safely. -
Arguments are strictly typed. If
bucketexpects a string and you write a number,terraform validatewill fail. This is a feature: errors are caught before anything touches the cloud. -
(known after apply)is normal. Do not be alarmed when you see it in the plan. It means "this value will exist after creation." For example,id: the bucket name itself Terraform knows right away, butarnis available only after the API responds. -
Not all arguments support in-place updates. Some resource attributes cannot change without recreation (for example, the EC2 instance type). Terraform marks these in the plan as
-/+ resource ..., meaning "destroy and recreate." Be careful: data will be lost. -
The lifecycle block guards against accidents. See tf-resource-lifecycle:
prevent_destroy = trueblocks deletion, andignore_changesignores drift on specific attributes.