Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Terraform variables accept values from a module’s caller, locals give reusable names to expressions inside the module, and outputs expose selected results. For repetition, use count when numbered, nearly identical instances fit the job, and for_each when each instance has a meaningful key. Use dynamic only to generate nested provider-defined blocks—not repeated resources.

How Terraform values flow through a module

A reusable module has an input interface, internal expressions, and outputs. The caller supplies input variables; expressions and locals can transform or reuse those values; resources and data sources consume them; and outputs expose selected results to the caller or other Terraform interfaces. HashiCorp describes module communication through inputs and outputs, with locals for reusable expressions in between.

These constructs are related, but they are not all “variables.” An input variable is declared in a variable block and supplied from outside the module. local.name refers to a named expression. each.key, each.value, count.index, and a dynamic-block iterator are temporary contextual values available only in the relevant block.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How do Terraform input variables work?

Input variables let a module consumer customize behavior without editing the module’s source. A variable can be declared in a root module or child module. A root module receives values from its execution environment; a parent module passes values to a child using arguments in a module block.

For example, a module might accept a deployment name:

variable "environment" {
  type        = string
  description = "Name used to identify this deployment."
}

resource "example_resource" "app" {
  name = var.environment
}

The resource is illustrative: actual resource types and arguments depend on the provider schema. A caller can pass the value to a child module like this:

module "app" {
  source      = "./modules/app"
  environment = "staging"
}

For root-module inputs, Terraform supports CLI options, environment variables, variable definition files, and HCP Terraform. Use the input mechanism that fits how the configuration is run; a child module receives its values from its parent’s module arguments.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose variable arguments deliberately

  • type constrains the expected value’s shape, such as a string, number, list, or object.
  • default makes the value optional for callers. Without a default, the caller must supply it.
  • description explains the purpose and expected value.
  • validation expresses rules that go beyond the type constraint.
  • sensitive suppresses the value in relevant CLI output. It does not, by itself, prevent the value from being stored in state or plan files.
  • nullable controls whether the value may be assigned null.
  • ephemeral and const are also documented variable features. Check the variable reference for semantics supported by the Terraform version your project pins.

HashiCorp’s style guide recommends ordering variable arguments as type, description, optional default, optional sensitive, then validation. Descriptive names and clear descriptions make module interfaces easier to use.

When should you use locals?

Use a local when an expression deserves a name or needs to be reused within the same module. Locals are not inputs for the caller to set. They can refer to variables, resources, data sources, and other locals.

locals {
  resource_name = "${var.environment}-app"
}

resource "example_resource" "app" {
  name = local.resource_name
}

This keeps a derived value in one place rather than duplicating its expression. Use var.environment for a caller-provided value and local.resource_name for a value computed inside the module.

Should you use count, for_each, or a dynamic block?

The key distinction is what gets repeated. count and for_each create instances of supported Terraform blocks. A dynamic block generates nested configuration blocks inside a containing block. They are not interchangeable loop syntaxes.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Construct What it repeats Identity and access Best fit Important constraint
count Instances of supported blocks, including resources and modules Numeric index; address an instance by index Nearly identical instances where a number or position is a natural identity Cannot be used in the same block as for_each
for_each Instances of supported blocks, including resources, modules, and data sources Map key or set member; use each.key and each.value Instances with meaningful identities or distinct values Keys or set members must be known before apply; sensitive values are not allowed
dynamic Nested configuration blocks Iterator object with key and value A provider-defined nested block needs to be generated from a collection Cannot generate Terraform meta-argument blocks such as lifecycle or provisioner

How do count and for_each differ?

Use count for numbered, similar instances

count takes a number and creates that many instances. Within the block, count.index is the numeric index for the current instance. A counted resource or module is addressed as a list of instances, such as example_resource.app[0]. Choose it when the instances are nearly identical and indexing them makes sense.

variable "replica_count" {
  type        = number
  description = "Number of app instances to create."
  default     = 2
}

resource "example_resource" "app" {
  count = var.replica_count
  name  = "app-${count.index}"
}

This example creates indexed instances. If the collection can change in ways that make position a poor identity, a map or set with meaningful keys may better express the intended instances.

Use for_each when identity matters

for_each accepts a map or a set of strings for resources and modules. Each map key or set member identifies an instance. Inside the block, each.key gives that identity and each.value gives its associated value.

variable "app_names" {
  type        = set(string)
  description = "Names that identify app instances."
  default     = ["api", "worker"]
}

resource "example_resource" "app" {
  for_each = var.app_names
  name     = each.key
}

These instances are addressed by key, for example example_resource.app["api"]. With a map, the key can identify an instance while the value supplies its configuration:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
variable "apps" {
  type = map(object({
    size = string
  }))
  description = "App settings keyed by app name."
}

resource "example_resource" "app" {
  for_each = var.apps
  name     = each.key
  size     = each.value.size
}

Choose stable, understandable keys that represent the intended instances. Terraform requires the map keys or set members to be known before apply. They cannot depend on impure functions such as uuid, bcrypt, or timestamp, and sensitive values cannot be used as for_each arguments because instance identities are shown in Terraform’s UI.

Do not put count and for_each in one block. Select the one whose identity model best matches the data and how you need to address the instances.

What is the difference between for_each and a dynamic block?

A resource-level for_each repeats resource instances. A dynamic block repeats nested blocks within one containing resource, data source, provider, or provisioner block. The provider schema determines which nested block types are valid.

A dynamic block names the nested block type in its label, iterates a collection, and uses content to define each generated block. The iterator defaults to the dynamic block label; an iterator argument can give it a different name. The iterator exposes key and value.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
dynamic "setting" {
  for_each = var.settings
  content {
    name  = setting.key
    value = setting.value
  }
}

Here, setting is the default iterator name for the setting block. The example is only valid where the containing block’s provider schema defines a compatible nested setting block and its arguments.

Dynamic blocks cannot generate Terraform meta-argument blocks such as lifecycle or provisioner, which Terraform processes earlier. Prefer literal nested blocks when they are clear and practical; overusing dynamic blocks can make configuration harder to read.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

How do outputs expose values from a module?

Outputs expose selected values from a module to its caller, Terraform CLI output, or HCP Terraform. In a child module, the parent refers to a declared output through the child’s module result.

output "app_name" {
  description = "Name assigned to the app resource."
  value       = example_resource.app.name
}

A parent module can consume that output with module.app.app_name. HashiCorp’s style guide recommends giving outputs descriptions and specifying their type for readability and module documentation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Repetition changes the result’s shape. A module using for_each produces a map keyed by the supplied identities; a module using count produces a list. The same instance-addressing distinction applies when you reference repeated resources. Data-source blocks can also use count or for_each to create separately addressable instances.

Validate against your Terraform and provider versions

Terraform language features and provider resource schemas are separate considerations. Pin and check the Terraform and provider versions used by your project, then validate that examples match those versions and the nested blocks and arguments the provider actually supports. The HashiCorp references below document the language constructs; they do not establish that a provider-specific example works with every provider version.

Official references

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.