Understanding Terraform for_each loop iteration - terraform

I am learning terraform and trying to understand the for_each loop iteration in terraform.
I am iterating through a loop for creating RGs in Azure cloud and what I want to understand is the difference between accessing the value of an instance using . or [""].
So for example, below is my tfvar file:
resource_groups = {
resource_group_1 = {
name = "terraform-apply-1"
location = "eastus2"
tags = {
created_by = "vivek89#test.com"
}
},
resource_group_2 = {
name = "terraform-apply-2"
location = "eastus2"
tags = {
created_by = "vivek89#test.com"
}
},
resource_group_3 = {
name = "terraform-apply-3"
location = "eastus2"
tags = {
created_by = "vivek89#test.com"
contact_dl = "vivek89#test.com"
}
}
}
and below is my terraform main.tf file:
resource "azurerm_resource_group" "terraformRG" {
for_each = var.resource_groups
name = each.value.name
location = each.value.location
tags = each.value.tags
}
I am confused with the expression in for_each in RG creation block. Both the below codes works and create RGs:
name = each.value.name
name = each.value["name"]
I want to understand the difference between the two and which one is correct.

They are equivalent as explained in the docs:
Map/object attributes with names that are valid identifiers can also be accessed using the dot-separated attribute notation, like local.object.attrname. In cases where a map might contain arbitrary user-specified keys, we recommend using only the square-bracket index notation (local.map["keyname"]).
The main difference is that dot notation requires key attributes to be valid identifiers. In contrast, the square-bracket notation works with any identifiers.

Related

Terraform: Easier way to run a flexible number of objects

Currently I have a powershell script that reads a yaml config file with all the objects I need created and creates a .tfvars file which contains all the variables, maps, lists of maps etc.
It would be something like the following:
global_tags = {
Provisioner = "Terraform"
}
resource_groups = {
myrg1 = {
location = "uksouth",
tags = {
ResourceType = "resourcegroup"
}
}
}
storage_accounts = {
mystorage1 = {
resource_group_name = "myrg1",
location = "uksouth",
account_tier = "Standard",
account_replication_type = "GRS",
tags = {
ResourceType = "storageaccount"
}
containers_list = [
{ name = "test_private_x", access_type = "private" },
{ name = "test_blob_x", access_type = "blob" },
{ name = "test_container_x", access_type = "container" }
]
}
The idea is to then pump each list of maps into each module to create the resources, e.g. main.tf would be just:
module "resourcegroup" {
source = "./modules/azure-resourcegroup"
resource_groups = var.resource_groups
global_tags = var.global_tags
}
module "storageaccount" {
source = "./modules/azure-storageaccount"
depends_on = [module.resourcegroup]
storage_accounts = var.storage_accounts
global_tags = var.global_tags
}
Also, an example of a simple module would be:
resource "azurerm_resource_group" "rg" {
for_each = var.resource_groups
name = each.key
location = each.value.location
tags = lookup(each.value,"tags",null) == null ? var.global_tags : merge(var.global_tags,each.value.tags)
}
The issue is that writing a complex module, say around storage account, isn't too bad if you are just feeding in all the params, but feeding in a list of maps and writing a module to read that list and create multiple flattened lists to perform say 15 different calls (to create containers, shares, network rules etc.) is very complex.
Obviously the reason I want to use for_each loops in the modules is so that my main.tf doesn't have to call the module multiple times with hard coded values for say 50 storage accounts.
Just wondering if I am missing an obvious way to create complicated multiples of each resource type ?
I appreciate I could do separate modules for containers, shares etc and break the complex maps down into simpler ones to pass to the additional modules, but I was trying to just have 1 storage account module that could handle anything and be fed by a complex list of maps so main.tf did not need editing, I could just control the config completely via a .tfvars file

Terraform error: Expected an attribute value, introduced by an equals sign ("=")

I'm really new to terraform and I'm having an issue doing something very basic.
I will only put in the relevant codes to help save time
In my variables.tf file, I have the following:
variable "keys_location" {
type = string
description = "keys are here"
}
I have kept the keys that are referenced in my wrkspace.vars, I have the following:
keys_location = {
"./keys/testing/certificate1.cer",
"./keys/testing/certificate2.cer",
}
And in my main.tf, I have this
resource "azurerm_virtual_network_gateway" "gw" {
name = "testing-${terraform.workspace}"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
type = "Vpn"
vpn_type = "RouteBased"
active_active = false
enable_bgp = false
sku = "VpnGw1"
ip_configuration {
name = "config"
public_ip_address_id = azurerm_public_ip.ip.id
private_ip_address_allocation = "Dynamic"
subnet_id = azurerm_subnet.subnet.id
}
test_config {
test_protocols = ["IkeV2","SSTP"]
address_space = [var.vpn_test_address_space]
dynamic "keys_location" {
for_each = var.keys_location
root_certificate {
name = "Root-Cert"
public_cert_data = file(var.keys_location)
}
}
}
When I do a terraform plan, the error I get is:
Error: Missing attribute value
on vars/wrkspace.vars line 5:
4: keys_location = {
5: "./keys/testing/certificate1.cer",
Expected an attribute value, introduced by an equals sign ("=").
How can this issue be fixed?
The Terraform for_each meta-argument operates on maps and sets, and iteration is done over either keys and values (in the case of maps) or values (in the case of sets). In order to iterate over the values in a set, one should use each.key.
In your case, because you're actually iterating over the values in the path_to_keys variable, the keys_location variable is unnecessary. You should instead reference each of the values inside the path_to_keys variable.
One possible solution would be changing your main.tf to the following:
resource "azurerm_virtual_network_gateway" "gw" {
name = "testing-${terraform.workspace}"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
type = "Vpn"
vpn_type = "RouteBased"
active_active = false
enable_bgp = false
sku = "VpnGw1"
ip_configuration {
name = "config"
public_ip_address_id = azurerm_public_ip.ip.id
private_ip_address_allocation = "Dynamic"
subnet_id = azurerm_subnet.subnet.id
}
test_config {
test_protocols = ["IkeV2","SSTP"]
address_space = [var.vpn_test_address_space]
dynamic "keys_location" {
for_each = var.path_to_keys
root_certificate {
name = "Root-Cert"
public_cert_data = file(each.key)
}
}
}
your variables.tf to:
variable "path_to_keys" {
type = set
description = "keys are here"
}
and your wrkspace.tfvars to:
path_to_keys = [
"./keys/testing/certificate1.cer",
"./keys/testing/certificate2.cer",
]
I can see two problems with what you shared, and one of them is the cause of this error.
The error itself is reporting that you've used braces { }, which are delimiters used for writing object/map values, but the content of the braces looks like you intended to define a list instead.
If you did intend to define a list then you should use square brackets [ ] to indicate that:
keys_location = [
"./keys/testing/certificate1.cer",
"./keys/testing/certificate2.cer",
]
If you actually did intend to declare a map then you'll need to choose a unique key for each element, which is what this error message is trying to tell you (because it's assuming you wanted to declare a map). I can't predict what would be good map keys so I've just used some placeholder ones for example here:
keys_location = {
cert1 = "./keys/testing/certificate1.cer",
cert2 = "./keys/testing/certificate2.cer",
}
If you make a change like I suggested above then I expect you will encounter the second error, which is that you've declared your variable as type = string but you've assigned it a list or map value. To make that work you'll need to specify a more appropriate type constraint for the variable.
If you intended to provide a list (using square brackets [ ]) then you could declare the variable as follows:
variable "keys_location" {
type = list(string)
}
If you intended to provide a map (using braces { }) then you could declare the variable as follows:
variable "keys_location" {
type = map(string)
}

Create a dynamic terraform resource

All the examples of terraform for_each and dynamic that I can find are nested within a resource. I want to create a dynamic list of resources themselves out of a 'set' input variable.
For example, the azurerm_mssql_firewall_rule is a descrete resource, so in order to have many firewall rules, I'd need something like
Parent Module:
module "AzureSqlServer" {
allowedIPs = local.azureSqlAllowedIPs
}
variable "azureSQLAllowedIPs" {
type = map(object({
name = string
ipAddress = string
}))
}
Child module:
resource "azurerm_mssql_firewall_rule" "azureSQLFirewall"{
for_each var.azureSQLAllowedIPs
name = each.value["name"]
server_id = azurerm_mssql_server.azureSqlServer.id
start_ip_address = each.value["ipAddress"]
end_ip_address = each.value["ipAddress"]
}
Is there some way to do dynamic outside the block?
Turns out I was close. Thanks to this post I was able to figure it out:
variable "azureSQLAllowedIPs" {
type = map(object({
name = string
ipAddress = string
}))
}
resource "azurerm_mssql_firewall_rule" "azureSQLFirewall" {
for_each = var.azureSQLAllowedIPs
name = each.value.name
server_id = azurerm_mssql_server.sql_server.id
start_ip_address = each.value.ipAddress
end_ip_address = each.value.ipAddress
}
(missing a "=" after the for_each and reference the map element by dot notation instead of string reference.

terraform - Iterate over two linked resources

I’m trying to write some code which would take an input structure like this:
projects = {
"project1" = {
namespaces = ["mynamespace1"]
},
"project2" = {
namespaces = ["mynamespace2", "mynamespace3"]
}
}
and provision multiple resources with for_each which would result in this:
resource "rancher2_project" "project1" {
provider = rancher2.admin
cluster_id = module.k8s_cluster.cluster_id
wait_for_cluster = true
}
resource "rancher2_project" "project2" {
provider = rancher2.admin
cluster_id = module.k8s_cluster.cluster_id
wait_for_cluster = true
}
resource "rancher2_namespace" "mynamespace1" {
provider = rancher2.admin
project_id = rancher2_project.project1.id
depends_on = [rancher2_project.project1]
}
resource "rancher2_namespace" "mynamespace2" {
provider = rancher2.admin
project_id = rancher2_project.project2.id
depends_on = [rancher2_project.project2]
}
resource "rancher2_namespace" "mynamespace3" {
provider = rancher2.admin
project_id = rancher2_project.project2.id
depends_on = [rancher2_project.project2]
}
namespaces are dependent on Projects and the generate id needs to be passed into namespace.
Is there any good way of doing this dynamically ? We might have a lot of Projects/namespaces.
Thanks for any help and advise.
The typical answer for systematically generating multiple instances of a resource based on a data structure is resource for_each. The main requirement for resource for_each is to have a map which contains one element per resource instance you want to create.
In your case it seems like you need one rancher2_project per project and then one rancher2_namespace for each pair of project and namespaces. Your current data structure is therefore already sufficient for the rancher2_project resource:
resource "rancher2_project" "example" {
for_each = var.projects
provider = rancher2.admin
cluster_id = module.k8s_cluster.cluster_id
wait_for_cluster = true
}
The above will declare two resource instances with the following addresses:
rancher2_project.example["project1"]
rancher2_project.example["project2"]
You don't currently have a map that has one element per namespace, so it will take some more work to derive a suitable value from your input data structure. A common pattern for this situation is flattening nested structures for for_each using the flatten function:
locals {
project_namespaces = flatten([
for pk, proj in var.projects : [
for nsk in proj.namespaces : {
project_key = pk
namespace_key = ns
project_id = rancher2_project.example[pk].id
}
]
])
}
resource "rancher2_namespace" "example" {
for_each = {
for obj in local.project_namespaces :
"${obj.project_key}.${obj.namespace_key}" => obj
}
provider = rancher2.admin
project_id = each.value.project_id
}
This produces a list of objects representing all of the project and namespace pairs, and then the for_each argument transforms it into a map using compound keys that include both the project and namespace keys to ensure that they will all be unique. The resulting instances will therefore have the following addresses:
rancher2_namespace.example["project1.mynamespace1"]
rancher2_namespace.example["project2.mynamespace2"]
rancher2_namespace.example["project2.mynamespace3"]
This seems to work too:
resource "rancher2_namespace" "example" {
count = length(local.project_namespaces)
provider = rancher2.admin
name = local.project_namespaces[count.index].namespace_name
project_id = local.project_namespaces[count.index].project_id
}

How to create a map in terraform

I'm confused on how to get this working, I have a sub-domian (module.foo.dev) and alternate domain name as *.foo.dev but it has to use the same zone_id as my root_domain.
I'm trying to use a local map something like
all_domains = {
["module.foo.dev","*.foo.dev"] = "foo.dev"
["bar.com"] = "bar.com"
}
My variables are as follows
primary_domain = "module.foo.dev"
sub_alternate_domain = ["*.foo.dev","bar.com"]
Eventually would be using that locals value in the below module
module:
resource "aws_route53_record" "record" {
count = var.validation_method == "DNS" ? local.all_domains : 0
name = aws_acm_certificate.certificate.domain_validation_options.0.resource_record_name
type = aws_acm_certificate.certificate.domain_validation_options.0.resource_record_type
zone_id = data.aws_route53_zone.selected[count.index].zone_id
ttl = "300"
records = [aws_acm_certificate.certificate.domain_validation_options.0.resource_record_value]
}
Can someone pls help me with this solution..
In Terraform a map can only have strings as keys (unquoted keys are still strings), so you need to swap your keys and values:
locals{
all_domains = {
"foo.dev" = ["module.foo.dev","*.foo.dev"]
"bar.com" = ["bar.com"]
}
}
Also, as above, your local variables need to be declared and assigned in a locals block.
The count argument on resources expects a whole non-negative number (0 or more) and will not accept a map as a value. You'll need to use for_each instead:
resource "aws_route53_record" "record" {
for_each = var.validation_method == "DNS" ? local.all_domains : {}
name = aws_acm_certificate.certificate.domain_validation_options.0.resource_record_name
type = aws_acm_certificate.certificate.domain_validation_options.0.resource_record_type
zone_id = data.aws_route53_zone.selected[count.index].zone_id
ttl = "300"
records = [aws_acm_certificate.certificate.domain_validation_options.0.resource_record_value]
}
The map type in the Expression Language doc provides some minimal additional guidance.

Resources