Why references matter
In HCL almost everything is a reference. Does the bucket name depend on the region? Reference data.aws_region. Does the name depend on a variable? var.something. Do you need an attribute of one resource inside another? aws_s3_bucket.demo.arn.
Without a solid grasp of the reference syntax, Terraform turns into copy and paste with no understanding. With it, the opposite is true.
The five main prefixes
| Prefix | What | Example |
|---|---|---|
var.name | Variable | var.region |
local.name | Local | local.name_prefix |
type.name.attribute | Resource | aws_s3_bucket.demo.arn |
data.type.name.attribute | Data source | data.aws_region.current.name |
module.name.output_name | Output of a child module | module.network.vpc_id |
There are also special ones available inside blocks:
count.index, inside a block withcount.each.key,each.value, inside a block withfor_each.self.attribute, inside aprovisioner(advanced).
Simple examples
variable "env" { default = "dev" }locals { prefix = "myapp-${var.env}"}
data "aws_region" "current" {}resource "aws_s3_bucket" "logs" { bucket = "${local.prefix}-logs-${data.aws_region.current.name}"# myapp-dev-logs-us-east-1
}
resource "aws_s3_bucket_versioning" "logs" {bucket = aws_s3_bucket.logs.id # reference to the first bucket
versioning_configuration {status = "Enabled"
}
}
Inside ${...} vs without it
In modern HCL you need ${...} only when the expression is part of a string:
# part of a string: ${...} is requiredbucket = "myapp-${var.env}-${local.region}"# whole expression: quotes and ${...} are not neededbucket = local.bucket_name
count = var.instance_count
region = data.aws_region.current.name
The old style "${var.foo}" for a single expression still works, but Terraform will complain with the warning "Interpolation-only expressions are deprecated".
The splat operator [*]: take one field from all of them
If a resource is created with count or for_each, it becomes a list or a map. To get, for example, every arn:
resource "aws_s3_bucket" "logs" {count = 3
bucket = "logs-${count.index}"}
# all ARNs as a list
output "all_arns" {value = aws_s3_bucket.logs[*].arn
# = [aws_s3_bucket.logs[0].arn, aws_s3_bucket.logs[1].arn, aws_s3_bucket.logs[2].arn]
}
The [*] sign is called the splat operator. It applies to count and for_each in the same way.
An alternative syntax uses for:
output "all_arns" {value = [for b in aws_s3_bucket.logs : b.arn]
}
Addresses with an index
# count
aws_s3_bucket.logs[0] # first
aws_s3_bucket.logs[length(aws_s3_bucket.logs) - 1] # last
# for_each
aws_s3_bucket.regional["us"]
aws_s3_bucket.regional["eu"].arn
# inside a resource with count
resource "aws_s3_bucket" "logs" {count = 3
bucket = "logs-${count.index}" # 0, 1, 2}
# inside a resource with for_each
resource "aws_s3_bucket" "regional" {for_each = toset(["us", "eu"])
bucket = "data-${each.key}" # us, eu tags = { region = each.value } # same value as each.key for a set}
Which attributes a resource has
When you write aws_s3_bucket.demo.X, which values of X are available? That depends on the provider. The list is in the provider documentation:
The "Attributes Reference" section is what you can read. The "Arguments" section is what you can set.
Tip: remember the address pattern terraform.io/providers/<vendor>/<provider>/latest/docs/resources/<resource_name>, you will use it constantly.
Pitfalls
-
localvslocalsagain. The declaration islocals { ... }, the reference islocal.name. Nos. -
A resource with count is a list in state, and an address without an index does not work everywhere.
aws_s3_bucket.logs.arnis an error when count > 1. You needaws_s3_bucket.logs[0].arnoraws_s3_bucket.logs[*].arn. -
var.nameis for a variable, not for locals. Beginners often writevar.prefixwhile meaning a local. These are two different namespaces. -
(known after apply)breaks your intuition. This happens when you reference an attribute that is computed on the cloud side. Terraform knows that the value will appear, but does not show it in the plan. That is normal. -
Circular dependencies are fatal. This happens when A references B and B references A. Terraform fails with "cyclic dependency". The fix is tf-locals or a rethink of the architecture.
-
You cannot reference a resource from inside its own block.
resource "x" { foo = self.bar }does not work:selfis available only inprovisionerblocks. Normally there is no way around it.