Variable Validation in Terraform
Write custom validation in variables — Terraform foundations tutorial, lesson 22. Learn how to enforce custom rules on variable values to prevent misconfigurations, with hands-on examples and troubleshooting tips.
Focus: write custom validation in variables
You've written Terraform configurations that deploy resources, but what happens when someone passes a value that breaks your infrastructure? Terraform's built-in type constraints only check types, not business rules — so a string variable will happily accept "prod" or "production" even when only one is valid. Without custom validation, you only discover these mistakes during terraform apply, often after partial creation and wasted cycles. In this lesson, you'll learn how to write custom validation in variables — the guardrails that catch bad values before they ever reach the API. By the end, you'll be able to enforce everything from simple regex matches to multi-field cross-checks, making your modules safer and your pipelines more reliable.
The problem this lesson solves
Imagine a module that provisions an AWS S3 bucket. You define a variable like this:
variable "bucket_name" {
type = string
}
It compiles fine. But what if someone passes "MY_BUCKET"? Or "my bucket"? Terraform will accept both, and only later — during terraform apply — will AWS reject the name because of invalid characters or spaces. By then, you might have already created dependent resources, and you're left with a half-applied state and a confusing error message from the provider.
Cost of missing validation: - Slow feedback loops: errors surface at apply time, not plan time. - Inconsistent states: different users pass different values, causing drift. - Hard-to-debug failures: provider errors are often cryptic and far from the root cause.
The fix: Terraform's validation block inside a variable declaration acts as a pre-flight check. It runs during terraform plan and terraform validate, so you fail fast with a message you control.
Core concept / mental model
Think of a Terraform variable as a gatekeeper. The type constraint is the bouncer that checks for ID — it only verifies that the value is a string, a number, a list, etc. The validation block is the security checklist — it asks deeper questions: Is this string in the allowed set? Does it match a regex? Is this number within a sane range? If the checklist fails, the gatekeeper turns the value away with a custom error message you've written.
Anatomy of a validation block:
variable "environment" {
type = string
validation {
condition = var.environment == "dev" || var.environment == "prod"
error_message = "The environment must be either 'dev' or 'prod'."
}
}
Key parts:
- condition — any expression that evaluates to true or false. The validation passes only when this is true.
- error_message — a string that shows up in the error output. Keep it human-readable and specific.
You can have multiple validation blocks in one variable — Terraform treats them as a logical AND (all must pass). If any fails, the first error message (or all? — actually Terraform collects all failed validations and shows them together) is shown in the error output.
Pro tip: Validation happens during
terraform validateandterraform plan— not duringterraform applyif you skip plan. So always runterraform plan(orterraform applywith-auto-approvedoes run plan internally; either way, the check happens before any resource changes). You'll never see a validation failure mid-apply.
How it works step by step
Let's trace what happens when you define a validation and then pass a bad value.
Step 1: Define the variable with validation.
variable "instance_type" {
type = string
validation {
condition = contains(["t2.micro", "t3.micro", "t3.small"], var.instance_type)
error_message = "Instance type must be one of t2.micro, t3.micro, or t3.small."
}
}
Step 2: Run terraform validate or terraform plan.
- If you pass
var.instance_type = "t3.large", the condition is false → validation fails. - Terraform shows an error like:
Error: Invalid value for variable
on variables.tf line 4, in variable "instance_type":
4: variable "instance_type" {
The instance type must be one of t2.micro, t3.micro, or t3.small.
Step 3: The error is raised before any resource is created or modified.
Why this matters: The failure happens during plan, so your state file remains untouched, and you avoid partial applies. You can also use can(), try(), and functions like regexall() to build complex conditions.
Important details:
- The condition expression can reference var.<name> but must return a bool. Terraform will auto-convert if possible (e.g., can() returns bool), but it's safer to write explicit boolean logic.
- You can't use count or for_each on the validation itself; it's static per variable.
- Validation blocks are available for any variable type: strings, numbers, lists, maps, objects, and even any.
Hands-on walkthrough
Let's build a realistic example: a module that accepts a database config with strict rules. We'll create variables.tf and a main.tf that uses the variables.
# variables.tf
variable "db_name" {
type = string
validation {
condition = can(regex("^[a-z][a-z0-9_]{0,63}$", var.db_name))
error_message = "db_name must start with a lowercase letter and contain only lowercase letters, digits, and underscores (max 64 chars)."
}
}
variable "db_port" {
type = number
validation {
condition = var.db_port >= 1024 && var.db_port <= 65535
error_message = "db_port must be between 1024 and 65535."
}
validation {
condition = var.db_port != 5432
error_message = "db_port 5432 is reserved for the default Postgres port."
}
}
variable "db_tags" {
type = map(string)
validation {
condition = var.db_tags["owner"] != ""
error_message = "db_tags must include an 'owner' key with a non-empty value."
}
}
# main.tf
resource "aws_instance" "db" {
ami = "ami-12345678"
instance_type = "t3.micro"
tags = merge(var.db_tags, {
Name = var.db_name
})
}
Then run terraform validate. If you pass db_tags = { team = "data" } (missing owner), you'll get an error. Let's test with terraform plan:
# terraform.tfvars
db_name = "my_db_1"
db_port = 3306
db_tags = {
owner = "data-team"
}
✅ All validations pass → plan proceeds.
Now change db_port to 5432:
db_name = "my_db_1"
db_port = 5432
db_tags = {
owner = "data-team"
}
Run terraform plan:
Error: Invalid value for variable
on variables.tf line 10, in variable "db_port":
2: condition = var.db_port != 5432
The db_port 5432 is reserved for the default Postgres port.
What's happening? Terraform evaluates both validations for db_port. The first (>= 1024 && <= 65535) passes, but the second fails, so the plan is aborted.
Another example with can(): Suppose you want to ensure a string is parseable as an IP address:
variable "cidr_block" {
type = string
validation {
condition = can(cidrhost(var.cidr_block, 0))
error_message = "cidr_block must be a valid CIDR notation (e.g., '10.0.0.0/16')."
}
}
Word count target: keep the walkthrough focused but complete.
Compare options / when to choose what
| Approach | Use case | Pros | Cons |
|---|---|---|---|
| Type constraint only | Simple type safety | Minimal code | No business rules |
| Custom validation block | Business rules on single variable | Fast fail, custom errors | One variable per rule; no cross-variable checks |
locals + validate with terraform console |
Testing expressions manually | Quick iteration | Not part of config; not automated |
| Precondition/Postcondition on resources | Cross-resource or state-dependent checks | Powerful, checks against current state | Runs during apply; doesn't prevent all bad plan values |
When to choose validation block: - The rule depends only on the value of that variable. - You want to catch mistakes early (plan-time). - You want to keep your module self-contained.
When to choose resource preconditions: - The rule depends on more than one variable. - The rule depends on the value of another resource (e.g., a name must be unique across an account).
When to use can():
- When you want to test whether a value can be parsed by a function (e.g., regex, cidrhost). It returns true if the function succeeds, false otherwise.
Variations:
- Use regex() for pattern matching, but regex() returns a list of matches; you can use can(regex(pattern, var.value)) to test existence.
- Use contains() for whitelist checks.
- Use length() for string/number range comparisons.
Troubleshooting & edge cases
1. The validation block is ignored — plan succeeds with a bad value.
- Cause: The condition expression might reference a wrong variable name, or the condition always evaluates to true.
- Fix: Run terraform validate and inspect the condition. Add a temporary output with the condition result to debug.
2. Error message is not shown.
- Cause: The validate command may be using a different state or variable file. Ensure you're in the right directory.
3. Invalid expression in validation condition.
- Cause: Using a function unavailable in variable context (e.g., file() or templatefile()). Only pure functions are allowed.
4. Condition returns a non-bool value.
- Terraform will attempt to convert, but if it can't, the error is cryptic. Always write conditions that return true/false.
5. Handling null values.
- If a variable is optional and set to null, a validation condition may fail on null. Use var.x == null ? true : (your condition).
6. Across-variable validation isn't possible in the variable block.
- You cannot validate that var.a != var.b inside a variable's validation block. Use a precondition on a resource or terraform validate with check blocks (in Terraform 1.5+).
7. Performance with large lists — if you have a huge whitelist, keep it in a local or use a regex alternation pattern.
What you learned & what's next
You now know how to write custom validation in variables — setting up validation blocks, using functions like can(), contains(), and regex(), and understanding when to use these over other guardrails. You can explain the core idea behind variable validation, and you've completed a practical exercise that catches invalid database configurations before they hit your infrastructure.
Next in the track: In the next lesson, you'll learn about preconditions and postconditions — how to enforce rules that cross resource boundaries, like checking that a security group is attached to a VM. That's the natural next step: moving from per-variable checks to per-resource state guards.
Now go add validation to every variable that has a non-obvious rule — your future self (and your teammates) will thank you.
Practice recap
Create a new Terraform module for an EC2 instance. Add variables for instance_type, name, and tags. Write validation to ensure instance_type is in a whitelist, name matches ^[a-z][a-z0-9-]*$, and tags includes an owner key. Run terraform validate with a bad value and a good value to see the error output. Experiment with using can() and contains() in your conditions.
Common mistakes
- Writing conditions that always evaluate to
true— e.g., missing thevar.reference — so validation never triggers. - Forgetting to handle
nullvalues for optional variables; the condition calls a function onnulland crashes. - Trying to validate across multiple variables in a single
validationblock — that's not supported; use resource preconditions instead.
Variations
- Use
can(regex(...))instead ofcontains()for pattern-free validation when you have a regex for allowed formats. - Implement
preconditionblocks on resources to validate cross-variable rules, such as ensuring two variables don't conflict. - Leverage
terraform consoleto test your validation condition expressions interactively before committing them to code.
Real-world use cases
- Enforce a strict naming convention for AWS S3 buckets (e.g., lowercase, no underscores) with a regex validation in a shared module.
- Prevent accidental use of the default database port (5432) in a Postgres RDS module by adding a validation block with a clear error.
- Validate that a map of tags includes a mandatory
ownerkey with a non-empty value, ensuring every resource has an accountable owner.
Key takeaways
- The
validationblock runs duringterraform validateandplan, stopping bad values before any resource changes. - Each
validationblock pairs a booleanconditionwith a customerror_messageyou write. - Use functions like
contains(),regex(), andcan()to build expressive conditions. - Multiple validation blocks on one variable are combined with AND logic — all must pass.
- Validation is per-variable only; for cross-variable rules, use resource preconditions or
checkblocks. - Need to handle
nullvalues explicitly, especially for optional variables.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.