Terraform: Using count index to interpolate other variables?

Created on 15 Oct 2014  ยท  16Comments  ยท  Source: hashicorp/terraform

I'm attempting to create 3 subnets, each in different availability zones:

resource "aws_subnet" "coreospub0" {
  vpc_id = "${aws_vpc.coreos.id}"
  cidr_block = "172.64.0.0/22"
  availability_zone = "${var.aws_region}a"
  map_public_ip_on_launch = true
}

resource "aws_subnet" "coreospub1" {
  vpc_id = "${aws_vpc.coreos.id}"
  cidr_block = "172.64.4.0/22"
  availability_zone = "${var.aws_region}b"
  map_public_ip_on_launch = true
}

resource "aws_subnet" "coreospub2" {
  vpc_id = "${aws_vpc.coreos.id}"
  cidr_block = "172.64.8.0/22"
  availability_zone = "${var.aws_region}c"
  map_public_ip_on_launch = true
}

I was then trying to simplify the aws_instance resource duplication by using count and interpolating subnet_id:

resource "aws_instance" "coreos" {
  ami = "${lookup(var.aws_amis, var.aws_region)}"
  instance_type = "${var.aws_instance_type}"
  count = 3
  key_name = "${var.aws_key_name}"
  security_groups = ["${aws_security_group.ssh.id}", "${aws_security_group.self.id}"]
  subnet_id = "${aws_subnet.coreospub${count.index}.id}"
}

But I end up with a plan that tries to reference the string and not the actual subnet_id:

+ aws_instance.coreos.2
...
    subnet_id:         "" => "${aws_subnet.coreospub2.id}"

Is there any way to accomplish this yet?

documentation

Most helpful comment

Just in case someone lands here trying to achieve this with 0.7 or later, the way to do this without relying on concat is:

lookup(var.vpc_availability_zone, "zone_${count.index}")

Or, in my case:

lookup(var.vpc_availability_zone, "zone_${count.index % 3}")

All 16 comments

This was also requested in #398. Wrote up my notes in that thread.

Hmm, I'm not sure if it is the same yet. For static things (like a hostname) you can use ${count.index} as of 0.3.0. Are you trying to use count.index as a reference to other interpolated variables like I am? The count.index does work and the variable I want to interpolate is listed as the subnet_id. But I think it is just a double interpolation problem (${aws_subnet.coreospub2.id}needs to be interpolated again).

I think we need to document how to do this. You should use map structures for this (map variable types) with the lookup function. I've tagged as docs.

@andyshinn yep you're right and I was unaware of the count.index until this thread. thanks!

@mitchellh Using the lookup function works if the values are static. But if they're dynamic (like the subnet ids in this example), you can't reference them in a map since variable values are required to be static. Is there another way to accomplish this now?

I just submitted https://github.com/hashicorp/terraform/pull/554 which helps solve this problem in a slightly different way. There's a new "element" interpolation function which you can use to get a specific index from a splat. So for the example above, if you use the count = 3 to generate your subnets as a multi-value variable like:

variable "zones" {
    default = {
        zone0 = "us-west-2a"
        zone1 = "us-west-2b"
        zone2 = "us-west-2c"
    }
}

variable "cidr_blocks" {
    default = {
        zone0 = "172.64.0.0/22"
        zone1 = "172.64.0.4/22"
        zone2 = "172.64.0.8/22"
    }
}

resource "aws_subnet" "coreospub" {
  vpc_id = "${aws_vpc.coreos.id}"
  cidr_block = "${lookup(var.cidr_blocks, concat("zone", count.index))}"
  availability_zone = "${lookup(var.zones, concat("zone", count.index))}"
  map_public_ip_on_launch = true
  count = 3
}

You then get a aws_subnet.coreospub.*.id splat to play around with. To use that when creating instances with the new element function, it'd look like:

resource "aws_instance" "coreos" {
  ami = "${lookup(var.aws_amis, var.aws_region)}"
  instance_type = "${var.aws_instance_type}"
  count = 3
  key_name = "${var.aws_key_name}"
  security_groups = ["${aws_security_group.ssh.id}", "${aws_security_group.self.id}"]
  subnet_id = "${element(aws_subnet.coreospub.*.id, count.index)}"
}

Note that I made the element function wrap if the index is greater than the splat length, so if you do a count = 6, for example, you'll end up with 2 instances per AZ/subnet pair.

Curious when will we be able to using count.index to reference a resource already created just as @andyshinn pointed. I ran into the same problem

resource "aws_subnet" "public" {
    vpc_id = "${aws_vpc.default.id}"
    cidr_block = "${concat(var.vpc_cidr_block_base, ".", count.index ,".0/24")}"
    availability_zone = "${concat(var.region, lookup(var.vpc_availability_zone, concat("zone_", count.index)))}"
    count = "${var.zone_count}"
    depends_on = ["aws_vpc.default", "aws_internet_gateway.default"]
}
# Routing table for public subnets
resource "aws_route_table" "public" {
    vpc_id = "${aws_vpc.default.id}"
    route {
        cidr_block = "0.0.0.0/0"
        gateway_id = "${aws_internet_gateway.default.id}"
    }
    depends_on = ["aws_vpc.default", "aws_internet_gateway.default"]
}
resource "aws_route_table_association" "public" {
    subnet_id = "${aws_subnet.public.${count.index}.id}"
    route_table_id = "${aws_route_table.public.id}"
    count = "${var.zone_count}"
    depends_on = ["aws_vpc.default", "aws_internet_gateway.default"]
}
Output
module.vpc.aws_route_table_association.public.1: Creating...
  route_table_id: "" => "rtb-0668bf63"
  subnet_id:      "" => "${aws_subnet.public.1.id}"
module.vpc.aws_route_table_association.public.0: Creating...
  route_table_id: "" => "rtb-0668bf63"
  subnet_id:      "" => "${aws_subnet.public.0.id}"
module.vpc.aws_route_table_association.public.0: Error: The subnet ID '${aws_subnet.public.0.id}' does not exist (InvalidSubnetID.NotFound)

Using the way @rcostanzo suggested above (i.e. using the element function) did the job for me.

However I believe a way that provides more flexibility needs to be found and the only way I can imagine is eventually allowing for maps to use interpolated variables.

I just tried to use the new element function as @rcostanzo added, however I ran into an issue. It works fine when ran as part of the main tf file
i.e.

variable "instance_count" {
  default = "1"
}

resource "aws_instance" "base" {
  ami = "ami-b66ed3de"
  instance_type = "m3.medium"

  count = "${var.instance_count}"  
}

resource "aws_route53_record" "base_internal_dns" {
  name = "${concat("test", count.index, ".", "test.com")}"

  count = "${var.instance_count}"

  zone_id = "test.com"
  type = "A"
  ttl = "1"
  records = ["${element(aws_instance.base.*.private_ip, count.index)}"]
}

works fine.

However, when I try and use that as a module i.e.
outside of the file above:

module "base" {
  source = "./base"
  instance_count = "2"
}

I run into
Error running plan: Resource 'aws_instance.base' not found for variable 'aws_instance.base.*.private_ip'

Am I not referring to the aws_instance.base.*.private_ip correctly? Is it a module scoping issue?

I fixed my issue with #611

Fixed by @rubbish in #611

Just in case someone lands here trying to achieve this with 0.7 or later, the way to do this without relying on concat is:

lookup(var.vpc_availability_zone, "zone_${count.index}")

Or, in my case:

lookup(var.vpc_availability_zone, "zone_${count.index % 3}")

@rafaelmagu that was the final touch in getting terraform to name instances based on their region variable and count. thank you ever so much!

Anyway to reference data.aws_availability_zones.available.names[0] into this? I think I'm furthering the double interpolation pattern witnessed here

I tried similar code above to create 3 instances but public IP is not getting attached to all three instances. It attaches it to only 1st instance:

terraform code:

resource "aws_subnet" "public_subnet" {
  count  = "3"
  vpc_id = "${aws_vpc.vpc.id}"

  cidr_block              = "${cidrsubnet("${var.vpc_cidr}", 4, count.index)}" #count.index is 3 it creates 3 subnets
  availability_zone       = "${element(var.lst_azs, count.index)}"
  map_public_ip_on_launch = "true"
}

Above code creates 3 subnets and 3 different instances are attached to these 3 subnets. But only 1st instance is getting public ip. 2nd and 3rd are not getting public ip's.

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

Related issues

carl-youngblood picture carl-youngblood  ยท  3Comments

ronnix picture ronnix  ยท  3Comments

zeninfinity picture zeninfinity  ยท  3Comments

larstobi picture larstobi  ยท  3Comments

rjinski picture rjinski  ยท  3Comments