how can i iterate from a list to output value? - terraform

I m getting from terraform 12, call a list of values
data "oci_core_instances" "test_instances" {
#Required
compartment_id = "${var.compartment_ocid}"
availability_domain = "${data.oci_identity_availability_domains.ads.availability_domains[0].name}"
}
// numInstances = 3 for my case
locals {
numInstances = length(data.oci_core_instances.test_instances.instances)
}
and i want to iterate like (pseudo code) :
# Output the result single element
output "format_instances_name_state" {
value = "${
for (i=0 ; i< 3; i++)
format("%s=>%s",data.oci_core_instances.test_instances.instances[i].display_name,data.oci_core_instances.test_instances.instances[i].state)
} "
}
how can i do this in terraform ?
i have tried this :
# Output the result single element
output "format_instances_name_state" {
value = "${
for i in local.numInstances :
format("%s=>%s",data.oci_core_instances.test_instances.instances[i].display_name,data.oci_core_instances.test_instances.instances[i].state)
} "
}
but i m getting this error:
Error: Extra characters after interpolation expression
on main.tf line 64, in output "format_instances_state_element_single":
63:
64: for i in local.numInstances :
Expected a closing brace to end the interpolation expression, but found extra
characters.
any ideas ?

It seems like what you really want here is a map from display name to state, in which case the following expression would produce that:
output "instance_states" {
value = {
for inst in data.oci_core_instances.test_instances.instances : inst.display_name => inst.state
}
}
If you really do need that list of strings with => inside for some reason, you can adapt the above to get it, like this:
output "format_instances_state_element_single" {
value = [
for inst in data.oci_core_instances.test_instances.instances : "${inst.display_name}=>${inst.state}"
]
}
In this second case the for expression is marked by [ ] brackets instead of { } braces, which means it will produce a list result rather than a map result.

Related

How to access a local using a variable in Terraform

I have the following code.
mymodule
variable "senses" {
type = string
}
locals {
sounds = {
"cat" = "meow"
"dog" = ["bark", "woof"]
}
}
output "noise" {
value = local[var.senses]["cat"]
}
call mymodule
module "mymodule" {
source = "../../../modules/mymodule"
senses = "sound"
}
returns error:
Error: Invalid reference
on ../../../modules/mymodule/outputs.tf line 62, in output "noise":
62: value = local[var.senses]["cat"]
The "local" object cannot be accessed directly. Instead, access one of its
attributes.
my code can't seem to handle
value = local[var.senses]["cat"]
Any suggestions on how i can get this to work?
I don't believe it's possible to use a variable to switch which local you're reading. I.e. local[var.senses] is the root of the issue.
If you refactor slightly and put your values inside a single, known, value--such as local.senses it should then let you do a key lookup within that value.
So, if you modify your locals to place your values in a senses key:
locals {
senses = {
"sounds" = {
"cat" = "meow"
"dog" = ["bark", "woof"]
}
}
}
and update your lookup to use that field:
value = local.senses[var.senses]["cat"]
Then I believe it will work, since your are doing a key lookup against a specific local rather than trying to dynamically select the local.

Build a map from conditional output values in terraform

I have a module that outputs two values, key and value. The module contains a third value is_needed, which is a boolean. I can run this module any number of times and get a map of the outputs. Is there a way to conditionally add values to a map based on the is_needed boolean?
For example, I can create a map with all of the values like this:
locals {
map_of_values = tomap({
for instance in module.my_module : instance.key => instance.value
})
}
Can I create a map with only some of the values? Something akin to this pseudo-code:
locals {
map_of_needed_values = tomap({
for instance in module.my_module if is_needed: instance.key => instance.value
})
}
Yes. In fact your pseudo code is very close to the correct syntax.
locals {
map_of_needed_values = tomap({
for instance in module.my_module :
instance.key => instance.value if instance.is_needed
})
}
Here's a full block of functioning code that will help you see it all working. I was not completely sure of the structure of your module.my_module outputs, so I guessed.
variable "my_module" {
default = {
"instance_1" = {
"key" = "hello"
"value" = "world"
"is_needed" = false
}
"instance_2" = {
"key" = "foo"
"value" = "bar"
"is_needed" = true
}
}
}
locals {
map_of_needed_values = tomap({
for instance in var.my_module :
instance.key => instance.value if instance.is_needed
})
}
output "map_of_needed_values" { value = local.map_of_needed_values }

Terraform - inverting a map

I'm stuck trying to write a terraform expression which can turn this:
subnets = {
my_subnet_1 = {
nsg = "my_nsg_1",
addresses = "my_addresses_1"
}
my_subnet_2 = {
nsg = "my_nsg_2",
addresses = "my_addresses_2"
}
}
into
nsgs_assocs = {
my_nsg_1 = "my_subnet_1"
my_nsg_2 = "my_subnet_2"
}
I've tried the following:
locals {
nsgs_assocs = zipmap(
var.subnets.*.nsg,
keys(var.subnets)
)
}
but this gives an error:
Error: Invalid function argument
on ..\..\modules\vnet\main.tf line 22, in locals:
21: nsgs_assocs = zipmap(
22: var.subnets.*.nsg,
23: keys(var.subnets)
24: )
Invalid value for "keys" parameter: element 0: string required.
For context, I've inherited a bunch of scripts which I'm trying to refactor without changing the results of a terraform plan.
One of the modules has a lot of related lookup maps - e.g:
nsgs_assocs = {
my_nsg_1 = "my_subnet_1"
my_nsg_2 = "my_subnet_2"
}
subnet_addresses = {
my_subnet_1 = "my_addresses_1"
my_subnet_2 = "my_addresses_2"
}
which I've condensed into my first above sample which I think will be more maintainable in the long run.
However, for backward compatibility with the existing terraform state I need to generate the original nsgs_assocs inside my module so that a for_each continues to use the nsg name as the resource key instead of the subnet name (which causes a destroy / create pair due to the key change).
You're on the right track. It does not work, because splat expression works with arrays, and var.subnets is a map. In order to fix it, you need to convert it into array and it can be done by using values terraform function:
locals {
nsgs_assocs = zipmap(
values(var.subnets)[*].nsg,
keys(var.subnets)
)
}
If you have:
variable "subnets" {
default = {
my_subnet_1 = {
nsg = "my_nsg_1",
addresses = "my_addresses_1"
}
my_subnet_2 = {
nsg = "my_nsg_2",
addresses = "my_addresses_2"
}
}
}
then the following is incorrect
var.subnets.*.nsg
Thus, it should be values(var.subnets).*.nsg:
locals {
nsgs_assocs = zipmap(
values(var.subnets).*.nsg,
keys(var.subnets)
)
}
resulting in:
{
"my_nsg_1" = "my_subnet_1"
"my_nsg_2" = "my_subnet_2"
}
There are a few different ways to achieve this, and the zipmap-based solutions others have shared are fine answers too, but I also wanted to show an example using for expressions because I (subjectively) tend to think this form is easiest to read and understand:
locals {
nsgs_allocs = {
for k, s in var.subnets : s.nsg => k
}
}
As long as all of your subnets have unique nsg values, the above should produce the result you were looking for.
In situations where the new key isn't unique -- for example, if in your cases there could be multiple subnets with the same nsg value -- you can use the for expression's "grouping" mode, which would produce a map of lists of subnet values so that there can potentially be more than one value under each key:
locals {
nsgs_allocs = {
for k, s in var.subnets : s.nsg => k...
}
}
nsgs_assocs = {
my_nsg_1 = ["my_subnet_1"]
my_nsg_2 = ["my_subnet_2"]
}

flattening output contents of a composite map

I have two module that output respectively
output "discovery_service_hostname" {
value = "${aws_appmesh_virtual_service.service.name}"
}
and
output "discovery_service_arn" {
value = zipmap( aws_service_discovery_service.sd[*].name, aws_service_discovery_service.sd[*].arn)
}
Both are used in the main script that outputs
output "services" {
value = {
"web" = "${module.web.discovery_service_hostname}"
"wwb-backend" = "${module.web_backend.discovery_service_hostname}"
"wwb-backend-n" = "${module.web_backend_n.discovery_service_hostname}"
}
}
in this case I used the 1st module for web andweb-backend, while I used the 2nd module for web-backend-n
I need to access the service arn via lookup function in a 3rd script, but I would avoid duplicating the whole code to handle the two cases
final output like this
discovery_service = {
"web" = "arn:xxx1"
"web-backend" = "arn:xxx2"
"web-backend-n" = {
"web-backend-n-1" = "arn:xxx3"
"web-backend-n-2" = "arn:xxx4
"web-backend-n-3" = "arn:xxx5"
}
Is there a way to have an output like
discovery_service = {
"web" = "arn:xxx1"
"web-backend" = "arn:xxx2"
"web-backend-n-1" = "arn:xxx3"
"web-backend-n-2" = "arn:xxx4
"web-backend-n-3" = "arn:xxx5"
}
thanks!
I will answer my own question. Solution is to always output a map (even from the module with single outputs) like this:
output "discovery_service_arn" {
value = zipmap( [ aws_service_discovery_service.sd.name ], [ aws_service_discovery_service.sd.arn ])
}
and
output "discovery_service_arn" {
value = zipmap( aws_service_discovery_service.sd[*].name, aws_service_discovery_service.sd[*].arn)
}
then in the final script use merge to get a single map like
output "discovery_service" {
value = merge(
module.web.discovery_service_arn,
module.web_backend.discovery_service_arn,
module.web_backend_n.discovery_service_arn
)
}

Splitting a comma separated string

I have an output that is a multi-valued, comma separated string.
input.tf
resource "azurerm_app_service" "testap" {
name = "MySuperCoolAppServer001"
location = "eastus"
resource_group_name = "notshown"
app_service_plan_id = "notshown"
}
output.tf
output "output_tf_testap_outbound_ip_addresses" {
value = "${azurerm_app_service.testap.outbound_ip_addresses}"
}
And I get this in the console:
output_tf_testap_outbound_ip_addresses =
1.2.3.4,1.2.3.5,1.2.3.6,1.2.3.7,1.2.3.8,1.2.3.9
How do I get the first item of the list? In this case, I'm trying to isolate the value:
1.2.3.4
Is there a way to get a "collection" of all the items when the total number of items is not known before run time? (The list above has 6 items).
The following code doesn't seem to work:
output "first_ip" {
value = ["${azurerm_app_service.testap.outbound_ip_addresses[0]}"]
}
===================== APPEND =================
first_ip_no_index works. first_ip does not
output "first_ip_no_index" {
value = ["${split(",", azurerm_app_service.tf_middle_tier_azurerm_app_service.outbound_ip_addresses)}"]
}
output "first_ip" {
value = "${split(",", azurerm_app_service.tf_middle_tier_azurerm_app_service.outbound_ip_addresses)[0]}"
}
first_ip generated this error:
Error reading config for output first_ip: parse error at 1:91:
expected "}" but found "["
You can use the split() function to split a string into a list.
output "output_tf_testap_outbound_ip_addresses" {
value = ["${split(",", azurerm_app_service.testap.outbound_ip_addresses)}"]
}
After that you can then index it by using the element(list, index) syntax:
output "first_ip" {
value = "${element(split(",", azurerm_app_service.testap.outbound_ip_addresses), 0}"
}
You should also normally be able to use the list\[index\] syntax like this:
output "first_ip" {
value = "${split(",", azurerm_app_service.testap.outbound_ip_addresses)[0]}"
}
However there seems to be a bug in Terraform 0.11 that prevents slicing the result of the split function, throwing the following error:
Error: Error loading /tmp/tf-split-test/main.tf: Error reading config
for output foo: parse error at 1:25: expected "}" but found "["
You could use a local to split the list and then slice that to get around this if you'd prefer to use this syntax over the element function.
locals {
outbound_ip_addresses_list = "${split(",", azurerm_app_service.testap.outbound_ip_addresses)}"
}
output "first_ip" {
value = "${local.outbound_ip_addresses_list[0]}"
}

Resources