What a utility provider is
Most providers (aws, kubernetes, github) represent an external
system: the API of a cloud or a service. A utility provider is not an
API, it is language functionality: a random value generator, a timer,
a trigger. These providers give you what HCL does not have built in.
All of them are official HashiCorp providers. Versions are stable, and breaking changes are rare.
random
Generates values that are deterministic within the state and random at creation time.
terraform { required_providers { random = {source = "hashicorp/random"
version = "~> 3.6"
}
}
}
resource "random_id" "suffix" {byte_length = 4
}
resource "aws_s3_bucket" "demo" { bucket = "linuxlab-${random_id.suffix.hex}"}
random_id.suffix.hex is an 8-character hex string (4 bytes * 2). It is
unique at creation, then fixed in state. Every later plan keeps the same
value.
Variants
| Resource | What it generates | Main use case |
|---|---|---|
random_id | Bytes to hex/base64 | Unique name suffixes |
random_string | A string of characters | Names, passwords (not great) |
random_password | A string with a minimum of character classes | Real passwords (sensitive) |
random_pet | A word pair like "honest-zebra" | Human-readable names for the lab |
random_uuid | UUID v4 | Correlation, request IDs |
random_integer | A number in a range | Random port, index |
random_shuffle | Shuffles a list | AZ distribution |
keepers: regenerate on change
resource "random_id" "suffix" {byte_length = 4
keepers = {bucket_name = var.bucket_name
}
}
If var.bucket_name changes, random_id is recreated, which gives a new
hex value and a new bucket. The rule is "when N changes, generate a new
value."
Without keepers, the value never changes without an explicit taint
or -replace.
time
Time manipulation in HCL.
terraform { required_providers { time = {source = "hashicorp/time"
version = "~> 0.12"
}
}
}
time_static: the moment of creation
resource "time_static" "created_at" {}resource "aws_s3_bucket" "demo" { bucket = "linuxlab-${formatdate("YYYYMMDD", time_static.created_at.rfc3339)}"}
Records a timestamp in state on the first apply. After that it does not change. Useful for immutable marks (a bucket name that contains the creation date).
time_sleep: a delay during apply
resource "aws_iam_role" "app" {name = "app"
# ...
}
resource "time_sleep" "wait_for_iam" {depends_on = [aws_iam_role.app]
create_duration = "30s"
}
resource "aws_eks_cluster" "demo" {depends_on = [time_sleep.wait_for_iam]
# ...
}
The IAM role is created, but AWS is eventually consistent: until
propagation finishes, EKS does not see the role. time_sleep waits 30
seconds between them.
This is a workaround, not a solution. If you can detect readiness through a data source, prefer that. If you cannot, sleep. The option is there.
time_rotating
resource "time_rotating" "cert" {rotation_days = 30
}
The id attribute changes every 30 days. Combined with random_password
and keepers it gives you "rotate the password once a month." This is
rarely useful, because secret managers (Vault) usually do it better.
null_resource (deprecated)
An old pattern, the "non-resource" from the null provider, used to hang
triggers and a provisioner on something:
resource "null_resource" "bootstrap" { triggers = {version = var.script_version
}
provisioner "local-exec" {command = "./bootstrap.sh"
}
}
It still works, but it is not used in new code. It has been replaced
by terraform_data.
terraform_data (since TF 1.4+)
Built into Terraform, so you do not need a separate provider.
resource "terraform_data" "bootstrap" {input = var.script_version
triggers_replace = [var.script_version]
provisioner "local-exec" {command = "./bootstrap.sh"
}
}
Advantages:
- It does not require
required_providers.null, because it is built in. - It can store a value (
input): it becomes available throughterraform_data.bootstrap.output. triggers_replaceis a list; when any item changes, the resource is recreated.
terraform_data as a "buffer" for a value
variable "image_tag" {type = string
default = "latest"
}
resource "terraform_data" "image_version" {input = var.image_tag
}
resource "aws_ecs_task_definition" "app" {# ...
container_definitions = jsonencode([{name = "app"
image = "myrepo/app:${terraform_data.image_version.output}"}])
lifecycle {replace_triggered_by = [terraform_data.image_version]
}
}
When the tag changes, the task definition is force-recreated through
replace_triggered_by. This is the "canary" pattern: tie the lifecycle of
a heavy resource to a light indicator.
Which provider when
| You need | Use |
|---|---|
| A unique name suffix | random_id |
| A strong password | random_password |
| A creation timestamp | time_static |
| To wait for AWS to "catch up" | time_sleep |
| A recreate trigger when X changes | terraform_data with triggers_replace |
To run local-exec on change | terraform_data with a provisioner |
| To store a computed value between resources | terraform_data.input/output |
Pitfalls
-
random in LocalStack is the same as in real AWS. This provider does not depend on the cloud. Tests with LocalStack are fully valid for random/time/terraform_data.
-
random_passwordis NOT more secure thanrandom_string. Both use crypto/rand. Butrandom_passwordis marked sensitive and is not printed in logs. For secrets, always userandom_passwordso the password does not leak throughterraform outputor plan logs. -
A
random_*value rotates only through-replaceor keepers. A plainapplydoes not change it. This is a feature, not a bug: otherwise the bucket would get a different name on every apply. -
time_sleepspends real apply time. 30s on every apply is bad for CI. Use it only when the alternative does not work. -
null_resourceis deprecated, not forbidden. You do not have to migrate old code; for new code, writeterraform_data. There is no difference between them in behavior. The difference is in aesthetics and how well they are integrated. -
A
provisioneronterraform_datais a last resort. Idempotency is not guaranteed, errors are hard to handle, and sandboxing is zero. The replacement forlocal-execis anexternalprovider data source (see tf-archive-external-http). -
A
randomresource lands in state with its value in the clear. This is obvious forrandom_id, butrandom_passwordis also stored in state in the clear; it is sensitive only in output. Protect your state.