Terraform for-each loop on map or maps - terraform

I am trying to build a virtual network using a terraform script. I am struggling with creating a loop on the variable "vcn" below. My use case is as follows
A "vcn" object can have 1 to many "ads" objects
Each "ads" object has 1 to many "subnets" object
I represented the object as shown below ( assuming that is the correct representation )
How do I create a for-each loop in a terraform script?
variable "vcn" {
type = map(object({
vcn_cidr_block_in = string
ads = map(object({
subnets = map(object({
sub_cidr_block_in = string
sub_display_name_in = string
sub_dns_label_in = string
}))
ad_name = string
}))
}))
}
Appreciate any guidance to solve this problem.
TIA

Related

How can i set a count in for_each in terraform

I'm learning terraform by building a template to create my infrastructure in the hetzner cloud. For this purpose I'm using the hcloud provider.
I create a map variable hosts to create >1 server with different configuration.
variable "hosts" {
type = map(object({
name = string
serverType = string
serverImage = string
serverLocation = string
serverKeepDisk = bool
serverBackup = bool
ip = string
}))
}
This is working fine. But I need to configure also volumes. I need only for 2 servers additional volumes and terraform has to check if variable volume is true or not. If true a new volume with given details should be created and attached to the server.
For this I edit my variable hosts:
variable "hosts" {
type = map(object({
name = string
serverType = string
serverImage = string
serverLocation = string
serverKeepDisk = bool
serverBackup = bool
ip = string
volume = bool
volumeName = string
volumeSize = number
volumeFormat = string
volumeAutomount = bool
volumeDeleteProtection = bool
}))
}
in the main.tf the volume block looks like this, but it doesnt work because for_each and count cant be used together. How can I get what I'm looking for? Is that possible?
resource "hcloud_volume" "default" {
for_each = var.hosts
count = each.value.volume ? 1 : 0
name = each.value.volumeName
size = each.value.volumeSize
server_id = hcloud_server.default[each.key].id
automount = each.value.volumeAutomount
format = each.value.volumeFormat
delete_protection = each.value.volumeDeleteProtection
}
The former iterative meta-argument count will not provide you with the functionality you need here, as you need to access the volume bool type on a per var.hosts iteration in the map. To that end, you can add a conditional in a for expression within the for_each meta-argument.
for_each = { for host, values in var.hosts: host => values if values.volume }
This will construct a map for the value of the for_each meta-argument. It will contain every key value pair of var.hosts for which the volume object key is true.
It would seem like this would be a good fit for a collect or map method or function which transforms list and map types and exist in many other languages, but these do not yet exist in Terraform. Therefore, we use a for expression lambda equivalent.

How to concatenate strings in Terraform output with for loop?

I have multiple aws_glue_catalog_table resources and I want to create a single output that loops over all resources to show the S3 bucket location of each one. The purpose of this is to test if I am using the correct location (because it is a concatenation of variables) for each resource in Terratest. I cannot use aws_glue_catalog_table.* or aws_glue_catalog_table.[] because Terraform does not allow to reference a resource without specifying its name.
So I created a variable "table_names" with r1, r2, rx. Then, I can loop over the names. I want to create the string aws_glue_catalog_table.r1.storage_descriptor[0].location dynamically, so I can check if the location is correct.
resource "aws_glue_catalog_table" "r1" {
name = "r1"
database_name = var.db_name
storage_descriptor {
location = "s3://${var.bucket_name}/${var.environment}-config/r1"
}
...
}
resource "aws_glue_catalog_table" "rX" {
name = "rX"
database_name = var.db_name
storage_descriptor {
location = "s3://${var.bucket_name}/${var.environment}-config/rX"
}
}
variable "table_names" {
description = "The list of Athena table names"
type = list(string)
default = ["r1", "r2", "r3", "rx"]
}
output "athena_tables" {
description = "Athena tables"
value = [for n in var.table_names : n]
}
First attempt: I tried to create an output "athena_tables_location" with the syntax aws_glue_catalog_table.${table} but does does.
output "athena_tables_location" {
// HOW DO I ITERATE OVER ALL TABLES?
value = [for t in var.table_names : aws_glue_catalog_table.${t}.storage_descriptor[0].location"]
}
Second attempt: I tried to create a variable "table_name_locations" but IntelliJ already shows an error ${t} in the for loop [for t in var.table_names : "aws_glue_catalog_table.${t}.storage_descriptor[0].location"].
variable "table_name_locations" {
description = "The list of Athena table locations"
type = list(string)
// THIS ALSO DOES NOT WORK
default = [for t in var.table_names : "aws_glue_catalog_table.${t}.storage_descriptor[0].location"]
}
How can I list all table locations in the output and then test it with Terratest?
Once I can iterate over the tables and collect the S3 location I can do the following test using Terratest:
athenaTablesLocation := terraform.Output(t, terraformOpts, "athena_tables_location")
assert.Contains(t, athenaTablesLocation, "s3://rX/test-config/rX",)
It seems like you have an unusual mix of static and dynamic here: you've statically defined a fixed number of aws_glue_catalog_table resources but you want to use them dynamically based on the value of an input variable.
Terraform doesn't allow dynamic references to resources because its execution model requires building a dependency graph between all of the objects, and so it needs to know which exact resources are involved in a particular expression. However, you can in principle build your own single value that includes all of these objects and then dynamically choose from it:
locals {
tables = {
r1 = aws_glue_catalog_table.r1
r2 = aws_glue_catalog_table.r2
r3 = aws_glue_catalog_table.r3
# etc
}
}
output "table_locations" {
value = {
for t in var.table_names : t => local.tables[t].storage_descriptor[0].location
}
}
With this structure Terraform can see that output "table_locations" depends on local.tables and local.tables depends on all of the relevant resources, and so the evaluation order will be correct.
However, it also seems like your table definitions are systematic based on var.table_names and so could potentially benefit from being dynamic themselves. You could achieve that using the resource for_each feature to declare multiple instances of a single resource:
variable "table_names" {
description = "Athena table names to create"
type = set(string)
default = ["r1", "r2", "r3", "rx"]
}
resource "aws_glue_catalog_table" "all" {
for_each = var.table_names
name = each.key
database_name = var.db_name
storage_descriptor {
location = "s3://${var.bucket_name}/${var.environment}-config/${each.key}"
}
...
}
output "table_locations" {
value = {
for k, t in aws_glue_catalog_table.all : k => t.storage_descriptor[0].location
}
}
In this case aws_glue_catalog_table.all represents all of the tables together as a single resource with multiple instances, each one identified by the table name. for_each resources appear in expressions as maps, so this will declare resource instances with addresses like this:
aws_glue_catalog_table.all["r1"]
aws_glue_catalog_table.all["r2"]
aws_glue_catalog_table.all["r3"]
...
Because this is already a map, this time we don't need the extra step of constructing the map in a local value, and can instead just access this map directly to build the output value, which will be a map from table name to storage location:
{
r1 = "s3://BUCKETNAME/ENVNAME-config/r1"
r2 = "s3://BUCKETNAME/ENVNAME-config/r2"
r3 = "s3://BUCKETNAME/ENVNAME-config/r3"
# ...
}
In this example I've assumed that all of the tables are identical aside from their names, which I expect isn't true in practice but I was going only by what you included in the question. If the tables do need to have different settings then you can change var.table_names to instead be a variable "tables" whose type is a map of object type where the values describe the differences between the tables, but that's a different topic kinda beyond the scope of this question, so I won't get into the details of that here.

Adding a default field for type = map(object()) in variavles.tf

I have the following variable in my variables.tf file:
variable "accounts" {
type = map(object({
field1 = string,
field2 = list(string),
field3 = list(string),
field4 = list(string),
field5 = string
}))
}
I need to add a default field so that my users don't have to specify every field. For example, if field2 is an empty list, I don't want the user to have to define field2 =[]
I've tried some variations of the following but nothing seems to work.
default = {
default = {
"field1" = "",
"field2" = [],
"field3" = [],
"field4" = [],
"field5" = ""
}
}
Anyone have any idea of how to do it or know if it's even possible?
The closest thing in Today's Terraform is to have your callers set the attribute value to null and then handle that null somehow inside your module.
One way to do that is to combine the input variable with a local value that normalizes it, like this:
variable "example" {
type = map(object({
a = string
b = string
}))
}
locals {
example = {
for k, v in var.example : k => {
a = coalesce(v.a, "default a")
b = coalesce(v.b, "default b")
}
}
}
The coalesce function can be a good choice if you have a fallback value that isn't null or "", but really you can use any expression/function you like here to handle the null case in whatever way is appropriate for your needs.
You can then use local.example elsewhere in the module to get the normalized value, or var.example to get the raw value as given by the caller.
From a caller's perspective omitting those two attributes would look like this:
example = {
a = null
b = null
}
Terraform v0.14 (which is about to have its first beta release at the time I'm writing this) will include an experimental new feature to allow marking object type attributes as optional in type constraints:
terraform {
experiments = [module_variable_optional_attrs]
}
variable "example" {
type = map(object({
a = optional(string)
b = optional(string)
}))
}
The effect of this new optional(...) annotation is that the caller can omit that attribute when passing in an object value, in which case Terraform will perform the type conversion by inserting the attribute with the value null, rather than returning an error as it would by default.
Combined with the normalization approach I showed above this would then achieve the module interface you were looking for, without the need for callers to explicitly set null in order to omit the attributes:
example = {}
Unless this feature sees some show-stopping feedback during the Terraform 0.14 series it'll likely be stabilized in Terraform 0.15.
A default can only be defined at the variable level, so with your current variable structure there's no way around forcing the users to define all fields, except perhaps by pre-processing their input before it's passed to Terraform.

Terraform - use resource name and string for name

strange question. Can I, instead of using a variable, also use the resource property of an resource + a string to construct a name:
For example:
resource "azurerm_network_security_group" "nsgvmss" {
name = **"NSG - azurerm_resource_virtual_machine_scale_set.vmss.name"**
location = azurerm_resource_group.rgapp.location
resource_group_name = azurerm_resource_group.rgapp.name
}
this works of course with variables like "NSG, ${var.vssname}" but again,
was wondering if i can use the resource name of the object in TF as well
Thanks
This is called string interpolation. Also see expressions (which isn't as pre-0.12-centric)
...
name = "NSG - ${azurerm_resource_virtual_machine_scale_set.vmss.name}"
...

Terraform - List to String to create the azure subscription list

I am creating a new custom Azure role and trying to pass all the subscription IDs to the assignable scope argument using the below code but I am having issues converting from list to string with the correct string format. Here is the role definition module doc - https://www.terraform.io/docs/providers/azurerm/r/role_definition.html
Appreciate any inputs or guidance!
locals {
subscription_list = formatlist("/subscriptions/%s", data.azurerm_subscriptions.all.subscriptions[*].subscription_id)
quoted_subsciption_list = formatlist("%q", local.sub_list)
}
When I use join function to convert this into a string using the below code,
join(",", local.quoted_subsciption_list)
I am getting the output as "\"/subscriptions/7yed1028-4525-4533-b608-fb74c2a9c1rr\",\"/subscriptions/7uef9fad-dabf-8icf-8379-a3df99e7613c\",
I want the output to be "/subscriptions/7yed1028-4525-4533-b608-fb74c2a9c1rr", "/subscriptions/7uef9fad-dabf-8icf-8379-a3df99e7613c".```
locals {
subscription_list = formatlist("/subscriptions/%s", data.azurerm_subscriptions.all.subscriptions[*].subscription_id)
quoted_subsciption_list = formatlist("%q", local.sub_list)
}
When I use join function to convert this into a string using the below code,
```join(",", local.quoted_subsciption_list)```
I am getting the output as **"\"/subscriptions/7yed1028-4525-4533-b608-fb74c2a9c1rr\",\"/subscriptions/7uef9fad-dabf-8icf-8379-a3df99e7613c\",**
I want the output to be **"/subscriptions/7yed1028-4525-4533-b608-fb74c2a9c1rr", "/subscriptions/7uef9fad-dabf-8icf-8379-a3df99e7613c".**
local.subscription_list is already list(string) which is what azurerm_role_definition's assignable_scopes needs, so you can do this in your azurerm_role_definition resource block:
assignable_scopes = local.subsciption_list

Resources