4
votes

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?

1
Can you share how you've tried to access elements from a set and the errors you got from doing that? Normally you'd just cast it to a list and slice it.ydaetskcoR
...which is not something I have ever seen before or had to do myself in my four years of Terraform, so this use case and that comment (hopefully soon answer) are both interesting to me. Other than that, i would have said "yes" to the question, except that for_each is not a meta variable (sorry to be pedantic).Matt Schuchard
Why would you say for_each is not a meta variable? terraform.io/docs/configuration/resources.html#meta-arguments lists it as one of the meta arguments available for all resources although it's more complex than that as it also appears in dynamic blocks where the others are not (mostly because dynamic requires it).ydaetskcoR

1 Answers

11
votes

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"
  },
]