How to Fix Terraform Module Input Type Mismatch Error (Invalid Value)
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5 mins
TL;DR
- What broke: Terraform received a value whose type (e.g.,
string) doesn't satisfy the module variable's declared type constraint (e.g.,list(string)ornumber), causing an immediate plan failure. - How to fix it: Align the value passed in the module block with the exact type constraint declared in the module's
variableblock — wrap strings intolist()/toset(), cast numbers explicitly, or fix the constraint itself. - Use our Client-Side Sandbox below to auto-refactor this — paste your module call and variable definition to get corrected code instantly.
The Incident (What Does the Error Mean?)
Raw error output from terraform plan or terraform apply:
Error: Invalid value for module input
on main.tf line 12, in module "vpc":
12: subnet_count = "three"
The given value is not suitable for child module variable "subnet_count"
defined at modules/vpc/variables.tf:4,1-29: a number is required.
Terraform's type system is enforced at plan time, not apply time. The moment the type constraint check fails, the entire graph evaluation halts. No partial plans are generated. In a CI/CD pipeline this means the pipeline exits non-zero and any downstream apply step is blocked. In a monorepo with module chains, one bad input can cascade and block unrelated workspace plans if they share a root module.
The Attack Vector / Blast Radius
This isn't a security exploit vector, but the blast radius is operationally severe:
- Pipeline paralysis: Every
terraform planin the affected workspace fails until the mismatch is resolved. If your org uses a single root module calling 10+ child modules, one type mismatch blocks all of them. - Drift accumulation: While the plan is broken, engineers may resort to manual console changes to unblock themselves — creating infrastructure drift that Terraform will fight against on the next successful plan.
- Subtle data corruption risk: The more dangerous variant is when Terraform coerces a type instead of rejecting it (e.g., passing
"1"wherenumberis expected — Terraform will auto-convert this, silently). This means yourcountorfor_eachlogic runs on a coerced value that may not be what the module author intended, provisioning the wrong number of resources without any error. - State file inconsistency: If a partial refactor is applied before the type error surfaces in a dependent module, you can end up with orphaned resources in state.
How to Fix It (The Solution)
Basic Fix — Correct the Passed Value
The most common case: you're passing a string literal where a list(string) or number is required.
# main.tf — module call
module "vpc" {
source = "./modules/vpc"
- subnet_cidrs = "10.0.1.0/24"
+ subnet_cidrs = ["10.0.1.0/24"]
- subnet_count = "three"
+ subnet_count = 3
}
# modules/vpc/variables.tf — variable definition (no change needed here if correct)
variable "subnet_cidrs" {
- type = string
+ type = list(string)
description = "List of subnet CIDR blocks"
}
variable "subnet_count" {
type = number
description = "Number of subnets to create"
}
Enterprise Best Practice — Explicit Type Constraints + Validation Blocks
Never rely on Terraform's implicit coercion. Add validation blocks to module variables so mismatches produce actionable, human-readable errors instead of cryptic type failure messages. Also use object() or tuple() types for complex inputs to enforce shape at the boundary.
# modules/vpc/variables.tf
variable "subnet_cidrs" {
type = list(string)
description = "List of subnet CIDR blocks in CIDR notation"
+
+ validation {
+ condition = alltrue([can(cidrhost(cidr, 0)) for cidr in var.subnet_cidrs])
+ error_message = "All elements of subnet_cidrs must be valid CIDR blocks (e.g., 10.0.1.0/24)."
+ }
}
variable "subnet_count" {
type = number
description = "Number of subnets"
+
+ validation {
+ condition = var.subnet_count > 0 && var.subnet_count <= 32
+ error_message = "subnet_count must be a positive integer no greater than 32."
+ }
}
# main.tf — use explicit type conversion functions when sourcing from dynamic data
module "vpc" {
source = "./modules/vpc"
- subnet_cidrs = var.raw_cidrs_string
+ subnet_cidrs = split(",", var.raw_cidrs_string)
- subnet_count = var.count_from_ssm
+ subnet_count = tonumber(var.count_from_ssm)
}
Key conversion functions to know:
| Scenario | Function |
|---|---|
| String → Number | tonumber(val) |
| String → Bool | tobool(val) |
| String → List | split(",", val) or tolist([val]) |
| Any → String | tostring(val) |
| Set → List | tolist(val) |
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
Type mismatches should never reach a human code review. Catch them at commit time.
1. terraform validate as a Pre-Commit Hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.83.0
hooks:
- id: terraform_validate
- id: terraform_tflint
2. TFLint with Deep Type Checking
# .tflint.hcl
plugin "terraform" {
enabled = true
version = "0.5.0"
source = "github.com/terraform-linters/tflint-ruleset-terraform"
}
rule "terraform_typed_variables" {
enabled = true
}
TFLint's terraform_typed_variables rule enforces that every variable has an explicit type constraint — eliminating the any type footgun that allows silent coercions.
3. Checkov Policy for Type Constraint Enforcement
checkov -d . --check CKV_TF_1 --framework terraform
For custom OPA policies in Atlantis or Spacelift:
# policy/terraform_type_constraints.rego
package terraform.module_inputs
deny[msg] {
var := input.variables[_]
not var.type
msg := sprintf("Variable '%v' is missing a type constraint. All module variables must declare explicit types.", [var.name])
}
4. GitHub Actions Gate
# .github/workflows/tf-validate.yml
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "~1.8"
- run: terraform init -backend=false
- run: terraform validate
- run: tflint --recursive
Block the PR merge on any non-zero exit from terraform validate. No exceptions.