terraform: performing a map operation on a list? - terraform

I have a terraform list
a = [1,2,3,4]
Is there a way for me to apply a function (e.g. *2) on the list, to get
b = [2,4,6,8]
I was looking for an interpolation syntax, perhaps map(a, _*2), or even something like
variable "b" {
count = "${length(a)}"
value = "${element(a, count.index)} * 2
}
As far as I can see no such thing exists. Am I missing something?

As per #Rowan Jacob's answer, this is now possible in v0.12 using the new for expression.
See: https://www.terraform.io/docs/configuration/expressions.html#for-expressions
variable "a" {
type = "list"
default = [1,2,3,4]
}
locals {
b = [for x in var.a : x * 2]
}
output "local_b" {
value = "${local.b}"
}
gives
Outputs:
local_b = [2, 4, 6, 8,]

This is currently an open issue. A new version of Terraform was recently announced which should give the ability to do this, among many other HCL improvements.
I think currently your best bet would be to create local values for each element of the list (remember that you can't use interpolation syntax in the default value of variables; locals exist to get around this limitation). However, I'm not sure if locals have a count attribute.

Related

Terraform Ternary Condition Working in Reverse

I have a Terraform module calling a submodule, which also calls another submodule. The final module uses a ternary condition as part of some logic to determine whether a dynamic block should be omitted in a resource definition.
I'm going to only include the pertinent code here, else it would get unnecessarily complicated.
The first module call:
module "foobar" {
source = "./modules/foobar"
...
vpc_cidr = "10.0.0.0/16"
# or vpc_cidr = null, or omitted altogether as the default value is null
...
}
The second module (in "./modules/foobar"):
module "second_level" {
source = "./modules/second_level"
...
vpc_config = var.vpc_cidr == null ? {} : { "some" = "things }
...
}
The third module (in "./modules/second_level"):
locals {
vpc_config = var.vpc_config == {} ? {} : { this = var.vpc_config }
}
resource "aws_lambda_function" "this" {
...
dynamic "vpc_config" {
for_each = local.vpc_config
content {
"some" = vpc_config.value["some"]
}
...
}
This is all horribly simplified, as I'm sure you're already aware, and you might have some questions about why I'm doing things like in the second level ternary operator. I can only say that there are "reasons", but they'd detract from my question.
When I run this, I expect the dynamic block to be filled when the value of vpc_cidr is not null. When I run it with a value in vpc_cidr, it works, and the dynamic block is added.
If vpc_cidr is null however, I get an error like this:
│ 32: security_group_ids = vpc_config.value["some"]
│ ├────────────────
│ │ vpc_config.value is empty map of dynamic
The really odd this is that if I swap the ternary around so it's actually the reverse of what I want, like this: vpc_config = var.vpc_config == {} ? { this = var.vpc_config } : {} everything works as I want.
EDIT
Some more context after the correct answer, because what I'm asking for indeed looks strange.
Wrapping this map into another single-element map with a hard-coded key if it's not empty
I was originally doing this because I needed to iterate just once over the map in the for_each block (and it contains more than a single key), so I'm faking a single key by putting a dummy key in there to iterate over.
As #martin-atkins points out in the answer though, for_each can iterate over any collection type. Therefore, I've simplified the locals assignment like this:
locals {
vpc_config = length(var.vpc_config) == 0 ? [] : [var.vpc_config]
}
This means that I can run a more direct dynamic block, and do what I really want, which is iterate over a list:
dynamic "vpc_config" {
for_each = local.vpc_config
content {
subnet_ids = var.vpc_config["subnet_ids"]
security_group_ids = var.vpc_config["security_group_ids"]
}
}
It's still a little hacky because I'm converting a map to a list of maps, but it makes sense more sense further up the chain of modules.
Using the == operator to compare complex types is very rarely what you want, because == means "exactly the same type and value", and so unlike many other contexts is suddenly becomes very important to pay attention to the difference between object types and map types, map types of different element types, etc.
The expression {} has type object({}), and so a value of that type can never compare equal to a map(string) value, even if that map is empty. Normally the distinction between object types and map types is ignorable because Terraform will automatically convert between them, but the == operator doesn't give Terraform any information about what types you mean and so no automatic conversions are possible and you must get the types of the operands right yourself.
The easiest answer to avoid dealing with that is to skip using == at all and instead just use the length of the collection as the condition:
vpc_config = length(var.vpc_config) == 0 ? {} : { this = var.vpc_config }
Wrapping this map into another single-element map with a hard-coded key if it's not empty seems like an unusual thing to be doing, and so I wonder if this might be an XY Problem and there might be a more straightforward way to achieve your goal here, but I've focused on directly answering your question as stated.
You might find it interesting to know that the for_each argument in a dynamic block can accept any collection type, so (unlike for resource for_each, where the instance keys are significant for tracking) you shouldn't typically need to create synthetic extra maps to fake conditional blocks. A zero-or-one-element list would work just as well for generating zero or one blocks, for example.
All of your code is behaving as expected. The issue here is that the dynamic block iterator is likely not being lazily evaluated at compilation, but rather only at runtime. We can workaround this by providing a "failover" value to resolve against for the situation when vpc_config.value is empty, and therefore has no some key.
content {
"some" = try(vpc_config.value["some"], null)
}
Since we do not know the specifics, we have to assume it is safe to supply a null argument to the some parameter.

Iterate a map in a map

I have a variable in tf declared as an object map that takes a string, and a child map of string values.
variable "apps" {
type = map(object({
hits = number,
id = map(string),
}))
Variable looks like this:
default = {
test = {
hits = 1,
id = {
one = "image-1234"
two = "image-4567"
}
}
In a resource declaration, I need the hits variable but also the child keys and values of the id map. How can I achieve this?
I have tried many code iterations, but would love to do something like:
name = id[each.key] to get the current key
I tried using count and getting a list of keys/values, e.g
I also tried to see if I can get the key value from the map, e.g.:
description = each.value[currentiteration].Value
In other words, I tried lots of combinations, even though the above is not right.

What is the key in complex data type map(object)

I am new to terraform and I am trying to understand the below code snippet.
Variable is of type map(objects) and Its looping over map(objects) and keys function takes a map and returns a list containing the keys from that map. Example: https://www.terraform.io/docs/configuration/functions/keys.html
I believe the output will be something like:
network_ids = {
network_alias = 123
network_alias = 456
network_alias = 789
}
What will be the value for network_alias? I went through many links but I am unable to understand.
code snippet:
locals {
network_ids = {
for network_alias in keys(var.networks) :
network_alias => aws_vpc.subnet[network_alias].id
}
}
variable "networks" {
type = map(object({
network_number = string
availability_zone = string
}))
}
I'm happy to break this down.
So, to begin with, network_ids is a terraform local value. Generally, local values are used to store off computations that you don't want to repeat over and over again.
network_ids is specifically a map for expression. map for expressions are used to build up maps from other, "enumerable" values.
In terraform a map is like a HashMap in other languages (dict in python, Hash in ruby, HashMap in Java and so on). map contains an association list mapping a unique key (always a string) to a value which could be of any, consistent type (meaning you can't have things like { "a" = 4, "b" = "c" } since 4 and "c" have different types).
In your specific example, for network_alias in keys(var.networks) says, basically
loop through the keys (again, strings) in var.networks and bind each one to the name network_alias.
The network_alias => aws_vpc.subnet[network_alias].id part says
build a new map where the key is the same as the keys we're looping over, and the value is a lookup of a subnet's ID indexed by the key.
That all being said, local.network_ids will not end up looking like the following, because maps have distinct keys and the comprehension actually evaluates the key value
network_ids = {
network_alias = 123
network_alias = 456
network_alias = 789
}
It's impossible for me to tell you exactly what the value will be because I don't know the value of var.networks nor that of aws_vpc.subnet.

Select a Range of Elements from a List in Terraform

Is there a way to select a range of elements from a list in Terraform?
For example - if we have:
[a, bb, ccc, dddd, eeeee]
How can the first 3 elements be selected?
a, bb, ccc
And then the 4th and 5th elements?
dddd, eeeee
Subsets of lists as you are looking for are often referred to as slices. Terraform has a built-in function for this, called slice, which is availble since version 0.8.8. You are looking for
slice(<put_reference_to_list_here, 0, 3)
slice(<put_reference_to_list_here, 3, 5)
From the documentation:
slice(list, from, to) - Returns the portion of list between from (inclusive) and to (exclusive).
Most notably slice is picky about the to parameter, which may must be less or equal to the lists length, otherwise TF will complain.
These fromIndex,toIndex interfaces are not intuitive for me, so I started to keep code snippets around for every language I (have to) use. This is my helper for Terraform:
variable "mylist" { default = [ 101, 102, 103, 104, 105 ] }
locals{
everything = "${slice(var.mylist, 0 , length(var.mylist) )}"
butlast = "${slice(var.mylist, 0 , length(var.mylist)-1)}"
thelast = "${slice(var.mylist, length(var.mylist)-1, length(var.mylist) )}"
}
data "null_data_source" "slices" {
inputs {
everything = "${join(",",local.everything)}"
butlast = "${join(",",local.butlast)}"
thelast = "${join(",",local.thelast)}"
}
}
output "slices" {
value = "${data.null_data_source.slices.outputs}"
}
To spare you the effort of terraform init; terraform refresh:
data.null_data_source.slices: Refreshing state...
Outputs:
slices = {
butlast = 101,102,103,104
everything = 101,102,103,104,105
thelast = 105
}
Use the slice function. That page describes the full expression language available to you.
Depending on where that list comes from, you might find it more convenient to split it up at its source. For instance, instead of declaring 5 aws_instance resources then trying to slice their output this way, have two separate aws_instance declarations of 3 and 2 instances respectively, and just deal with the entire list of outputs.

Argument list unpacking

I'm currently trying to retrieve the max value contained in a list, but the max() builtin accept a variable number of floats instead of a single list.
variable "my_list" {
default = [1, 2]
}
output map_out {
value = "${max(var.my_list)}"
}
Error: output.map_out: At column 3, line 1: max: argument 1 should be type float, got type list in:
There's a way to accomplish this?
I think I need something similar to the argument list unpacking in Python, but I don't know how to do this in Terraform.
Function "map" does not accept list, it only accept individual floats.
https://www.terraform.io/docs/configuration/interpolation.html#max-float1-float2-
so only that format will work.
output map_out {
value = "${max(var.my_list[0],var.my_list[1])}"
}
if the number of values can differ - may be sort will work instead.

Resources