Terraform · lesson 6 of 17
Beginner
7- 1 Terraform Fundamentals (IaC)
- 2 Terraform Workflow (init → validate → plan → apply)
- 3 Terraform State (tfstate, remote state, locking)
- 4 Terraform Variables and Outputs
- 5 Providers and Resources (the building blocks of Terraform)
- 6 Terraform Locals: Computed Values Without Repetition
- 7 Terraform Modules (reuse, composition, and clean structure)
Intermediate
7- 8 Terraform count and for_each (repeat resources safely)
- 9 The lifecycle Block: prevent_destroy, create_before_destroy, ignore_changes
- 10 Data Sources and Dependencies (read existing infrastructure)
- 11 sensitive = true: What It Hides, and What It Doesn't
- 12 Remote State Safety (locking, drift, and recovery)
- 13 Workspaces and Environments (dev/stage/prod patterns)
- 14 Terraform CI/CD with GitHub Actions (plan/apply)
Advanced
3Also part of Terraform
4Terraform Locals: Computed Values Without Repetition
A locals block computes a value once from variables or other locals, so one edit cascades everywhere, shown with the real replacement plan it causes.
By SkillByExample · Updated
Any value used more than once can end up with one copy updated and another forgotten. A
locals block computes the value once, and everything else refers to it by name. Examples use
Terraform 1.16.5 and hashicorp/local 2.9.1.
A locals block with a dependency chain
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.9"
}
}
}
locals {
environment = "staging"
app_name = "orders-api"
# A local can reference another local
full_name = "${local.app_name}-${local.environment}"
# And do real computation, not just string joins
replica_count = local.environment == "production" ? 5 : 2
}
resource "local_file" "config" {
filename = "${path.module}/generated-config.txt"
content = "service=${local.full_name}\nreplicas=${local.replica_count}\n"
}
output "resolved_name" {
value = local.full_name
}
output "resolved_replicas" {
value = local.replica_count
}
$ terraform apply -auto-approve -no-color
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# local_file.config will be created
+ resource "local_file" "config" {
+ content = <<-EOT
service=orders-api-staging
replicas=2
EOT
+ content_base64sha256 = (known after apply)
+ content_base64sha512 = (known after apply)
+ content_md5 = (known after apply)
+ content_sha1 = (known after apply)
+ content_sha256 = (known after apply)
+ content_sha512 = (known after apply)
+ directory_permission = "0777"
+ file_permission = "0777"
+ filename = "./generated-config.txt"
+ id = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ resolved_name = "orders-api-staging"
+ resolved_replicas = 2
local_file.config: Creating...
local_file.config: Creation complete after 0s [id=d28c70f13bcd999b995544b3c05e8114f0fbc772]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
resolved_name = "orders-api-staging"
resolved_replicas = 2
The strings orders-api-staging and 2 aren’t written anywhere in the config. They’re computed,
and the plan shows the computed values, not local.full_name. Locals are known at plan time
when everything they depend on is known, which is why the outputs already have values in the
plan.
Changing one local cascades everywhere it’s used
Change one line, environment = "production", and apply again:
$ terraform apply -auto-approve -no-color
local_file.config: Refreshing state... [id=d28c70f13bcd999b995544b3c05e8114f0fbc772]
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement
Terraform will perform the following actions:
# local_file.config must be replaced
-/+ resource "local_file" "config" {
~ content = <<-EOT # forces replacement
- service=orders-api-staging
+ service=orders-api-production
- replicas=2
+ replicas=5
EOT
~ content_base64sha256 = "UAN/k0R8sD0QmwoUrlr9DP9ump56zzTSIOyJsQf7lSs=" -> (known after apply)
~ content_base64sha512 = "GjrYwJJJsaDlqMvqCMEjEBzjZTWRc66BE8u/YvKuek7jUOZPJg80qzAS8p7iCkHZ3V1Xhy+R4eGYsm4SA+XqUQ==" -> (known after apply)
~ content_md5 = "8abf6f91dba1c78ccdfe2b1e6a362b31" -> (known after apply)
~ content_sha1 = "d28c70f13bcd999b995544b3c05e8114f0fbc772" -> (known after apply)
~ content_sha256 = "50037f93447cb03d109b0a14ae5afd0cff6e9a9e7acf34d220ec89b107fb952b" -> (known after apply)
~ content_sha512 = "1a3ad8c09249b1a0e5a8cbea08c123101ce365359173ae8113cbbf62f2ae7a4ee350e64f260f34ab3012f29ee20a41d9dd5d57872f91e1e198b26e1203e5ea51" -> (known after apply)
~ id = "d28c70f13bcd999b995544b3c05e8114f0fbc772" -> (known after apply)
# (3 unchanged attributes hidden)
}
Plan: 1 to add, 0 to change, 1 to destroy.
Changes to Outputs:
~ resolved_name = "orders-api-staging" -> "orders-api-production"
~ resolved_replicas = 2 -> 5
local_file.config: Destroying... [id=d28c70f13bcd999b995544b3c05e8114f0fbc772]
local_file.config: Destruction complete after 0s
local_file.config: Creating...
local_file.config: Creation complete after 0s [id=5cf2018b13895ce8b2713b79d815ec152fded303]
Apply complete! Resources: 1 added, 0 changed, 1 destroyed.
Outputs:
resolved_name = "orders-api-production"
resolved_replicas = 5
Both full_name and replica_count were recomputed, and the conditional flipped from 2 to
5. The -/+ comes from local_file itself, which can’t edit a file’s content in place. A
different resource type might apply the same change as an in-place update.
A local can’t be set from outside
$ terraform plan -var full_name=custom -no-color
Error: Value for undeclared variable
A variable named "full_name" was assigned on the command line, but the root
module does not declare a variable of that name. To use this value, add a
"variable" block to the configuration.
That’s the line between a variable and a local. A variable is an input the caller controls. A local is internal, and nothing on the command line can override it. If callers need to override a value, make it a variable and compute the local from it.
Practice
1. Add a boolean local is_production and expose it as an output called is_prod.
Solution
locals {
is_production = local.environment == "production"
}
output "is_prod" {
value = local.is_production
}
$ terraform apply -auto-approve -no-color
local_file.config: Refreshing state... [id=5cf2018b13895ce8b2713b79d815ec152fded303]
Changes to Outputs:
+ is_prod = true
You can apply this plan to save these new output values to the Terraform
state, without changing any real infrastructure.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
is_prod = true
resolved_name = "orders-api-production"
resolved_replicas = 5
A configuration can have several locals blocks, and a local can hold any type, not just
strings. Adding an output changes nothing real, so the apply only records the new output.
2. Rewrite the original locals block in reverse order, with replica_count first and
environment last. Does it still work?
Solution
locals {
replica_count = local.environment == "production" ? 5 : 2
full_name = "${local.app_name}-${local.environment}"
app_name = "orders-api"
environment = "production"
}
$ terraform validate -no-color
Success! The configuration is valid.
$ echo 'local.replica_count' | terraform console
5
Order doesn’t matter. Terraform builds a graph from the references, so replica_count waits for
environment no matter where each one is written.
3. Add an output that references local.does_not_exist and validate.
Solution
output "broken" {
value = local.does_not_exist
}
$ terraform validate -no-color
Error: Reference to undeclared local value
on main.tf line 35, in output "broken":
35: value = local.does_not_exist
A local value with the name "does_not_exist" has not been declared.
A misspelled local fails validation. It never quietly becomes an empty string in a resource name.
4. Build a tags map local that always has app and env, and adds pager = "on-call"
only in production. Inspect it with terraform console.
Solution
locals {
tags = merge(
{ app = local.app_name, env = local.environment },
local.environment == "production" ? { pager = "on-call" } : {},
)
}
$ echo 'local.tags' | terraform console
{
"app" = "orders-api"
"env" = "production"
"pager" = "on-call"
}
merge with a conditional empty map is the usual way to add keys only in some environments.
Every resource that sets tags = local.tags then gets the same set.
Next: modules, which package a group of resources, their variables and their outputs into a reusable unit.