Terraform nested loop in nested list - terraform

I'm trying to create a dynamic method to create vms in multiple environments that will be configured by the end user.
Tried for loops with nested loops. flatten function, count, etc but haven't found a way to reach my goal.
I have terrafrom.tfvars with the follwing structure:
Bigip_devices = {
main_hub = {
region = "eastus"
azs = ["1"] #Azure availabilty zones
vnet_name = "vnet-main" # Vnet name to deploy to
bigip_instance_count = 2 # Number of instnaces to delpoy
cluster = "yes" # Deploy as a cluster or stand alone device
version = "" # Leave blank for default value
sku = "" # Leave blank for default value - f5-bigip-virtual-edition-25m-best-hourly
offer = "" # Leave blank for default value - f5-big-ip-best
instance_type = "" # Leave blank for default value - Standard_DS3_v2
disable_password_authentication = "" #Leave blank for default value
tags = ""
}
spoke = {
region = "eastus"
azs = ["1","2"] #Azure availabilty zones
vnet_name = "vnet-spoke" # Vnet name to deploy to
bigip_instance_count = 4 # Number of instnaces to delpoy
cluster = "yes" # Deploy as a cluster or stand alone device
version = "" # Leave blank for default value
sku = "" # Leave blank for default value - f5-bigip-virtual-edition-25m-best-hourly
offer = "" # Leave blank for default value - f5-big-ip-best
instance_type = "" # Leave blank for default value - Standard_DS3_v2
disable_password_authentication = "" #Leave blank for default value
tags = ""
}
}
What is the correct method to iterate each key in the list( in the example the are 2 keys - main hub and spoke) and to create the amount of virtual machines corresponding to the bigip_instance_count setting.
In the above example, I want to create 2 environments, one with 2 devices and the second with 4 devices.
Is there a way to achieve it?

It would be really convenient if you transform the above complex JSON into a collection that has one element per resource you want to create. To achieve this, you could use the flatten function.
locals {
# A list of objects with one object per instance.
flattened_values = flatten([
for ip_key, ip in var.Bigip_devices : [
for index in range(ip.bigip_instance_count) : {
region = ip.region
azs = ip.azs
ip_index = index
ip_key = ip_key
cluster = ip.cluster
version = ip.version
sku = ip.sku
offer = ip.offer
instance_type = ip.instance_type
disable_password_authentication = ip.disable_password_authentication
tags = ip.tags
}
]
])
}
With the above flattened function, you get below list of collection of resources, you would like to create.
flattened_value_output = [
{
"azs" = [
"1",
]
"cluster" = "yes"
"disable_password_authentication" = ""
"instance_type" = ""
"ip_index" = 0
"ip_key" = "main_hub"
"offer" = ""
"region" = "eastus"
"sku" = ""
"tags" = ""
"version" = ""
},
{
"azs" = [
"1",
]
"cluster" = "yes"
"disable_password_authentication" = ""
"instance_type" = ""
"ip_index" = 1
"ip_key" = "main_hub"
"offer" = ""
"region" = "eastus"
"sku" = ""
"tags" = ""
"version" = ""
},
{
"azs" = [
"1",
"2",
]
"cluster" = "yes"
"disable_password_authentication" = ""
"instance_type" = ""
"ip_index" = 0
"ip_key" = "spoke"
"offer" = ""
"region" = "eastus"
"sku" = ""
"tags" = ""
"version" = ""
},
{
"azs" = [
"1",
"2",
]
"cluster" = "yes"
"disable_password_authentication" = ""
"instance_type" = ""
"ip_index" = 1
"ip_key" = "spoke"
"offer" = ""
"region" = "eastus"
"sku" = ""
"tags" = ""
"version" = ""
},
{
"azs" = [
"1",
"2",
]
"cluster" = "yes"
"disable_password_authentication" = ""
"instance_type" = ""
"ip_index" = 2
"ip_key" = "spoke"
"offer" = ""
"region" = "eastus"
"sku" = ""
"tags" = ""
"version" = ""
},
{
"azs" = [
"1",
"2",
]
"cluster" = "yes"
"disable_password_authentication" = ""
"instance_type" = ""
"ip_index" = 3
"ip_key" = "spoke"
"offer" = ""
"region" = "eastus"
"sku" = ""
"tags" = ""
"version" = ""
},
]
From the above collection, you could iterate & create resources with unique keys like below::
resource "some_resource" "example" {
for_each = {
# Generate a unique string identifier for each instance
for ip in local.flattened_value_output : format("%s-%02d", ip.ip_key, ip.ip_index + 1) => ip
}
}
This way, the creation or updating of resources is guaranteed as each resource uses a unique key.
For more details, refer this discussion I had with Hashicorp personnel.

Related

Terraform using random provider with count in module

I am having child module for Windows virtual machine.
Then I have root module (main.tf file), where I am using that child module
module "vm-win-resource" {
source = "./Modules/ServerWindows"
count = 2
vm-name = "vm-win-${random_string.rnd.result}" #OR "vm-win-${module.rnd-num.rnd-result}"
vm-rg = module.rg-resouce.rg-name
vm-location = module.rg-resouce.rg-location
nic-name = "vm-win-${random_string.rnd.result}-nic1" #OR "vm-win-${module.rnd-num.rnd-result}-nic1"
nic-rg = module.rg-resouce.rg-name
nic-location = module.rg-resouce.rg-location
nic-ip-subnet = "HERE IS SUBNET ID"
}
In same main.tf file, if I use random_string provider directly
resource "random_string" "rnd" {
length = 4
min_numeric = 4
special = false
lower = true
}
or if I create module, for random number and use it in module for virtual machine, result is same.
module "rnd-num" {
source = "./Modules/RandomNumber"
}
I get same name (generated number for both)
+ vm-win-name = [
+ [
+ "vm-win-6286",
+ "vm-win-6286",
],
]
So in both cases, value is generated only once.
Question is how can I generate random number for every loop in module for virtual machine?
Thank you for any help!
UPDATE
As workaround, I have placed provider to generate random number into virtual machine resource/module specification
resource "azurerm_windows_virtual_machine" "vm-resource" {
name = "${var.vm-name}-${random_string.rnd.result}"
resource_group_name = var.vm-rg
location = var.vm-location
size = var.vm-size
admin_username = var.vm-admin
admin_password = var.vm-adminpwd
network_interface_ids = [
azurerm_network_interface.nic-resource.id,
]
os_disk {
caching = "ReadWrite"
storage_account_type = var.vm-os-disk-type
}
source_image_reference {
publisher = var.vm-os-image.publisher
offer = var.vm-os-image.offer
sku = var.vm-os-image.sku
version = var.vm-os-image.version
}
tags = var.resource-tags
}
resource "random_string" "rnd" {
length = 4
min_numeric = 4
special = false
lower = true
}
it does the job but I would prefer to use it in main.tf file and not directly in resource/module specification, if it is possible.
A few words about how Terraform random_string works:
random_string generates a random string from specific characters. This string is generated once. Referencing its result attribute in multiple places will provide you the same output. Using it as random_string.rnd.result will not act as a function call, this means that it will provide the same value in every place.
The result value of a random_string will not change after consecutive applies. This is obvious, if we think about it. If it would change, the usage of random_string would be dangerous, since it would result in re-provisioning the resources which are referencing it.
If we want to have multiple different random strings, we have to define multiple random_string resources. For example:
resource "random_string" "rnd" {
count = 2
length = 4
min_numeric = 4
special = false
lower = true
}
module "vm-win-resource" {
source = "./Modules/ServerWindows"
count = 2
vm-name = "vm-win-${random_string.rnd[count.index].result}"
vm-rg = module.rg-resouce.rg-name
vm-location = module.rg-resouce.rg-location
nic-name = "vm-win-${random_string.rnd[count.index].result}-nic1"
nic-rg = module.rg-resouce.rg-name
nic-location = module.rg-resouce.rg-location
nic-ip-subnet = "HERE IS SUBNET ID"
}
Please note, we are using a count for the random_string resource as well.

how to create multiple dynamic block entries from a list pulled from a map

i have varible which is a map
variable ltm-datagroups = {
"0" = {
datagroup_name = "abc"
datagroup_type = "ip"
datagroup_addresses = ["10.0.0.0/8", "172.16.1.0/24"]
}
"1" = {
datagroup_name = "def"
datagroup_type = "ip"
datagroup_addresses = ""
}
}
i pass this map to a module, which looks up the value for "datagroup_addresses". from that i want to create multiple entries, based on length of the list.
resource "bigip_ltm_datagroup" "datagroup" {
for_each = var.ltm-datagroups
name = lookup(var.ltm-datagroups[each.key], "datagroup_name")
type = lookup(var.ltm-datagroups[each.key], "datagroup_type")
dynamic "record" {
for_each = lookup(var.ltm-datagroups[each.key], "datagroup_addresses") != "" ? ["${length(lookup(var.ltm-datagroups[each.key], "datagroup_addresses"))}"] : []
content {
name = lookup(var.ltm-datagroups[each.key], "datagroup_addresses")
}
}
}
this is the error i see
Error: Incorrect attribute value type
on modules/ltm-datagroup/main.tf line 8, in resource "bigip_ltm_datagroup" "datagroup":
8: name = lookup(var.ltm-datagroups[each.key], "datagroup_addresses")
|----------------
| each.key is "0"
| var.ltm-datagroups is object with 2 attributes
Inappropriate value for attribute "name": string required.
Error: Incorrect attribute value type
on modules/ltm-datagroup/main.tf line 8, in resource "bigip_ltm_datagroup" "datagroup":
8: name = lookup(var.ltm-datagroups[each.key], "datagroup_addresses")
|----------------
| each.key is "1"
| var.ltm-datagroups is object with 2 attributes
Inappropriate value for attribute "name": string required.
i am stuck on the last part. how to run the dynamic block multiple times? while iterating through the entries in the list?
Not sure I fully understand your desired outcome, but if you want to create record dynamically, then it should be:
dynamic "record" {
for_each = lookup(var.ltm-datagroups[each.key], "datagroup_addresses") != "" ? toset(lookup(var.ltm-datagroups[each.key], "datagroup_addresses")) : []
content {
name = record.value
}
}

Terraform- Azure IPgroups tags update for no reason

I'm using Azure and terraform to deploy some ipgroups.
I'm using loops and modules to deploy my ipgroups and everything is deploying correctly.
But when I do a terraform plan after my deployment, terraforms says it will update my ipgroups tags.
And of course, I changed nothing between 2 terraform runs :
# module.CreateAzureRmIpGroup.azurerm_ip_group.ipgroup[0] will be updated in-place
~ resource "azurerm_ip_group" "ipgroup" {
id = "xxx"
name = "ipgr-all-allservers-weeu-001"
~ tags = {
+ "Area" = "westeurope"
+ "Business_Line" = "bb"
+ "Creation_Date" = "10/02/2021"
+ "Environment" = "hub"
+ "Gdpr" = "2"
+ "Owner" = "aa"
+ "Ressource_Type" = "ipgroup"
- "area" = "westeurope" -> null
- "business_Line" = "bb" -> null
- "creation_Date" = "10/02/2021" -> null
- "environment" = "hub" -> null
- "gdpr" = "2" -> null
- "owner" = "aa" -> null
- "ressource_Type" = "ipgroup" -> null
}
# (3 unchanged attributes hidden)
}
As you can see , It will pass my tags to null and pass from null to my old tags. Just the tags , not even the ip of ipgroups...
I create the tags like I do for others services and I only have a problem with ipgroups tags.....(no pb with networks/firewalls/vpn)
Here's my ipgroup resource :
resource "azurerm_ip_group" "ipgroup" {
count = length(var.ipgroup)
name = "ipgr-${var.ipgroup[count.index]["ipgr_content"]}-${var.ipgroup[count.index]
["ipgr_loc"]}-${var.ipgroup[count.index]["id"]}"
location = var.ipgroup_location
resource_group_name = var.ipgroup_rg_name
cidrs = split(",", var.ipgroup[count.index]["ipgr_cidr"])
tags = {
Owner = var.ipgroup[count.index]["owner"]
Business_Line = var.ipgroup[count.index]["business_line"]
Area = var.ipgroup[count.index]["area"]
Environment = var.ipgroup[count.index]["environment"]
Creation_Date = var.ipgroup[count.index]["creation_date"]
Gdpr = var.ipgroup[count.index]["gdpr"]
Ressource_Type = "ipgroup"
}}
And this is a tfvars example :
ipgroup = [{
ipgr_content = "all-allservers"
ipgr_loc = "weeu"
id = "001"
ipgr_cidr = "10.0.0.0/20"
owner = "aa"
business_line ="bb"
area = "westeurope"
environment = "hub"
creation_date = "10/02/2021"
gdpr = "2"
},]
I tried to comment my tags but even with that terraform still update the ressource's tag.
I don't know if there is a real impact to have an update everytime (I will have to do an apply if I have to add more ipgroups). I don't want to take the risk when I will use these ipgroups in my firewall.

I have created 4 subnets and created output values to get all subnets ids in one variable .So how can I plan to retrieve 2 values to attach the nics

Error: Unbalanced parentheses
on .terraform\modules\nics\main.tf line 19, in resource "azurerm_network_interface" "NIC1":
19: subnet_id = "${element(var.subnetwork-subnetid.*.id, (0,1))}"
output values of subnets:
output "subnetwork-subnetid" {
value = concat(azurerm_subnet.subnetwork.*.id, azurerm_subnet.subnetwork6.*.id)
}
nic.tf
resource "azurerm_network_interface" "NIC1" {
#count = "${length(var.subnetwork-subnetid)}"
#for_each= toset(var.subipv4)
count = "${length(var.subipv4)}"
name = "${lookup(element(var.subipv4, count.index), "name")}"
#name = var.nic-name
location = var.rg-location
resource_group_name = var.rg-name
enable_ip_forwarding = true
enable_accelerated_networking = true
ip_configuration {
name = "ipconfig"
subnet_id = "${element(var.subnetwork-subnetid.*.id, (0,1))}"
private_ip_address_allocation = "Dynamic"
#public_ip_address_id = azurerm_public_ip.pubip.id
#public_ip_address_id = azurerm_public_ip.pubip.*.id
primary = true
}
tags = {
name = "${lookup(element(var.subipv4, count.index), "name")}"
}
}```
Please someone help me in this issue.Thanks!
Second argument in element is index:
index finds the index for a particular element value.
Thus to get few elements from the list based on indices, you can do:
subnet_id = [ for idx in [0, 1]: element(var.subnetwork-subnetid.*.id, idx) ]
If you want a range of indies, you can use slice:
subnet_id = slice(var.subnetwork-subnetid.*.id, 0, 2)

Ommit optional blocks in terraform module

Currently I'm trying to create a universal sql_database module in Terraform. I want to have control over arguments I want to include in this resource. For example one time I need only required arguments but next time in another project I need them plus threat_detection_policy block with all nested arguments.
modules/sql_database.tf
resource "azurerm_sql_database" "sql-db" {
name = var.sql-db-name
resource_group_name = data.azurerm_resource_group.rg-name.name
location = var.location
server_name = var.server-name
edition = var.sql-db-edition
collation = var.collation
create_mode = var.create-mode
requested_service_objective_name = var.sql-requested-service-objective-name
read_scale = var.read-scale
zone_redundant = var.zone-redundant
extended_auditing_policy {
storage_endpoint = var.eap-storage-endpoint
storage_account_access_key = var.eap-storage-account-access-key
storage_account_access_key_is_secondary = var.eap-storage-account-access-key-is-secondary
retention_in_days = var.eap-retention-days
}
import = {
storage_uri = var.storage-uri
storage_key = var.storage-key
storage_key_type = var.storage-key-type
administrator_login = var.administrator-login
administrator_login_password = var.administrator-login-password
authentication_type = var.authentication-type
operation_mode = var.operation-mode
}
threat_detection_policy = {
state = var.state
disabled_alerts = var.disabled-alerts
email_account_admins = var.email-account-admins
email_addresses = var.email-addresses
retention_days = var.retention-days
storage_account_access_key = var.storage-account-access-key
storage_endpoint = var.storage-endpoint
use_server_default = var.use-server-default
}
}
modules/variables.tf (few sql_database vars)
variable "sql-db-edition" {
type = string
}
...
variable "state" { #for example this should be optional
type = string
}
...
main.tf
module "sql_database" {
source = "./modules/sql_database"
sql-db-name = "sqldbs-example"
location = "westus"
server-name = "sqlsrv-example"
storage-uri = "" #some values
storage-key = ""
storage-key_type = ""
administrator-login = ""
administrator-login-password = ""
authentication-type = ""
operation-mode = ""
sql-db-edition = "Standard"
collation = "SQL_LATIN1_GENERAL_CP1_CI_AS"
create-mode = "Default"
sql-requested_service_objective_name = "S0"
requested_service_objective_id = ""
read-scale = "false"
zone_redundant = ""
source_database_id = ""
restore_point_in_time = ""
max_size_bytes = ""
source_database_deletion_date = ""
elastic_pool_name = ""
#variables below should be all optional
state = ""
disabled_alerts = ""
email_account_admins = ""
email_addresses = ""
retention_days = 6
storage_account_access_key = ""
storage_endpoint = ""
use_server_default = ""
storage_endpoint = ""
storage_account_access_key = ""
storage_account_access_key_is_secondary = "false"
retention_in_days = 6
}
Thank you in advance for help!
For your requirements, I think a possible way is to set the default values inside the module and make the default values act as you do not set them. For example, in the threat_detection_policy block, the property use_server_default, when you do not set it, the default value is Disabled. And when you want to set them, just input the values in the module block.

Resources