When input variable types are left ambiguous, module consumers are more likely to pass the wrong values and you end up with unexpected plan diffs. Terraform ships with the HCL2 type system, so you can declare type constraints on the variable block via the type argument. This article focuses on the four most-used types — string, list, map, and object — and walks through the design and implementation patterns for keeping your inputs consistent.
The Terraform Associate exam frequently tests the behavior of each type and how it interacts with surrounding features like validation and for_each. Learning these alongside real-world design principles turns them into reliable points on the exam.
Terraform input variables are declared with a variable block, and adding a type argument validates user input and tfvars values during plan and apply. If you set a default, the type is inferred from that value implicitly, but you should still specify the type explicitly to make the contract at module boundaries crystal clear.
string holds a single value, list is an ordered sequence of the same type, map is an unordered key-value dictionary, and object is a struct-like type with a fixed attribute schema. object is great for locking down a module API contract, map is useful for accepting dictionaries with arbitrary keys, and list shines for inputs where order matters (for example, CIDR priority order).
any is flexible but weakens validation and lets bad input slip through, so avoid it outside of short-lived migration scenarios.
| Type | Typical literal example | Main use case | Gotchas |
|---|---|---|---|
| string | "prod" | Environment names, regions, identifiers | Combine with validation to check for empty strings and allowed characters |
| list(string) | ["web", "api"] | Ordered entry lists (e.g., security rules) | Cannot be used directly with for_each (convert to map or set) |
| map(string) | { env = "prod", team = "platform" } | Tags / labels, arbitrary-key dictionaries | Unordered. Designs that lean on lookup and merge are the standard |
| object({ name=string, size=string }) | { name = "app", size = "t3.small" } | Locking down module input/output contracts | All attributes are required (missing ones cause errors). Mind backward compatibility when extending |
| list(object({...})) | [{ name="a" }, { name="b" }] | Array of same-shaped objects (driving multiple resources) | In practice, keying by something like name and converting to a map for for_each is the stable approach |
Type constraint and validation flow (input → type check → module)
Declaration examples for the 4 main types (with type constraints)
variable "env" {
type = string
description = "Deployment environment (dev/stg/prod)"
validation {
condition = contains(["dev", "stg", "prod"], var.env)
error_message = "env must be one of dev, stg, or prod."
}
}
variable "subnets" {
type = list(string)
description = "Ordered list of subnet IDs"
}
variable "tags" {
type = map(string)
description = "Common tags"
default = {}
}
variable "server" {
type = object({
name = string
size = string
tags = map(string)
})
description = "Single server spec"
}string is the simplest type, but lax control over naming rules and allowed values leads to running into cloud-side naming constraints or mixing up environments. The standard practice is to combine it with validation that uses ranges or regular expressions.
If you need multi-line strings you can use a heredoc (<<-EOT ... EOT), but instead of feeding long text through input variables, you'll usually find template files or locals-based construction easier to maintain.
A safe string variable definition (regex + range)
variable "env" {
type = string
description = "Environment name (dev|stg|prod)"
validation {
condition = can(regex("^(dev|stg|prod)
NicheeLab を読み込み中…
quot;, var.env)) error_message = "env must be one of dev, stg, or prod." } } variable "service_name" { type = string description = "Service name (lowercase alphanumerics and hyphens, 1..30 chars)" validation { condition = can(regex("^[a-z0-9-]{1,30}
quot;, var.service_name)) error_message = "service_name must use only lowercase alphanumerics and hyphens (max 30 chars)." } }
A list is an ordered sequence where the order carries meaning. The type constraint list(T) applies T to every element. list(string) fits ID lists, while list(object) lets you express a set of entries that share the same schema.
A resource's for_each is limited to map or set, so you can't use list(object) directly with for_each. In practice, converting it into a map keyed by a unique attribute like name via a comprehension before for_each is the stable approach. When order matters and you use count, it's also important to design around index-dependent reorders that would cause destructive changes.
Converting list(object) to a map for for_each
variable "servers" {
description = "Servers to deploy"
type = list(object({
name = string
size = string
tags = map(string)
}))
}
locals {
servers_by_name = { for s in var.servers : s.name => s }
}
# Example: provider-agnostic pseudo resource
resource "example_server" "this" {
for_each = local.servers_by_name
name = each.value.name
size = each.value.size
tags = each.value.tags
}
map(string) is the go-to type for tags and labels. Order isn't guaranteed, so the order shown in plan output can shift. To stay consistent, default the map to {} and compose shared and override tags using merge — it's the easiest pattern to work with.
For potentially missing key lookups, use lookup(map, key, default) for safety. Constraining key formats (allowed characters) with validation lets you get ahead of cloud-side restrictions.
Composing and safely accessing map(string)
variable "tags" {
type = map(string)
description = "Common tags (overridable by callers)"
default = {}
validation {
condition = alltrue([for k in keys(var.tags) : can(regex("^[-a-z0-9]+
NicheeLab を読み込み中…
quot;, k))])
error_message = "Tag keys must contain only lowercase alphanumerics and hyphens."
}
}
locals {
base_tags = { "managed-by" = "terraform" }
final_tags = merge(local.base_tags, var.tags)
}
output "team_tag" {
value = lookup(local.final_tags, "team", "unknown")
}
object fixes both attribute names and types, which makes it a powerful contract at module boundaries. Every attribute is required, and missing one causes a type-mismatch error. When you add attributes later, plan for backward compatibility (existing consumers' tfvars must keep working) by splitting the new field into a separate variable or providing an extension hook via map(string).
You can use null as the default for an entire object, but the consumer has to handle null with coalesce or conditional logic to fill in defaults. To make individual attributes nullable, it's easier to enforce consistency by ruling out null at design time and providing explicit defaults instead.
Designing an object variable with validation
variable "server" {
description = "Server spec (strict schema)"
type = object({
name = string
size = string
tags = map(string)
})
validation {
condition = can(regex("^[a-z0-9-]+
NicheeLab を読み込み中…
quot;, var.server.name)) && length(var.server.name) <= 30
error_message = "server.name must use lowercase alphanumerics and hyphens, max 30 characters."
}
}
locals {
server_tags = merge({ "managed-by" = "terraform" }, var.server.tags)
}
# Example usage (output)
output "server_label" {
value = "${var.server.name}:${var.server.size}"
}
In practice, sticking to the basics — explicit variable types, validation to constrain value ranges, list converted to map before for_each, map composed and safely accessed with merge/lookup, and object used to lock down contracts — eliminates a lot of confusion. Normalizing inputs with the type conversion functions toset/tomap/tolist is also effective.
The exam frequently tests differences between list and map (order, for_each compatibility), the strictness of object, the pitfalls of any, and the role of validation. Also make sure you understand the input paths via tfvars and -var/-var-file, the effect of default, and the fact that type errors are caught during plan.
Input paths and normalization snippet (tfvars and type conversion)
# terraform.tfvars example
env = "stg"
subnets = ["subnet-aaa", "subnet-bbb"]
# CLI example
# terraform plan -var-file="env/stg.tfvars"
# Example of type normalization (list → map)
locals {
items = ["a", "b", "c"]
items_map = { for v in local.items : v => upper(v) }
}
Associate
問題 1
A module accepts the following input variable. Which tfvars definition matches the tags type constraint? variable "tags" { type = map(string) description = "Common tags" }
正解: A
map(string) is a dictionary whose keys and values are both strings. A matches the key:string → value:string shape. B is a list, C has a list for the value, and D is a single string — none of those satisfy the constraint.
When should I use map vs object?
Use map(string) when you want to accept a dictionary with arbitrary keys (e.g., tags). Use object when you want to lock down the attribute names and types as a strict contract. If you anticipate future expansion, a hybrid that uses object for required fields and map for optional extensions also works well.
Why can't I use a list with for_each, and how do I work around it?
for_each requires a map or a set. A list allows ordering and duplicates, so it has no stable keys. The workaround is to convert it into a map with a comprehension: { for v in var.list : key(v) => v }.
How do I handle type mismatch errors?
Compare the variable's type with the actual values in tfvars or defaults, and normalize them with toset/tomap/tolist. Check whether you've accidentally passed a list to a map(string), whether any required attributes are missing from your object, and review your validation conditions.
Practice with certification-focused question sets
無料で問題を解いてみるNicheeLab Editorial Team
NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.
HCL Syntax: Terraform's Configuration Language (2026)
HCL2 fundamentals for Terraform — blocks, attributes, expres...
Terraform Authoring & Operations Pro: Complete Guide (2026)
Tactics for the Terraform Pro exam — module authoring, works...
Terraform Providers: Plugin Management Fundamentals (2026)
Provider mechanics — required_providers, versions, mirrors, ...
Terraform Resource Blocks: Declarative Infra Units (2026)
Resource block fundamentals — addresses, references, common ...
Terraform Data Sources: Read-Only External Data (2026)
Data source basics — declaration, refresh behavior, dependen...