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
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.
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
}
}
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.
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)
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.