Terraform: Length of list or map?

Created on 4 Apr 2015  ยท  19Comments  ยท  Source: hashicorp/terraform

I figure I'm missing something here. How can I calculate the length of a list/map variable?

I'd like to specify a list of AWS availability zones, and generate a subnet per zone (either via a map of values, or the split() hack for now)

variable "zones" {
  default = {
    "0" = "us-west-2a"
    "1" = "us-west-2b"
    "2" = "us-west-2c"
  }
}

resource "aws_subnet" "public" {
  vpc_id = "${aws_vpc.main.id}"
  cidr_block = "10.0.${count.index * 4}.0/22"
  availability_zone = "${lookup(var.zones, count.index)}"
  count = 3 # How do I calculate this?
}
core enhancement

Most helpful comment

You can use the map helper functions to get the length of a map variable:

https://github.com/hashicorp/terraform/issues/1915

e.g.:

variable "aws_az_subnet_map" {
description = "The subnet id for each availability zone"
default = {
us-west-2a = "subnet-abcd1234"
us-west-2b = "subnet-efab5678"
us-west-2c = "subnet-cdef9012"
}
}
resource "aws_instance" "my_instance" {
count = "${length( keys(var.aws_az_subnet_map))}"
subnet_id = "${ element(values(var.aws_az_subnet_map)), count.index }"
}

All 19 comments

:+1:

Agreed. IMO some sort of count / length function should be in core.

I just hit the need for getting the length of a list too.

See #1495

This has been implemented & merged in #1495 , so the issue can be closed I guess?

It appears like #1495 references strings and lists but not maps, which was part of the original request.

@mikattack The original example can be reworked to the following:

variable "zones" {
  default = "us-west-2a,us-west-2b,us-west-2c"
}

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "public" {
  vpc_id = "${aws_vpc.main.id}"
  cidr_block = "10.0.${count.index * 4}.0/22"
  availability_zone = "${element(split(",", var.zones), count.index)}"
  count = "${length(split(",", var.zones))}"
}

not as elegant as if we'd have support for real lists default = ["us-west-2a","us-west-2b","us-west-2c"] , but we'll get there someday. So far this works pretty well for me.

I don't believe zones nor regions are good candidates for maps, it should really IMO be a list instead (whether that's a pseudo-list we've got now or real list in the future).

That said, if there is any valid use-case for ${length(map)}, it can be added, I can't see any at the moment.

This is exactly the problem we ran into. Your workaround with delimited strings and length() solves our problem until lists can be supported.

Agree that there maybe isn't an immediate need for map lengths. Though, if we had key(map) we could achieve the same thing, and that'd be useful for other things.

Going to close this since the core of the issue is implemented and there hasn't been activity since then. Anybody on this thread can follow up here or file a new thread if there's additional work to be tracked here! :+1:

@radeksimko: Actually I expected ${length(var.some_map)} to work out of the box, it was quite surprising to get a number that didn't match the number of entries in the map.

Maps with count, as described in the doc, are arguably a way more elegant solution than split-parsed comma-separated lists, pending actual lists.

The map technique also has the advantage over the list (comma-separated or actual) that it lets you specify several attributes varying over your otherwise similar resources (say a username and a path). Though doable with several lists, you need to keep them in sync, which is harder and uglier to express, and more error-prone:

variable "users" {
  default = {
    "0" = {
      "username" = "username.ofuser1"
      "path" = "/app1"
    }
    "1" = {
      "username" = "username.ofuser2"
      "path" = "/app2"
    }
  }
}

is more declarative than

variable "usernames" {
  default = "username.ofuser1,username.ofuser2,username.ofuser3"
}
variables "userpaths" {
  default = "path1,path2,path1"
}

[Edit: Just realized that I was repeating the first example, removed that part]

:+1: for map length

@ejoubaud Does that variable "users" syntax with the map of maps work? When I try that I get: ```Errors:

  • 1 error(s) occurred:
  • module root: 1 error(s) occurred:
  • Variable 'users': must be a string or a map```

@matthughes: No, good point, it doesn't, not sure why, maybe nested maps. I tend to wish it did though, as it does in JSON. I think I just came up with the example on the spot to illustrate how maps are more expressive than parseable string lists.

You can use the map helper functions to get the length of a map variable:

https://github.com/hashicorp/terraform/issues/1915

e.g.:

variable "aws_az_subnet_map" {
description = "The subnet id for each availability zone"
default = {
us-west-2a = "subnet-abcd1234"
us-west-2b = "subnet-efab5678"
us-west-2c = "subnet-cdef9012"
}
}
resource "aws_instance" "my_instance" {
count = "${length( keys(var.aws_az_subnet_map))}"
subnet_id = "${ element(values(var.aws_az_subnet_map)), count.index }"
}

subnet_id = "${ element(values(var.aws_az_subnet_map)), count.index }"

If I'm right, it should be:
subnet_id = "${ element(values(var.aws_az_subnet_map), count.index )}"

what if we need to add another availability zone? Say:

default = {
us-west-2a = "subnet-abcd1234"
us-west-2b = "subnet-efab5678"
us-west-2c = "subnet-cdef9012"
us-west-2d = "subnet-11223344"
}

when I run terraform plan again, it seems terraform will shuffle the map sequence instead of keeping the same --> force new resource is the last thing we want somehow...

The problem raised by @tuannvm still occurs -- I think @phinze may have closed this issue a little too quickly. For instance, the following azurerm_storage_share resource will attempt to destroy shares (potentially losing data!) when adding an item, regardless of the order within the map:

resource "azurerm_storage_share" "storage_shares" {
  count                = "${length(var.storage_shares)}"
  name                 = "${element(keys(var.storage_shares), count.index)}"
  resource_group_name  = "${var.resource_group_name}"
  storage_account_name = "${azurerm_storage_account.storage_account.name}"
  quota                = "${lookup(var.storage_shares, element(keys(var.storage_shares), count.index))}"
}

The problem also occurs when replacing the quota line with the following interpolation expression:

"${element(values(var.storage_shares), count.index))}"

var.storage_shares looks something like:

{
  "someshare": "100",
  "someothershare": "10"
}

@LukeCarrier I found out that terraform map will follow alpha numeric order! It means no matter how you change the order, it'll be kept like this:

resource "azurerm_storage_share" "storage_shares" {
  count                = "${length(var.storage_shares)}"
  name                 = "${element(keys(var.storage_shares), count.index)}"
  quota                = "${lookup(var.storage_shares, element(keys(var.storage_shares), count.index))}"
  resource_group_name  = "${var.resource_group_name}"
  storage_account_name = "${azurerm_storage_account.storage_account.name}"
}

So, if you add new item that potentially to sit between c (count) and s (storage_account_name), then the map will be suffered. The quick hack will be adding the item with the prefix character from t to z...

@tuannvm I think you're right -- thanks for saving us a ton of time troubleshooting this issue :confused:

It looks like this _is_ a known bug: #14477

I'm going to lock this issue because it has been closed for _30 days_ โณ. This helps our maintainers find and focus on the active issues.

If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

Was this page helpful?
0 / 5 - 0 ratings