Reputation: 61
I am pretty new to Terraform and I am using this function to re-use same list of subnets to launch more instances than subnets I have. (It'll just loop over) This works great if I provide my own map of subnets but the data in the remote state is a tuple and I get this error:
Invalid value for "inputMap" parameter: lookup() requires a map as the first
argument. data.terraform_remote_state.vpc.outputs.private_subnets is tuple with 4 elements
I have also tried the tomap( function but this fails with:
Invalid value for "v" parameter: cannot convert tuple to map of any single
type.
Here is my code:
count = var.instance_count
ami = var.ami
instance_type = "t2.medium"
subnet_id = lookup(data.terraform_remote_state.vpc.outputs.private_subnets, count.index%length(data.terraform_remote_state.vpc.outputs.private_subnets))
vpc_security_group_ids = ["${data.terraform_remote_state.foo_sg.outputs.foo_sg_id}"]
key_name = var.key_name
iam_instance_profile = var.iam_instance_profile
user_data = <<-EOF
#!/bin/bash
hostnamectl set-hostname --static "${var.app_name}-${count.index + 1}.${data.terraform_remote_state.vpc.outputs.private_zone_domain_name}"
echo "127.0.0.1 localhost.localdomain localhost4 localhost4.localdomain4 ${var.app_name}-${count.index + 1}.${data.terraform_remote_state.vpc.outputs.private_zone_domain_name} localhost" > hosts
echo "::1 localhost localhost.localdomain localhost6 localhost6.localdomain6" >> hosts
EOF
tags = {
Name = "${var.app_name}-${count.index +1}.${data.terraform_remote_state.vpc.outputs.private_zone_domain_name}"
}
Like I said my goal is to re-use the 4 subnets in that remote state I have, so if I want 6 instances it would loop through the 4 I have and the 5th and 6th instance would be on subnet 1 and 2 in the Tuple. Any suggestions would be appreciated!
Upvotes: 6
Views: 38861
Reputation: 3988
Some extra thoughts on not using count
in preference to for_each
.
If you have a set in Terraform, the position is not guaranteed.
So if you start with ["a", "b", "c"]
, you have 3 values whose index starts at 0 and goes up to 2.
Say you remove one of first 2 (maybe the actual data comes from somewhere else and is NOT a hard-coded set), the index of the first one is still 0, but now, only goes upto 1.
What's the problem with that?
When you use count
, the resource is named (Terraform state name, not cloud/target name) using the index.
So, all of a sudden, ALL of the values for the properties that were fine for "a" are now gone, and will be updated to the values for "b". Depending upon the resource, you may have to actually delete the resource!
An approach that I've been using to hide/runaway/fingers-in-my-ears-shouting-la-la-la to this issue is to use a map.
locals {
set_of_things = ["a", "b", "c"]
}
resource "a_resource" "thing" {
for_each = zipmap(local.set_of_things, local.set_of_things)
}
Here, the for_each
key becomes part of the resource name, so a_resource.thing["a"]
, a_resource.thing["b"]
, and a_resource.thing["c"]
.
So, what's the difference?
Well, if you NOW delete "a"
from the set_of_things
only a_resource.thing["a"]
is deleted and nothing happens to the others.
You are no longer bound to an index which has no knowledge of the content.
Sometimes this can be a little hard to get to terms with if your set is actually external.
Ideally, being able to pull the value of the thing you are currently using, along with something useful and unique so you have a common key for all the things that exist in relation to the external data.
data "external" "things" {
filter = {
regex = "^[a]" // Get all the things that start with the letter A
}
}
locals {
map_of_things = {
for thing in external.things : (thing.something_unique) => thing.the_property_you_were_using
}
}
resource "a_resource" "thing" {
for_each = local.map_of_things
property = each.value
}
The reason for this, for me, is everything is named as I want it to be named.
If you want a sort of quick shortcut, then the use of zipmap(local.set_of_things, local.set_of_things)
can get you over to the for_each
method easily enough. In that pattern, both each.key
and each.value
have the same values.
Upvotes: 0
Reputation: 925
You have 3 forms, to make the loop.
1º Using dynamic block
... Example:
resource "aws_security_group" "ecc_default" {
count = length(var.clusters)
vpc_id = var.vpc_id
name = "${terraform.workspace}.ecc-${var.clusters[count.index].engine}.sg"
description = "${terraform.workspace}.ecc-${var.clusters[count.index].engine}.sg"
ingress {
from_port = var.clusters[count.index].port
to_port = var.clusters[count.index].port
protocol = "tcp"
cidr_blocks = [var.vpc_cidr]
}
dynamic "ingress" {
iterator = item
for_each = var.enable_vpn ? var.vpn_cidr : []
content {
from_port = var.clusters[count.index].port
to_port = var.clusters[count.index].port
protocol = "tcp"
cidr_blocks = [item.value["cidr"]]
description = item.value["description"]
}
}
...
...
}
2º Using for_each
... (click here for answer)
3º Using arithmetic iteration... (click here for answer) Example:
resource "aws_security_group_rule" "ecc_internal" {
count = length(var.ingress_security_groups) > 0 ? length(var.ingress_security_groups) * length(var.clusters) : 0
security_group_id = aws_security_group.ecc_default[count.index % length(var.clusters)].id
type = "ingress"
from_port = var.clusters[count.index % length(var.clusters)].port
to_port = var.clusters[count.index % length(var.clusters)].port
protocol = "tcp"
source_security_group_id = var.ingress_security_groups[floor(count.index / length(var.clusters))]
}
And, for the error on convert: try to use coalescelist
with fake replacement. Like this:
resource "aws_lb_listener_certificate" "default" {
count = (coalescelist(data.aws_alb_listener.https, [{ certificate_arn = "" }])[0].certificate_arn != var.certificate_arn) ? local.enabled : 0
listener_arn = coalescelist(data.aws_alb_listener.https, [{ arn = "" }])[count.index].arn
certificate_arn = var.certificate_arn
}
The conversion of a tuple ([N1, N2, N3]
) to a map ({N1 = "", N2 = ""}
), is really not allowed. But, you can use a fake replacement to make the job. tomap
needs an object for argument. And map
, needs multiple args to make the result. Try to manipulate your list, to convert in map, with another form like this (if you want just convert, and not solve the problem):
environment_vars = jsonencode([
for key in sort(keys(data.external.environment_vars.result)) : {
name = key
value = lookup(data.external.environment_vars.result, key)
}])
I recommend for you: Terraform tips & tricks: loops, if-statements, and gotchas
Upvotes: 8