Terraform 0.12 - create multiple resources per every 1 resource created

Hello,

I have this code block working in terraform version < 0.12

variable "region" {
}

variable "instance_number_onset" {
  default = "0"
}

variable "gw_count" {
  default = 1
}

variable "vpc_names" {
  default = ["Dev", "Stage", "Prod", "Ops"]
}

locals {
  description = "Transit-Gateway-"
}

resource "aws_ec2_transit_gateway" "transit_gw" {
  count                           = "${var.gw_count}"
  description                     = "${local.description}${format("%03d", var.instance_number_onset + count.index)}"
  auto_accept_shared_attachments  = "enable"
  default_route_table_association = "disable"
  default_route_table_propagation = "disable"
  dns_support                     = "enable"
  vpn_ecmp_support                = "enable"

  tags = "${
    map(
    "Name", "Transit-Gateway-${format("%03d", var.instance_number_onset + count.index)}",
    "Region", "${var.region}",
    "Description", "${local.description}${format("%03d", var.instance_number_onset + count.index)}",
    "Managed_by", "terraform",
    )}"
}

resource "aws_ec2_transit_gateway_route_table" "rtb" {
  count              = "${var.gw_count * length(var.vpc_names)}"
  transit_gateway_id = "${element(aws_ec2_transit_gateway.transit_gw.*.id, count.index / length(var.vpc_names))}"

  tags = "${
    map(
    "Name", "-${element(aws_ec2_transit_gateway.transit_gw.*.id, count.index / length(var.vpc_names))}-rtb",
    "Region", "${var.region}",
    "Description", "-${local.description}${element(aws_ec2_transit_gateway.transit_gw.*.id, count.index / length(var.vpc_names))}-rtb",
    "Managed_by", "terraform",
    "VPC", ""
    )}"

  lifecycle {
    create_before_destroy = true

    ignore_changes = [
      "tags",
    ]
  }
}

This will create 4x aws_ec2_transit_gateway_route_table resources for every 1 aws_ec2_transit_gateway

With terraform 0.12 upgrade, it has converted the code to:

resource "aws_ec2_transit_gateway_route_table" "rtb" {
  count = var.gw_count * length(var.vpc_names)
  transit_gateway_id = element(
    aws_ec2_transit_gateway.transit_gw.*.id,
    count.index / length(var.vpc_names),
  )

  tags = {
    "Name" = "-${element(
      aws_ec2_transit_gateway.transit_gw.*.id,
      count.index / length(var.vpc_names),
    )}-rtb"
    "Region" = var.region
    "Description" = "-${local.description}${element(
      aws_ec2_transit_gateway.transit_gw.*.id,
      count.index / length(var.vpc_names),
    )}-rtb"
    "Managed_by" = "terraform"
    "VPC"        = ""
  }

  lifecycle {
    create_before_destroy = true

    ignore_changes = [tags]
  }
}

The errors im now receiving are:

Error: Error in function call

  on ../../../../modules/transit-gateway/main.tf line 24, in resource "aws_ec2_transit_gateway_route_table" "rtb":
  24:   transit_gateway_id = element(
  25:
  26:
  27:
    |----------------
    | aws_ec2_transit_gateway.transit_gw is tuple with 1 element
    | count.index is 1
    | var.vpc_names is tuple with 5 elements

Call to function "element" failed: invalid index: value must be a whole
number, between -9223372036854775808 and 9223372036854775807.

How can I revise my module to achieve the same result as before?

Thank you

Hi @popopanda,

The cause of this error is that the division operator / now performs conventional division (as opposed to integer division) in all cases. In prior versions of Terraform its behavior was inconsistent: it would sometimes use integer division and sometimes use floating point division.

To restore the previous behavior, you can explicitly specify that you want the effect of integer division using the floor function:

  tags = {
    "Name" = "-${element(
      aws_ec2_transit_gateway.transit_gw.*.id,
      floor(count.index / length(var.vpc_names)),
    )}-rtb"
    "Region" = var.region
    "Description" = "-${local.description}${element(
      aws_ec2_transit_gateway.transit_gw.*.id,
      floor(count.index / length(var.vpc_names)),
    )}-rtb"
    "Managed_by" = "terraform"
    "VPC"        = ""
  }

Taking the count.index and var.vpc_names that were current in the error message you shared:

  • count.index / length(var.vpc_names) = 1 / 5 = 0.2
  • floor(0.2) == 0
  • Therefore Terraform will select the zeroth element of aws_ec2_transit_gateway.transit_gw.*.id

The above describes the minimum to get back to the previous functionality, but once you have finished your initial upgrade to Terraform 0.12 you may wish to switch this to a new pattern that Terraform 0.12 supports, where you can factor out this “per gateway per VPC” structure into a local value. For example:

locals {
  gateway_vpcs = [
    for v in setproduct(aws_ec2_transit_gateway.transit_gw.*.id, var.vpc_names) : {
      transit_gateway_id = v[0]
      vpc_name           = v[1]
    }
  ]
}

resource "aws_ec2_transit_gateway_route_table" "rtb" {
  count = length(local.gateway_vpcs)

  transit_gateway_id = local.gateway_vpcs[count.index].transit_gateway_id

  tags = {
    Name        = "${local.gateway_vpcs[count.index].transit_gateway_id}-rtb"
    Description = "${local.description}${local.gateway_vpcs[count.index].transit_gateway_id}-rtb"
    VPC         = local.gateway_vpcs[count.index].vpc_name
    # ...
  }
}

The setproduct function will produce a list of lists containing every combination of its arguments, avoiding the need to repeat the division/modulo calculations for every reference to a particular pairing of transit gateway and VPC.

Thanks for the reply, much appreciate it

In regards to the for loop

    for v in setproduct(aws_ec2_transit_gateway.transit_gw.*.id, var.vpc_names) : {
      transit_gateway_id = v[0]
      vpc_name           = v[1]
    }

what would v[0] and v[1] be evaluated as during runtime assuming the list produced by setproduct is:

[
  [
    "tgw1",
    "dev",
  ],
  [
    "tgw1",
    "ops",
  ],
  [
    "tgw1",
    "prod",
  ],
  [
    "tgw1",
    "stage",
  ],
]

Ah, I think I understand it now. The for loop generates a list of maps based on the number of aws_ec2_transit_gateway.transit_gw.*.id that was created

[
  {
    transit_gateway_id = "tgw1",
    vpc_name = "dev",
  },
  {
    transit_gateway_id = "tgw1",
    vpc_name = "ops",
  },
  {
    transit_gateway_id = "tgw1",
    vpc_name = "prod",
  },
  {
    transit_gateway_id = "tgw1",
    vpc_name = "stage",
  },
]

and defining transit_gateway_id = local.gateway_vpcs[count.index].transit_gateway_id, just means count.index starts from 0, which matches the first entry of the list of maps?