Map value in module with for-each to output - terraform

I have a module that creates multiple resources for a list of names. So for each name supplied in a variable called instances, a set of resources (vm, ports, volumes) is created.
In the output of that module I want to have a map, that maps the instance (each value in instances) to the IP of an associated port.
This is the definition of the port
resource "openstack_networking_port_v2" "this" {
for_each = var.instances
name = "port-${each.key}"
network_id = var.network_id
admin_state_up = true
security_group_ids = var.security_group_ids
fixed_ip {
subnet_id = var.subnet_id
}
}
Until now, I had this in the output of the module
output "int-port" {
value = openstack_networking_port_v2.this
}
and this where I used it
int-ip : module.my-instance.int-port["${var.deployment}-my-01"].all_fixed_ips[0]
After upgrading terraform, I need to add sensitive = true to the output, as the openstack provider has marked something as sensitive, which will lead to the output not being printed. (I know I can get it with terraform output -json)
So I want to just return the IPs I need in the output instead of the whole object, but I can't figure out how.
I tried the following things:
output "int-ip" {
value = openstack_networking_port_v2.this.*.all_fixed_ips[0]
}
and
output "int-ip" {
value = openstack_networking_port_v2.this[*].all_fixed_ips[0]
}
which gives me
│ Error: Unsupported attribute
│
│ on ../../modules/ext-instance-v2/outputs.tf line 24, in output "int-port":
│ 24: value = openstack_networking_port_v2.this.*.all_fixed_ips[0]
│
│ This object does not have an attribute named "all_fixed_ips".
I also tried
output "int-ip" {
value = {
for instance in var.instances:
instance => openstack_networking_port_v2.this["${instance}"].all_fixed_ips[0]
}
}
and
output "int-ip" {
value = {
for instance in var.instances:
instance => openstack_networking_port_v2.this[instance].all_fixed_ips[0]
}
}
which leads to that error
│ Error: Invalid index
│
│ on ../../modules/ext-instance-v2/outputs.tf line 19, in output "int-ip":
│ 19: instance => openstack_networking_port_v2.this["${instance}"].all_fixed_ips[0]
│ ├────────────────
│ │ openstack_networking_port_v2.this is object with 9 attributes
│
│ The given key does not identify an element in this collection value.
It feels like I'm very close, but just out of reach.
I'm not only interested in the solution, but also in an explanation why the things I tried, did not work out.

Since your openstack_networking_port_v2.this is map due to for_each, it should be:
output "int-ip" {
value = values(openstack_networking_port_v2.this)[*].all_fixed_ips[0]
}
Update
Based on the comments. The correct way is for the last attempt is:
output "int-ip" {
value = {
for instance in keys(var.instances):
instance => openstack_networking_port_v2.this[instance].all_fixed_ips[0]
}
}
This is required, as instances is a map, but you need a list. In this case, you want to use the list of keys in the for-each.

The [*] operator (Splat operator) is designed for producing lists only. The documentation says the following about splat expressions on maps:
The splat expression patterns shown above apply only to lists, sets, and tuples. To get a similar result with a map or object value you must use for expressions.
Resources that use the for_each argument will appear in expressions as a map of objects, so you can't use splat expressions with those resources. For more information, see Referring to Resource Instances.
The simplest version of this with for expressions would be the following:
output "int-ip" {
value = {
for k, port in openstack_networking_port_v2.this : k => port.all_fixed_ips[0]
}
}
This means to construct a new mapping where each element of openstack_networking_port_v2.this (a map of objects) is translated into an element that has the same key but has just the first IP address as the value.
An important difference in my example above vs. the ones in your question is that the for clause has two symbols: k, port. This means to put the key of each element in k and the value of each element in port. If you write only a single symbol, like for port, then Terraform will bind that to the value of each element rather than the key, and there is no straightforward way to find the key corresponding to a particular value, only to find a value corresponding to a key.
It would also be possible in principle to use the keys function to repeat over the keys in particular, and then use the key to look up the element:
output "int-ip" {
value = {
for k in keys(openstack_networking_port_v2.this) : k => openstack_networking_port_v2.this[k].all_fixed_ips[0]
}
}
This is equivalent to the other for expression I showed above, but is more verbose due to needing to refer to openstack_networking_port_v2.this twice. Therefore I prefer the two-symbol for, particularly in cases like this where the expression being mapped has a large number of characters in it.

Related

How can I fix for_each" value depends on resource attributes that cannot be determined until apply

Context: I'm aware of the similar questions:
The "for_each" value depends on resource attributes that cannot be determined (Terraform)
for_each value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created
but I think mine is a bit different and it might be fixed by refactoring TF code since there's an additional input restriction.
My original example is very long so I came up with a minimum viable example instead:
I've got an input variable of type map that maps all possible numbers to names:
# tfvars.terraform
all_names_by_number = {
"1" = "alex",
"3" = "james",
"5" = "ann",
"8" = "paul",
}
# main.tf
locals {
# active_names_by_number is a map as well
# but it's a subset of all_names_by_number
# all_names_by_number = {
# "3" = "james",
# "5" = "ann",
# }
active_names_by_number = people_resource.example.active_names_map
}
# Resource that depedns on active_names_by_number
resource "foo" "active_items" {
for_each = local.active_names_by_number
name = "abc-${each.key}"
location = var.location
sub_id = data.zoo.sub[each.key].id
bar {
bar_name = each.value
}
}
When I run the terraform configuration above via terraform plan, I get:
Error: Invalid for_each argument
on main.tf line 286, in resource "foo" "active_items":
286: for_each = for_each = local.active_names_by_number
The "for_each" value depends on resource attributes that cannot be determined
until apply, so Terraform cannot predict how many instances will be created.
To work around this, use the -target argument to first apply only the
resources that the for_each depends on.
which totally makes sense since people_resource.example.active_names_map is "initialized" in runtime from another resource (response)
locals {
active_names_by_number = people_resource.example.active_names_map
}
but given the fact that active_names_by_number is a subset of all_names_by_number (input variable), how can I refactor the terraform configuration to show TF that local.active_names_by_number is bounded?
My ideas so far:
Use count instead of for_each as other answers suggest but I do need to use each.value in my example (and I can't use all_names_by_number to create extra resources.
Get rid of local.active_names_by_number and use var.all_names_by_number instead -- the major downside is TF will create extra resources which is pretty expensive.
Somehow write a nested for loop:
# pseudocode
for name in var.all_names_by_number:
if name is in people_resource.example.active_names_map:
# create an instance of foo.active_item
I also faced the same issue, so I followed the method of splitting it into two modules which #Alex Kuzminov suggested. Thanks for that.
So Instead of using for_each, use count and along with that use try block.
locals {
active_names_by_number = try(people_resource.example.active_names_map, tolist(["a", "b", "c", "d"]))
}
So initial exception will be resolved while terraform apply, then while actual resource running, it will replace with the actual content instead of a,b,c,d.
I hope this helps. Thanks.

error while running terraforrm plan "Inappropriate value for attribute "instance_id": string required."

#getting below error while running "terraform plan". i am trying to create three windows compute resources. the instance_name are of list type and a count is added to resource
Error: Incorrect attribute value type
on datasource.tf line 7, in data "oci_core_instance_credentials" "instance_credentials":
7: instance_id = ["${oci_core_instance.jde_instance.*.id}"]
Inappropriate value for attribute "instance_id": string required.
oci_core_instance_credentials requires just one value. But you are passing an array.
It should be as below assuming that oci_core_instance.jde_instance is an array. This way you are using first instance in the array:
instance_id = oci_core_instance.jde_instance[0].id
If its a single array it should be:
instance_id = oci_core_instance.jde_instance.id

Terraform Inappropriate value for attribute "subnets": element 0: string required

I am upgrading from tf 11 to tf 12. I've run into the issue where terraform plan produces the following error:
Error: Incorrect attribute value type
4: subnets = ["${var.alb_subnets}"]
Inappropriate value for attribute "subnets": element 0: string required.
The code snippet for this error is:
resource aws_alb "alb" {
name = "ecs-${var.app_name}"
internal = "${var.internal}"
subnets = ["${var.alb_subnets}"]
security_groups = ["${var.security_groups}"]
count = "${var.do_module}"
}
If anyone can help me with this I would appreciate it.
Change subnets = ["${var.alb_subnets}"] to subnets = var.alb_subnets
Its a update in terraform v0.12
Reference: https://www.terraform.io/upgrade-guides/0-12.html#referring-to-list-variables
The error message indicates that the argument subnets for the aws_alb resource expects elements of type string in its list type. The error indicates you provided a value for the argument that is not of type list(string). Although the value or type for the variable alb_subnets is not provided in the question, it can be assumed it is either a list or map given the name of the variable is plural. Assuming it is a list, you are casting it as a list(list(any)) when you specify it in your config as:
["${var.alb_subnets}"]
Deconstructing this, the [] specifies a list, and the variable is already a list. The elements of the variable are not provided in the question, but they can be assumed to be any without sacrificing accuracy.
Instead of specifying a nested list by wrapping the variable inside another list syntax with [], you can remove the outer brackets and:
resource aws_alb "alb" {
name = "ecs-${var.app_name}"
internal = "${var.internal}"
subnets = var.alb_subnets
security_groups = ["${var.security_groups}"]
count = "${var.do_module}"
}
will be a list(any) for the argument value. If your elements of alb_subnets are not strings, then you will have to fix that also to ensure the proper type of list(string) for the argument.

Calling specific for_each outputs

I'm just starting to use for_each loops and from what I understand for_each is different than count in the sense that count indexes numerically for outputs aws_transfer_key.transfer_key[1] vs for_each outputs aws_transfer_key.transfer_key["value"].
How do I call the output of a for_each value later on?
A resource or data block with the count argument set appears in expressions as a list, which is why you can access its instances with [0], [1], etc.
Similarly, a resource or data block with the for_each argument set appears in expressions as a map, so you can access its instances with ["foo"], ["bar"], etc.
In both cases the collection is of objects conforming to the resource type schema, so you can follow that with .attribute syntax to access individual attributes.
So first take the resource type and name, aws_transfer_key.transfer_key, which is a map. Then ["value"] to access the instance you want from the map. Then .foo to access the "foo" attribute. All together, that's aws_transfer_key.transfer_key["value"].foo.
If you want to access all of the "foo" attributes across all of the instances, you can project the map of objects into a map of strings using a for expression:
{ for k, v in aws_transfer_key.transfer_key : k => v.foo }
From the example here
resource "aws_security_group" "example" {
name = "example" # can use expressions here
dynamic "ingress" {
for_each = var.service_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
}
}
}
You reference the name of the dynamic block, in this case it's an ingress.

vpc_zone_identifier should be a list

I'm not getting my head around this. When doing a terraform plan it complains the value should be a list. Fair enough. Let's break this down in steps.
The error
1 error(s) occurred:
* module.instance-layer.aws_autoscaling_group.mariadb-asg: vpc_zone_identifier: should be a list
The setup
The VPC and subnets are created with terraform in another module.
The outputs of that module give the following:
"subnets_private": {
"sensitive": false,
"type": "string",
"value": "subnet-1234aec7,subnet-1234c8a7"
},
In my main.tf I use the output of said module to feed it into a variable for my module that takes care of the auto scaling groups:
subnets_private = "${module.static-layer.subnets_private}"
This is used in the module to require the variable:
variable "subnets_private" {}
And this is the part where I configure the vpc_zone_identifier:
Attempt: split
resource "aws_autoscaling_group" "mariadb-asg" {
vpc_zone_identifier = "${split(",",var.subnets_private)}"
Attempt: list
resource "aws_autoscaling_group" "mariadb-asg" {
vpc_zone_identifier = "${list(split(",",var.subnets_private))}"
Question
The above attempt with the list(split( should in theory work. Since terraform complains but doesn't print the actual value it's quite hard to debug. Any suggestions are appreciated.
Filling in the value manually works.
When reading the documentation very carefully it appears the split is not spitting out clean elements that afterwards can be put into a list.
They suggest to wrap brackets around the string ([" xxxxxxx "]) so terraform picks it up as a list.
If my logic is correct that means
subnet-1234aec7,subnet-1234c8a7 is outputted as subnet-1234aec7","subnet-1234c8a7 (note the quotes), assuming the quotes around the delimiter of the split command have nothing to do with this.
Here is the working solution
vpc_zone_identifier = ["${split(",",var.subnets_private)}"]
For the following helps:
vpc_zone_identifier = ["${data.aws_subnet_ids.all.ids}"]

Resources