How can I get a map using flatten in Terraform? - terraform

I have this variable:
applications = {
"app-name" = {
more_stuff = "x"
environments = ["dev", "stg"]
}
"workload" = {
random_thing = "y"
environments = ["dev"]
}
}
I want to create this map from it:
application_envs = {
"app-name-dev" = { more_stuff = "x" }
"app-name-stg" = { more_stuff = "x" }
"workload-dev" = { random_thing = "y" }
}
I've tried using flatten but I didn't have any success:
application_envs = flatten([
for application_key, application in var.applications : [
for env in application.environments : [
{"${application_key}-${env}" = workspace}
]
]
])
The problem is that it creates a list of objects:
result = [
{ "app-name-dev" = { ... } },
{ "app-name-stg" = { ... } },
...
]
How can I achieve the desired result?

You are headed in the correct direction, but for this kind of data transformation the algorithm requires two steps. Your first step is completely correct, but now you need the second step:
result = { for app_env in application_envs : keys(app_env)[0] => values(app_env)[0] }
This transforms the list(object) by iteratively mapping the key of each object element to the value of each object element. Testing the output produces:
result = {
app-name-dev = {
more_stuff = "x"
}
app-name-stg = {
more_stuff = "x"
}
}
as desired. Since the namespaces of the variables have been omitted from the question, I have likewise omitted them from the answer, but you may need to re-add them yourself when converting between the question code and the actual code.

Related

How to merge two level nested maps in terraform?

I know there is an open feature request for deepmerge but I just wanted to see if there is any work around for my use case. lets consider the following local variables:
locals {
default = {
class = "class1"
options = {
option1 = "1"
option2 = "2"
}
}
configs = {
configA = {
name = "A"
max = 10
min = 5
enabled = true
options = {
option3 = "3"
}
}
configB = {
name = "B"
max = 20
min = 10
enabled = false
}
}
}
so I can merge the configs with default like this:
for key, config in local.configs : key => merge(local.default, config)
and the result will be:
configs = {
configA = {
name = "A"
class = "class1"
max = 10
min = 5
enabled = true
options = {
option3 = "3"
}
}
configB = {
name = "B"
class = "class1"
max = 20
min = 10
enabled = false
options = {
option1 = "1"
option2 = "2"
}
}
}
The problem is the nested map (options property) gets completely replaced by configA since merge cannot handle nested merge. Is there any work around for it in terraform 1.1.3 ?
If you know the structure of the map, you can merge the included elements separately as you wish.
In this case this should work:
merged = {
for key, config in local.configs : key =>
merge(
local.default,
config,
{ options = merge(local.default.options, lookup(config, "options", {})) }
)
}
So first merge the top-level elements, and then handle the options separately.
There are various ways to do this including using this deepmerge provider:
https://registry.terraform.io/modules/cloudposse/config/yaml/0.5.0/submodules/deepmerge
Here is a way that assumes only that /usr/bin/jq exists. I am not saying it is pretty, but it does work and ensures that you get the same semantics as a jq * operator.
locals {
left = {...}
right = {...}
merged = {
for k, v in data.external.merge.result : k => jsondecode(v)
}
}
data "external" "merge" {
program = [
"/usr/bin/jq",
"((.left|fromjson) * (.right|fromjson))|with_entries(.value|=tojson)"
]
query = {
left = jsonencode(local.left)
right = jsonencode(local.right)
}
}

How to create a set by loop over a nested map

I have a map that I want to read in locals and generate a new map from. One field in the new map will be a set containing the values from the nested data structure. I can't figure out the syntax to do this.
//I want to generate a set of all zones from the nested zone fields
variable "my_var" {
type = object({
name = string
google_bigtable_clusters = any
})
default = {
app_name = "sdfsdfds"
instances = {
instance01 = [
{
zone = "asia-east1-a"
num_nodes = 1
},
{
zone = "asia-east1-b"
num_nodes = 1
},
{
zone = "asia-east1-c"
num_nodes = 1
},
{
zone = "asia-east2-a"
num_nodes = 1
},
],
instance02 = [
{
zone = "europe-west2-a"
num_nodes = 1
},
{
zone = "europe-west2-b"
num_nodes = 1
},
{
zone = "europe-west2-c"
num_nodes = 1
},
{
zone = "europe-west3-a"
num_nodes = 1
},
]
}
}
}
This throws The key expression produced an invalid result: string required.
// locals
new_map = {
some_field = "arbitrary string"
set_of_zones = {
for item in var.my_var.instances : item => {
for subitem in item : subitem.zone => {
zone = subitem.zone
}
}
}
}
I also tried to get the key name but that didn't work: for item in var.my_var.instances : item.key => {
Edit
I was able to do this but I don't understand why I don't have access to the key name here. I want to use the instance01, instance02, etc key name here: for item in var.my_var.instances : item[0].zone => {.
First, your type for your variable is all messed up. You have:
type = object({
name = string
google_bigtable_clusters = any
})
This means that Terraform will accept a value for the variable only if it has these two fields: name (a string) and google_bigtable_clusters (can be anything).
Your default value has neither of these fields. Instead, it only contains app_name and instances, so that's likely the cause of the first issue.
Regarding why you can't access the key name in your for loop, you need to specify both the key and value:
set_of_zones = {
for key, val in var.my_var.instances :
key => {
for subval in val:
subval.zone => {
zone = subval.zone
}
}
}
This is a really odd thing to want to do though, because you're going to end up with a map that looks like:
set_of_zones = {
instance01 = {
"asia-east1-a" => {
zone = "asia-east1-a"
}
}
}
Which doesn't seem super helpful since there is only one attribute in each map, and that attribute's value is the same as the key for that map.

Merging module output map

I'm trying out the new for_each function on a module, which itself outputs some values that I need to pass into another resource.
module "vnets" {
source = "../caf-virtual-network"
for_each = var.vnet_list
ARM_ENVIRONMENT = var.ARM_ENVIRONMENT
ARM_LOCATION = var.ARM_LOCATION
ARM_SUBSCRIPTION_ID = var.ARM_SUBSCRIPTION_ID
diagnostics_map = local.diagnostics_map
location = var.ARM_LOCATION
netwatcher = local.netwatcher
networking_object = each.value
tags = var.global_settings.tags
virtual_network_rg = "${module.names.standard["resource-group"]}-${each.value.vnet.resource_group_name}"
depends_on = [
module.resource_groups_networking
]
}
I can grab the output of the module for one or more of those objects by specifying something like this
output "subnets" { value = module.vnets["vnet_shared_services_object"].vnet_subnets } , which in turn looks like this:
"vnet_shared_services_object" = {
"sn-dev-uks-asdf-app-dynamic" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-app-dynamic"
"sn-dev-uks-asdf-artifactory" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-artifactory"
}
Here I'm specifying the output of ONE object, but I want to dynamically specify the output of both objects in one hit.
So I want this;
"vnet_shared_services_object" = {
"sn-dev-uks-asdf-app-dynamic" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-app-dynamic"
"sn-dev-uks-asdf-artifactory" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-artifactory"
}
"vnet_transit_object" = {
"AzureFirewallSubnet" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/AzureFirewallSubnet"
"GatewaySubnet" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/GatewaySubnet"
"sn-dev-uks-asdf-bind-dns" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/sn-dev-uks-asdf-bind-dns"
}
...output to look like this:
subnets = {
"sn-dev-uks-asdf-app-dynamic" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-app-dynamic"
"sn-dev-uks-asdf-artifactory" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-artifactory"
"AzureFirewallSubnet" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/AzureFirewallSubnet"
"GatewaySubnet" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/GatewaySubnet"
"sn-dev-uks-asdf-bind-dns" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/sn-dev-uks-asdf-bind-dns"
}
So i know doing the following will work, but the point i'm trying to make is that i don't know how many vnet modules i'm going to produce, and thus i need to make this dynamic:
output merge{
value = merge({
for key, value in module.vnets["vnet_shared_services_object"].vnet_subnets:
key => value
},
{
for key, value in module.vnets["vnet_transit_object"].vnet_subnets:
key => value
})
}
Using the guide on Terraform to flatten (https://www.terraform.io/docs/configuration/functions/flatten.html) the output object works, but it's not how i wish for it to be:
output stuff {
value = flatten([
for key, value in module.vnets: [
for subnet, id in value.vnet_subnets: {
"${subnet}" = id
}
]
])
}
...which equats to:
stuff = [
{
"sn-dev-uks-asdf-app-dynamic" = "/subscriptions/asdf/resourceGroups/rg-dev-uks-asdf-vnet-shared-services/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-app-dynamic"
},
{
"sn-dev-uks-asdf-artifactory" = "/subscriptions/asdf/resourceGroups/rg-dev-uks-asdf-vnet-shared-services/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-artifactory"
},
...and so on
]
an FYI, this does not help me :(
output {
value = merge(
for key, value in module.vnets:
key => value.vnets_subnets
)
}
Any help on this would be greatly appreciated!
I'm not sure if I correctly understand the input maps, but I tried to replicate the issue creating some mock variables.
For that I created the following variables:
variable "vnets" {
default = {
"vnet_shared_services_object" = {
"sn-dev-uks-asdf-app-dynamic" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-app-dynamic"
"sn-dev-uks-asdf-artifactory" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-artifactory"
}
}
}
variable "vnet_subnets" {
default = {
"vnet_transit_object" = {
"AzureFirewallSubnet" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/AzureFirewallSubnet"
"GatewaySubnet" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/GatewaySubnet"
"sn-dev-uks-asdf-bind-dns" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/sn-dev-uks-asdf-bind-dns"
}
}
}
Then the output was defiend as:
output stuff {
value = {for k,v in flatten([
for key, value in merge(var.vnets, var.vnet_subnets):
[for subkey1, subval1 in value: {"${subkey1}" = subval1}]
]): keys(v)[0] => values(v)[0]}
}
which resulted in:
stuff = {
"AzureFirewallSubnet" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/AzureFirewallSubnet"
"GatewaySubnet" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/GatewaySubnet"
"sn-dev-uks-asdf-app-dynamic" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-app-dynamic"
"sn-dev-uks-asdf-artifactory" = "/subscriptions/asdf/resourceGroups/asdf/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-shared-services/subnets/sn-dev-uks-asdf-artifactory"
"sn-dev-uks-asdf-bind-dns" = "/subscriptions/asdf/resourceGroups/qwer/providers/Microsoft.Network/virtualNetworks/vnet-dev-uks-asdf-transit/subnets/sn-dev-uks-asdf-bind-dns"
}
A colleague was able to answer this question with the following code:
locals {
subnet_list = {
for key, value in module.vnets:
key => value.vnet_subnets
}
subnet_map = merge(values(local.subnet_list)...)
}
it is the ... operator which is the key takeaway from this. you can look it up here; https://www.terraform.io/docs/configuration/expressions.html#expanding-function-arguments
... will expand a list of items to function parameters, hence you can call merge to merge a list of map

terraform how to describe variable type with changing keys in object

I've got an ever changing list of objects as variable and wanted to know how to properly describe its type
variable "lifecycle_rules" {
type = set(object({
# set(object({
# action = map(string)
# condition = map(string)
# }))
}))
default = [
{
first = [
{
condition = {
age = "1"
}
action = {
type = "Delete"
}
},
{
condition = {
age = "2"
}
action = {
type = "Delete"
}
}
]},
{
second = [
{
condition = {
age = "3"
}
action = {
type = "Delete"
}
},
{
condition = {
age = "4"
}
action = {
type = "Delete"
}
}
]
}
]
}
Here should be line with smth like this string = set(object({...
the first and second are always changing, so key value should be
string but can't really set it - any other thoguhts, how to write
type for the default below ?
You are almost there. I think the correct one is:
type = set(
map(
set(
object({condition = map(string),
action = map(string)})
)
)
)
In the map you don't specify attributes, as they can be different. In the most inner one you have object as condition and action are constant.

Terraform - How to modify map keys?

In Terraform 0.12.xx, is it possible to modify the keys in the map instead of their respective values? Let us assume that we have the following in the module definition:
locals {
task_logging = [
for k, v in var.task_logging_options : {
name = trimprefix(k,"TASK_LOGGING_")
value = v
}
]
}
However, trimprefix here only applies on value.
Then down below I am creating a task definition for ECS service:
{...}
"logConfiguration": {
"logDriver": "awsfirelens",
"secretOptions": [],
"options": ${jsonencode(local.task_logging_options)}
},
{...}
And finally, in the module instantiation, I am passing task_logging_options as follows:
task_logging_options = {
TASK_LOGGING_Name = "es"
TASK_LOGGING_Host = "some.host"
}
Where local function should strip the prefix TASK_LOGGING_ to build a JSON object for Fluentbit configuration.
End result should be an object, similar to snippet from terraform plan:
~ logConfiguration = {
logDriver = "awsfirelens"
~ options = {
- Buffer_Size = "False" -> null
- HTTP_Passwd = "READACTED" -> null
- HTTP_User = "READACTED" -> null
- Host = "READACTEDd" -> null
- Index = "READACTED" -> null
- Name = "es" -> null
- Port = "READACTED" -> null
+ TASK_LOGGING_Host = "some.host"
+ TASK_LOGGING_Name = "es"
- Tls = "On" -> null
- Trace_Output = "On" -> null
}
secretOptions = []
}
Not fully understand what do you want to achieve, but you can use trimprefix(k,"TASK_LOGGING_") as key as well.
For example:
locals {
task_logging2 = [
for k, v in var.task_logging_options : {
trimprefix(k,"TASK_LOGGING_") = v
}
]
}
will result in local.task_logging2 being:
[
{
"Host" = "some.host"
},
{
"Name" = "es"
},
]
Update
If object is required, the following could be used:
locals {
task_logging2 = {
for k, v in var.task_logging_options :
trimprefix(k,"TASK_LOGGING_") => v
}
}
which results in local.task_logging2 being:
{
"Host" = "some.host"
"Name" = "es"
}

Resources