Provisioners and Functions
Built-in Functions
Section titled “Built-in Functions”Terraform includes a rich library of built-in functions you can use in expressions. You cannot define custom functions — but the built-in set covers most needs.
String Functions
Section titled “String Functions”upper("hello") # "HELLO"lower("HELLO") # "hello"trimspace(" hello ") # "hello"replace("hello world", " ", "-") # "hello-world"format("server-%03d", 5) # "server-005"join(", ", ["a", "b", "c"]) # "a, b, c"split(",", "a,b,c") # ["a", "b", "c"]substr("terraform", 0, 5) # "terra"Numeric Functions
Section titled “Numeric Functions”min(5, 3, 9) # 3max(5, 3, 9) # 9ceil(4.2) # 5floor(4.8) # 4abs(-42) # 42Collection Functions
Section titled “Collection Functions”length(["a", "b", "c"]) # 3contains(["a", "b"], "a") # truelookup({a = 1, b = 2}, "a", 0) # 1 (default 0 if key missing)merge({a = 1}, {b = 2}) # {a = 1, b = 2}flatten([["a", "b"], ["c"]]) # ["a", "b", "c"]distinct(["a", "b", "a"]) # ["a", "b"]keys({a = 1, b = 2}) # ["a", "b"]values({a = 1, b = 2}) # [1, 2]zipmap(["a", "b"], [1, 2]) # {a = 1, b = 2}element(["a", "b", "c"], 1) # "b"concat(["a"], ["b", "c"]) # ["a", "b", "c"]Encoding Functions
Section titled “Encoding Functions”jsonencode({name = "web", port = 8080}) # '{"name":"web","port":8080}'jsondecode("{\"name\":\"web\"}") # {name = "web"}base64encode("hello") # "aGVsbG8="base64decode("aGVsbG8=") # "hello"yamlencode({replicas = 3}) # "replicas: 3\n"Filesystem Functions
Section titled “Filesystem Functions”file("scripts/init.sh") # read file contents as stringfileexists("scripts/init.sh") # true/falsetemplatefile("templates/config.tpl", { port = 8080 }) # render templatetemplatefile is particularly useful for generating config files:
server { listen ${port}; server_name ${hostname};}resource "aws_instance" "web" { user_data = templatefile("templates/userdata.sh", { db_host = aws_db_instance.main.address db_port = 5432 })}IP/Network Functions
Section titled “IP/Network Functions”cidrsubnet("10.0.0.0/16", 8, 1) # "10.0.1.0/24"cidrsubnet("10.0.0.0/16", 8, 2) # "10.0.2.0/24"cidrhost("10.0.1.0/24", 5) # "10.0.1.5"cidrsubnet is essential for dynamically computing subnet CIDRs from a VPC CIDR.
Type Conversion Functions
Section titled “Type Conversion Functions”tostring(42) # "42"tonumber("42") # 42tolist(toset(["a", "b"])) # ["a", "b"]toset(["a", "b", "a"]) # toset(["a", "b"])tomap({a = 1, b = 2}) # {a = 1, b = 2}Testing Functions in the Console
Section titled “Testing Functions in the Console”Use terraform console to experiment:
terraform console> cidrsubnet("10.0.0.0/16", 8, 3)"10.0.3.0/24"> format("env-%s-%02d", "prod", 1)"env-prod-01"Provisioners
Section titled “Provisioners”Provisioners execute scripts or commands on a resource after it’s created (or before it’s destroyed). They are a last resort — Terraform’s documentation explicitly warns against using them when better alternatives exist.
Why Avoid Provisioners?
Section titled “Why Avoid Provisioners?”- Not declarative — Provisioners run imperative commands; Terraform can’t track what they did.
- Not in state — If a provisioner script changes, Terraform doesn’t know. Plan won’t show the difference.
- Fragile — SSH connections fail, scripts error out, and partial runs leave resources in an unknown state.
- Better alternatives exist — Cloud-init / user_data for boot scripts, Ansible for configuration management, Packer for pre-built images.
When Provisioners Are Acceptable
Section titled “When Provisioners Are Acceptable”- Bootstrapping — A quick one-liner to register a node with a config management tool.
- One-time setup — Running a database migration that can’t be done any other way.
- Legacy systems — When there’s truly no API or provider support for what you need.
local-exec
Section titled “local-exec”Runs a command on the machine where Terraform is running:
resource "aws_instance" "web" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t3.micro"
provisioner "local-exec" { command = "echo ${self.public_ip} >> hosts.txt" }}remote-exec
Section titled “remote-exec”Runs a command on the remote resource via SSH or WinRM:
resource "aws_instance" "web" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t3.micro" key_name = "my-key"
connection { type = "ssh" user = "ubuntu" private_key = file("~/.ssh/id_rsa") host = self.public_ip }
provisioner "remote-exec" { inline = [ "sudo apt-get update", "sudo apt-get install -y nginx", ] }}Copies files or directories to the remote resource:
provisioner "file" { source = "configs/app.conf" destination = "/etc/app.conf"}Destroy-Time Provisioners
Section titled “Destroy-Time Provisioners”Run when a resource is being destroyed:
provisioner "local-exec" { when = destroy command = "echo 'Destroying ${self.id}' >> destroy.log"}terraform_data (Replacing null_resource)
Section titled “terraform_data (Replacing null_resource)”The null_resource was traditionally used to run provisioners without a real resource. As of Terraform 1.4, terraform_data is the recommended replacement:
resource "terraform_data" "run_script" { triggers_replace = [ aws_instance.web.id, timestamp(), ]
provisioner "local-exec" { command = "python3 scripts/configure.py ${aws_instance.web.public_ip}" }}triggers_replace— When any trigger value changes, the resource is replaced and provisioners re-run.- No provider dependency — it’s built into Terraform core.
null_resource (Legacy)
Section titled “null_resource (Legacy)”Still works but requires the hashicorp/null provider:
resource "null_resource" "run_script" { triggers = { instance_id = aws_instance.web.id }
provisioner "local-exec" { command = "python3 scripts/configure.py ${aws_instance.web.public_ip}" }}Prefer terraform_data for new configurations.
Alternatives to Provisioners
Section titled “Alternatives to Provisioners”| Need | Better Approach |
|---|---|
| Install packages on boot | user_data / cloud-init |
| Configure servers after creation | Ansible (run after terraform apply) |
| Pre-install software in images | Packer (build AMI, reference in Terraform) |
| Run database migrations | CI/CD pipeline step (after deploy) |
| Register with monitoring | Provider-specific resources (e.g. Datadog provider) |
Key Takeaways
Section titled “Key Takeaways”- Terraform has 100+ built-in functions — use
terraform consoleto explore them. templatefileis the cleanest way to generate config files from templates.cidrsubnetis essential for computing subnet CIDRs dynamically.- Avoid provisioners when possible — prefer user_data, Ansible, or Packer.
- Use
terraform_datainstead ofnull_resourcefor trigger-based provisioner execution. - If you must use a provisioner, prefer
local-execoverremote-exec(less fragile).