The Error
You run terraform plan or terraform apply and get stopped cold:
โ Error: Invalid function argument
โ
โ on main.tf line 14, in resource "aws_security_group" "example":
โ 14: cidr_blocks = tolist(var.allowed_cidr)
โ
โ Invalid value for "list" parameter: cannot convert string to list of any
โ single type.
The root cause is a type mismatch. You're passing a string to a function that expects a list or set โ such as tolist(), toset(), length(), element(), or join(). Terraform refuses to guess what you meant and fails immediately at plan time.
Why This Happens
Terraform's type system doesn't do silent coercions. Every built-in function has strictly typed parameters, and a wrong type means a hard failure โ no warnings, no fallbacks. Four situations trigger this most often:
- A variable is declared
type = stringbut gets passed to a list-expecting function - A module output returns a single string, but the calling module treats it as a list
- Someone wrote a comma-separated string like
"10.0.0.0/8,192.168.0.0/16"assuming Terraform would split it automatically โ it won't - A data source attribute is a string scalar, not a list (common with
aws_amiIDs, ARNs, etc.)
Step-by-Step Fix
Step 1 โ Identify the actual type of your value
Open the .tf file mentioned in the error and find the variable or attribute declaration. Here's a typical offender:
# WRONG โ declared as string, used as list
variable "allowed_cidr" {
type = string
default = "10.0.0.0/8"
}
resource "aws_security_group" "example" {
ingress {
cidr_blocks = tolist(var.allowed_cidr) # โ ERROR here
}
}
var.allowed_cidr is a plain string. tolist() needs a collection โ a set or tuple. That mismatch is the entire problem.
Step 2 โ Fix the variable type declaration
The cleanest fix: declare the variable as a list from the start.
# CORRECT โ declare as list
variable "allowed_cidr" {
type = list(string)
default = ["10.0.0.0/8"]
}
resource "aws_security_group" "example" {
ingress {
cidr_blocks = var.allowed_cidr # No conversion needed
}
}
Notice you don't even need tolist() anymore. A list(string) variable is already the right type for cidr_blocks โ just use it directly.
Step 3 โ If you must accept a string, use split() to convert it
CI pipelines, environment variables, and legacy configs often pass values as comma-separated strings. You can't always change the input format. Use split() to turn it into a real list:
variable "allowed_cidr" {
type = string
default = "10.0.0.0/8,192.168.0.0/16"
}
locals {
cidr_list = split(",", var.allowed_cidr)
}
resource "aws_security_group" "example" {
ingress {
cidr_blocks = local.cidr_list
}
}
Putting the conversion in a local keeps your resource blocks clean and makes the intent obvious to anyone reading the code later.
Step 4 โ Use toset() for unique collections (e.g. for_each)
for_each requires a set or map โ not a string, not a list. If your variable is still typed as string, this blows up:
# WRONG
resource "aws_iam_user" "example" {
for_each = toset(var.username) # var.username is a string โ ERROR
name = each.value
}
# CORRECT
variable "usernames" {
type = list(string)
default = ["alice", "bob", "carol"]
}
resource "aws_iam_user" "example" {
for_each = toset(var.usernames) # list โ set: OK
name = each.value
}
toset() converts a list to a set (deduplicating values in the process). It works on lists โ not strings.
Step 5 โ Wrap a single string in brackets when needed
Data source attributes like AMI IDs, ARNs, and account IDs are always string scalars. Most of the time that's fine โ but occasionally a downstream resource expects a list. Wrap it with brackets:
data "aws_ami" "ubuntu" {
most_recent = true
# ...
}
# data.aws_ami.ubuntu.id is a string
resource "aws_launch_template" "example" {
image_id = data.aws_ami.ubuntu.id # fine as-is
# Somewhere that needs a list:
# security_group_names = [data.aws_ami.ubuntu.id] โ wrap it
}
Verify the Fix
Start with terraform validate โ it catches type errors instantly, no API calls required:
terraform validate
A clean config outputs:
Success! The configuration is valid.
Then run terraform plan to confirm no runtime type issues remain. If the plan shows your resources with the correct CIDR blocks (or whichever attribute you fixed), you're done.
Quick Cheat Sheet: Type Conversion Functions
split(",", string)โ string โ list(string), splits on a delimitertolist(set_or_tuple)โ set/tuple โ list (does not work on strings)toset(list)โ list โ set, removes duplicates (does not work on strings)[value]โ wraps any scalar in a single-element list literalcompact(list)โ strips empty strings from a listflatten(list_of_lists)โ collapses nested lists into one flat list
Tips to Avoid This Error
- Declare variable types explicitly. Skip
type = anyโ it hides type problems until runtime, usually at the worst possible moment. - Read the function docs before using it. Every Terraform built-in has a clear signature showing what types each parameter accepts. Thirty seconds of reading saves twenty minutes of debugging.
- Test expressions in
terraform consolebefore committing them to config:
$ terraform console
> tolist(["a", "b"])
[
"a",
"b",
]
> tolist("not-a-list")
โ Error: Invalid function argument
โ cannot convert string to list of any single type.
- Add
terraform validateto your CI pipeline. It runs in under a second, requires no credentials, and catches type mismatches before they ever reachplan.

