Terraform

Hardening Inputs with Terraform variable validation: Validation Rules from an Associate/Pro Perspective

2026-04-19
NicheeLab Editorial Team

variable validation lets you declaratively enforce business rules that type constraints alone cannot express, using the Terraform language itself. You can attach explicit error messages, which improves both module reusability and safety.

The Associate exam focuses on basic syntax and evaluation timing, while the Pro exam often covers collections, conditional checks, and unknown-value handling. In practice, rejecting bad input at the module boundary also lowers maintenance cost.

Fundamentals and Evaluation Timing of variable validation

A validation block inside a variable block requires its condition to be true; if it is false, evaluation fails with the text of error_message. The condition is a Terraform language expression that references var.<name>.

Evaluation timing is the moment the variable's final value becomes known. Usually that is at plan time, and a false result stops the plan with an error. If the value is unknown (for example because it depends on a data source) and the condition result is indeterminate, evaluation is deferred until apply, unless it can be decisively determined to be false.

  • Declarative and side-effect free: no external commands, no resource references
  • Designed to work alongside type constraints: use type for coarse-grained rules and validation for fine-grained rules
  • Write error messages for the consumer with concrete details (expected format and examples)
  • Applied uniformly regardless of input source (-var, tfvars, env vars, or Cloud/Enterprise variables)
ItemRoleExample
type constraintEnforces type and basic constraintsstring, number, list(string), object({...})
defaultDefault value when no input is supplieddefault = 443
validationDeclares rules that types cannot expressAMI must start with "ami-", port in 1..65535, etc.

Evaluation pipeline (conceptual)

Input source(-var, tfvars, env, Cloud)Value resolution and default applicationType constraint and type conversioncustom validationtrue → continue / false → errorplan/applyinput → value resolution/default → type constraint → custom validation → plan/apply

Minimal example

variable "image_id" {
  type = string

  validation {
    condition     = can(regex("^ami-[0-9a-f]+
NicheeLab を読み込み中…
quot;, var.image_id)) error_message = "image_id must be a valid AMI ID starting with 'ami-'. Example: ami-0123abcd"; } }

Syntax and Best Practices

A validation block consists of condition (required) and error_message (required). condition is a pure expression and can use functions, operators, and for expressions. Keep error messages short and specific enough that the consumer can fix the input immediately.

Guard fragile checks like regex and numeric ranges with can() so that malformed inputs do not error out the regex itself. Keep heavy computation and large collection traversals to a minimum, and aim for expressions that stay readable and maintainable.

  • Narrow the shape with type first, then layer business rules on with validation
  • Protect regex with can(): can(regex(pattern, s))
  • Express compound conditions compactly with logical AND/implication: (env != "prod" || count >= 3)
  • Include expected format, allowed range, and an example in your message
FunctionUse caseNotes
regex, canFormat and pattern validationregex returns false on no match but errors on a bad pattern → wrap with can
startswith, endswithSimple prefix/suffix checksMore readable than regex for simple cases
containsElement/value membership checkGreat fit for checking against an allowed set
alltrue, anytrueAggregate boolean-array checksVery powerful in combination with for expressions
length, distinctLength and uniqueness checksCommonly used to express "no duplicates"

Designing error messages: key angles

What is expectedExpected format or conditionAllowed range/formatBoundary values and patternsExamplesuch as ami-0123abcdThe 3 elements of a short, specific message

Common ways to write guards

variable "account_id" {
  type = string
  validation {
    condition     = can(regex("^[0-9]{12}
NicheeLab を読み込み中…
quot;, var.account_id)) error_message = "account_id must be a 12-digit number."; } } variable "port" { type = number validation { condition = var.port >= 1 && var.port <= 65535 error_message = "port must be in the range 1-65535."; } }

Common Validation Patterns for Strings and Numbers

Lean on readability for simple patterns. Use startswith for prefix checks, regex for strict formats, and plain comparisons for ranges.

regex tends to hurt maintainability, so keep it just-enough and prefer startswith or length when an alternative exists.

  • Prefix: startswith(var.id, "ami-")
  • Range: var.replicas >= 2 && var.replicas <= 10
  • Minimum length: length(var.name) >= 3
  • Enumeration: contains(["dev","stg","prod"], var.env)
GoalExample conditionExample error message
AMI formatcan(regex("^ami-[0-9a-f]+
NicheeLab を読み込み中…
quot;, var.ami))
ami must be a hex ID starting with 'ami-'.
Minimum name lengthlength(var.name) >= 3name must be at least 3 characters long.
Replica count rangevar.replicas >= 2 && var.replicas <= 10replicas must be in the range 2-10.
Environment enumerationcontains(["dev","stg","prod"], var.env)env must be one of dev/stg/prod.

Guidance for choosing a pattern

What kind of requirement?Simple prefix/suffixstartswith/endswithStrict formatregex + canNumeric rangeComparison operatorsAllowed setcontains on listChoosing the right function for each requirement type

Examples: port and AMI

variable "port" {
  type = number
  validation {
    condition     = var.port >= 1 && var.port <= 65535
    error_message = "port must be in the range 1-65535.";
  }
}

variable "ami" {
  type = string
  validation {
    condition     = can(regex("^ami-[0-9a-f]+
NicheeLab を読み込み中…
quot;, var.ami)) error_message = "ami must be a valid ID starting with 'ami-'."; } }

Validating Collections and Composite Types

For lists, maps, and objects, combine for expressions with alltrue/anytrue to validate every element. Use distinct to enforce no-duplicates.

Constraints on tags or on key length and character classes are easier to maintain when you break the condition into readable units.

  • Prefix check across all elements: alltrue([for s in var.subnets : startswith(s, "subnet-")])
  • No duplicates: length(distinct(var.names)) == length(var.names)
  • Allowed set: alltrue([for e in var.envs : contains(["dev","stg","prod"], e)])
  • Map key/value length: alltrue([for k, v in var.tags : length(k) <= 128 && length(v) <= 256])
PatternCondition (excerpt)Notes
Per-element format checkalltrue([for s in var.subnets : startswith(s, "subnet-")])More readable than regex for simple formats
No duplicateslength(distinct(var.ids)) == length(var.ids)Use distinct to check uniqueness
Required keys presentalltrue([for k in ["Name","Owner"] : contains(keys(var.tags), k)])Combine keys(map) with contains

Flow for validating elements with a for expression

list/mapfor expression: each element to boolbool arrayalltrueAre all true?list/map → for expression turns each element into a bool → bool array → judged with alltrue

Examples: subnet IDs and tags

variable "subnets" {
  type = list(string)
  validation {
    condition     = length(var.subnets) > 0 && alltrue([for s in var.subnets : startswith(s, "subnet-")])
    error_message = "subnets must contain at least 1 item, and each element must be an ID starting with 'subnet-'.";
  }
}

variable "tags" {
  type = map(string)
  validation {
    condition     = alltrue([for k, v in var.tags : length(k) <= 128 && length(v) <= 256])
    error_message = "tags keys must be at most 128 chars and values at most 256 chars.";
  }
}

variable "names" {
  type = list(string)
  validation {
    condition     = length(distinct(var.names)) == length(var.names)
    error_message = "names must not contain duplicate values.";
  }
}

Conditional Validation and Handling Unknown Values

Conditional validation is most readable when expressed as implication. To apply a strict constraint only when env is prod, write env != "prod" || strictCondition.

When unknown values (values not yet resolved at plan time) are involved, Terraform will not error unless the condition can be decisively determined to be false. Wrap spots like regex where a malformed input could blow up evaluation itself in can() to keep things safe.

  • Idiomatic implication: (allow when A does not hold) or (impose B when A holds)
  • Don't accidentally turn unknowns into evaluation errors — guard with can()
  • Do not depend on external resource attributes or data source values — they cannot be referenced in the first place
SituationTerraform behaviorMitigation / how to write it
Strict only for prodCondition resolves to a definite true/falseenv != "prod" || strictCondition
Unknown value involvedIf the result is unclear, plan proceeds and re-evaluates at applyGuard with can() or other safe functions
Malformed patternExpression evaluation itself errors outPair things like regex with can()

How unknown evaluation looks

condition evaluationtruecontinuefalseerrorunknownre-evaluated at applyresolves to true/falsecondition evaluation has three branches: true, false, unknown

Apply a stricter constraint only when env is prod

variable "env" {
  type = string
  validation {
    condition     = contains(["dev","stg","prod"], var.env)
    error_message = "env must be one of dev/stg/prod.";
  }
}

variable "az_count" {
  type = number
  validation {
    condition     = var.env != "prod" || var.az_count >= 2
    error_message = "When env=prod, az_count must be at least 2.";
  }
}

Exam Prep: Tested Points and Anti-Patterns

Exams tend to test the role split between type constraints and validation, the difference vs precondition/postcondition, evaluation timing, and unknown-value handling. validation is a static check on input values and cannot reference resource attributes or data source results.

Common anti-patterns: complex regex that destroys readability, depending on external state, and error messages that are too abstract. Narrow with types first, then secure adequate safety with simple functions.

  • validation lives inside variable blocks. Use precondition/postcondition for resource-state checks
  • resource attributes and data references are not allowed (out of variable scope)
  • Split multiple conditions for readability. Use can() to prevent evaluation errors
  • Include guidance for fixing the input in your message (expected value, range, example)
FeatureDefined inExecution timingPrimary use
type constraintvariableAt value resolution (pre-plan stage)Basic type/schema constraints
validationvariableWhen the value becomes known (mainly at plan time)Declarative business-rule checks
pre/postconditionresource or module callState checks during plan/applyChecks on resource attributes and module outputs

Do/Don't notes

[OK] Use type for the shape → validation for the rules
[OK] Branch with implication driven by env
[NG] Reference resource attributes or data in validation
[NG] Overly complex regex that destroys readability

Anti-pattern fix example

# Bad example (cannot reference resource attributes)
# variable "size" {
#   validation {
#     condition     = var.size <= aws_instance.example.cpu_core_count  # not allowed
#     error_message = "...";
#   }
# }

# Good example: validation for relationships between inputs, precondition for resource state
variable "size" {
  type = number
  validation {
    condition     = var.size >= 1 && var.size <= 64
    error_message = "size must be in the range 1-64.";
  }
}

# (Reference) Use precondition/postcondition on the resource side for state checks
# resource "aws_instance" "example" {
#   # ...
#   lifecycle {
#     precondition {
#       condition     = self.cpu_core_count >= var.size
#       error_message = "The instance type does not satisfy size.";
#     }
#   }
# }

Check Your Understanding

Associate / Pro

問題 1

A module has two variables, env and instance_count. You want to require instance_count to be at least 3 only when env=prod, and impose no restriction otherwise. Which validation is the best fit?

  1. A. variable "instance_count" { type = number validation { condition = var.env != "prod" || var.instance_count >= 3 error_message = "When env=prod, specify a value of 3 or more." } }
  2. B. variable "instance_count" { type = number validation { condition = var.instance_count >= 3 error_message = "Always specify a value of 3 or more." } }
  3. C. variable "instance_count" { type = number validation { condition = contains(["dev","stg"], var.env) && var.instance_count >= 3 error_message = "dev/stg requires 3 or more." } }
  4. D. Compare env and instance_count via a resource precondition (no validation on the variable)

正解: A

The idiomatic way to express an env-driven conditional constraint is implication. env != "prod" || var.instance_count >= 3 unconditionally allows non-prod and imposes the lower bound only when prod. B always requires 3+, which is wrong. C wrongly restricts dev/stg. D is also wrong because input-value consistency belongs in variable validation (state checks are for pre/postcondition).

Frequently Asked Questions

When is validation evaluated — at plan or apply time?

If the value is known, validation runs at plan time and a false result stops the plan with an error. If the value is unknown and the result is indeterminate, evaluation is deferred to the apply stage.

Can I define multiple validation blocks on a single variable?

Yes. But readability drops as conditions pile up, so in practice it is best to combine related checks into a single readable expression, or keep a small number of logically separated blocks.

Can validation reference resource attributes or data source values?

No. validation checks input variables only and cannot reference resource or data attributes. Use validation for relationships between inputs, and pre/postcondition for resource-state consistency.

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

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.


Related articles
Terraform

HCL Syntax: Terraform's Configuration Language (2026)

HCL2 fundamentals for Terraform — blocks, attributes, expres...

Terraform

Terraform Authoring & Operations Pro: Complete Guide (2026)

Tactics for the Terraform Pro exam — module authoring, works...

Terraform

Terraform Providers: Plugin Management Fundamentals (2026)

Provider mechanics — required_providers, versions, mirrors, ...

Terraform

Terraform Resource Blocks: Declarative Infra Units (2026)

Resource block fundamentals — addresses, references, common ...

Terraform

Terraform Data Sources: Read-Only External Data (2026)

Data source basics — declaration, refresh behavior, dependen...

Browse all Terraform articles (102)
© 2026 NicheeLab All rights reserved.