Is for_each the only way to retrieve values from a Terraform set?

Terraform recently introduced the set datatype, described on this page as:

set(...): a collection of unique values that do not have any secondary identifiers or ordering.

It is difficult to find documentation on how to retrieve values from Terraform sets. With a map, you can index on the key:

password = var.passwords["kevin"]

With a list, you can index on the element number:

a_record = var.records[1]

However, I cannot use either of those methods to retrieve values from a set, even a set with only a single item.

In other places, the documentation mentions for_each as a method to get values out of a set.

variable "subnet_ids" {
  type = list(string)
}

resource "aws_instance" "server" {
  for_each = toset(var.subnet_ids)

  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"
  subnet_id     = each.key # note: each.key and each.value are the same for a set

  tags = {
    Name = "Server ${each.key}"
  }
}

Is the for_each meta variable the only way to access the values in a set?


You can also slice a set by first casting it to a list and then accessing it as a list.

As an example:

variable "set" {
  type = set(string)
  default = [
    "foo",
    "bar",
  ]
}

output "set" {
  value = var.set
}

output "set_first_element" {
  value = var.set[0]
}

This will error because sets can't be directly accessed by an index:

Error: Invalid index

  on main.tf line 14, in output "set_first_element":
  14:   value = var.set[0]

This value does not have any indices.

If you instead cast it with the tolist function then you can access it as expected:

output "set_to_list_first_element" {
  value = tolist(var.set)[0]
}
Outputs:

set = [
  "bar",
  "foo",
]
set_to_list_first_element = bar

Note that this returns bar instead of foo. Strictly speaking sets are unordered in Terraform so you can't rely on the order at all and are only consistent during a single run of Terraform but in practice they are stable and in the the case of set(string) they are sorted in lexicographical order:

When a set is converted to a list or tuple, the elements will be in an arbitrary order. If the set's elements were strings, they will be in lexicographical order; sets of other element types do not guarantee any particular order of elements.

The main place this comes up is if you are dealing with a resource or data source that returns a set type in Terraform 0.12 but need to use a singular value. A basic example might be something like this:

data "aws_subnet_ids" "private" {
  vpc_id = var.vpc_id

  tags = {
    Tier = "Private"
  }
}

resource "aws_instance" "app" {
  ami           = var.ami
  instance_type = "t2.micro"
  subnet_id     = tolist(data.aws_subnet_ids.example.ids)[0]
}

This would create an EC2 instance in a subnet tagged with Tier = Private but set no other constraint on where it should be.

While you referenced being able to access values with for_each you can also loop through a set with a for expression:

variable "set_of_objects" {
  type = set(object({
    port    = number
    service = string
  }))

  default = [
    {
      port    = 22
      service = "ssh"
    },
    {
      port    = 80
      service = "http"
    },
  ]
}

output "set_of_objects" {
  value = var.set_of_objects
}

output "list_comprehension_over_set" {
  value = [ for obj in var.set_of_objects : upper(obj.service) ]
}

This then outputs the following:

list_comprehension_over_set = [
  "SSH",
  "HTTP",
]
set_of_objects = [
  {
    "port" = 22
    "service" = "ssh"
  },
  {
    "port" = 80
    "service" = "http"
  },
]