Terraform’s type system looks simple at first glance—strings, numbers, lists—but it gets interesting once you start modeling real infrastructure inputs. One type that trips up even experienced users is set.
It behaves like a list, but with strict rules around uniqueness and ordering. If you’ve ever had Terraform “shuffle” your values or throw odd diffs, chances are a set was involved.
What is a set in Terraform?
A set is an unordered collection of unique values. Unlike lists, sets do not preserve order and automatically remove duplicates.
Basic example:
1variable "allowed_ips" {
2 type = set(string)
3}
4If a user provides:
1allowed_ips = [
2 "192.168.1.1",
3 "192.168.1.2",
4 "192.168.1.1"
5]
6Terraform will internally treat it as:
1{
2 "192.168.1.1",
3 "192.168.1.2"
4}
5Duplicate values are removed automatically.
Why use a set instead of a list?
Here’s where things get practical.
- You don’t care about order
- You want automatic deduplication
- You need stable comparisons regardless of input order
A common use case is security rules, tags, or identifiers where order has no semantic meaning.
Example: Security group rules
1variable "open_ports" {
2 type = set(number)
3 default = [22, 80, 443, 80]
4}
5This ensures you never accidentally create duplicate rules.
Here’s where things get tricky: Ordering
Sets are unordered. That means:
- Terraform may display values in a different order than you defined
- Plan output can look confusing
- You cannot rely on index-based access
This will not work as expected:
1var.allowed_ips[0]Because sets don’t guarantee position.
If ordering matters, use a list instead.
Converting between list and set
You’ll often need to switch between types. Terraform provides helper functions.
Convert list to set
1locals {
2 unique_ips = toset(var.allowed_ips)
3}Convert set to list
1locals {
2 ip_list = tolist(var.allowed_ips)
3}Be careful: converting a set to a list introduces non-deterministic ordering.
Using set with objects
Sets aren’t limited to primitive types. You can define sets of complex objects.
1variable "users" {
2 type = set(object({
3 name = string
4 role = string
5 }))
6}Terraform determines uniqueness based on the entire object structure.
Example input:
1users = [
2 { name = "alice", role = "admin" },
3 { name = "bob", role = "dev" },
4 { name = "alice", role = "admin" }
5]
6The duplicate object will be removed.
Subtle gotcha
If two objects differ even slightly, Terraform treats them as distinct:
1{ name = "alice", role = "admin" }
2{ name = "alice", role = "Admin" }These are considered different due to case sensitivity.
Using sets with for_each
Sets work well with for_each, but there’s a constraint: Terraform needs stable keys.
This works:
1resource "aws_security_group_rule" "example" {
2 for_each = var.allowed_ips
3
4 cidr_blocks = [each.value]
5 from_port = 80
6 to_port = 80
7 protocol = "tcp"
8}Each value becomes its own instance.
But with complex objects, you may need to transform the set into a map:
1locals {
2 user_map = {
3 for user in var.users : user.name => user
4 }
5}
6This ensures predictable keys.
A common mistake developers make
Using a set when order actually matters.
Example scenario:
- Provisioning subnets in a specific sequence
- Assigning priorities
- Index-based resource references
In these cases, a list is the correct choice. Using a set will eventually lead to confusing diffs or broken assumptions.
Validation with set types
You can enforce constraints on sets using validation blocks.
1variable "allowed_regions" {
2 type = set(string)
3
4 validation {
5 condition = alltrue([
6 for region in var.allowed_regions :
7 contains(["us-east-1", "us-west-1"], region)
8 ])
9 error_message = "Only approved regions are allowed."
10 }
11}This keeps your infrastructure inputs clean and predictable.
When should you reach for set?
Use a set when:
- Duplicates must be eliminated automatically
- Order is irrelevant
- You want consistent comparisons across runs
Avoid it when:
- You rely on ordering
- You need index access
- You expect stable output formatting
Final thought
The set type in Terraform is deceptively simple. It’s powerful for enforcing uniqueness and simplifying input handling, but it can introduce subtle bugs if you assume list-like behavior.
Once you internalize that sets are unordered and deduplicated by design, they become a reliable tool in your Terraform toolkit—especially for modeling real-world infrastructure constraints.