for_each throws an error after it has been "moved" - terraform

So, i have used a for_each in each resource earlier.
And now I have moved that to the module.
module "architect" {
source = "./modules/architect"
for_each = toset(var.vm_names)
vm_name = each.value
vm_key = each.key
}
I have also defined up the vm_name and vm_key in /modules/architect/variables.tf:
variable "vm_name" {
type = string
}
variable "vm_key" {
type = string
}
I`m trying to set a public IP address on each VM.
And this worked fine when I had the for_each(commented out) in each resource.
resource "azurerm_public_ip" "pubip" {
#for_each = toset(var.vm_names)
name = "${var.vm_name}-PublicIp"
resource_group_name = azurerm_resource_group.rsg.name
location = azurerm_resource_group.rsg.location
allocation_method = "Dynamic"
}
resource "azurerm_network_interface" "main" {
#for_each = toset(var.vm_names)
name = "${var.vm_name}-nic"
location = azurerm_resource_group.rsg.location
resource_group_name = azurerm_resource_group.rsg.name
ip_configuration {
name = "testconfiguration1"
subnet_id = azurerm_subnet.subnet.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.pubip[var.vm_key].id
}
The error it throws here is:
Error: Invalid index
on modules\architect\main.tf line 109, in resource "azurerm_network_interface" "main":
109: public_ip_address_id = azurerm_public_ip.pubip[var.vm_key].id
|----------------
| azurerm_public_ip.pubip is object with 16 attributes
| var.vm_key is "OSL-SPLK-HF01"
The given key does not identify an element in this collection value.
Here is the block for the 109 line, where the last line is the number 109:
ip_configuration {
name = "testconfiguration1"
subnet_id = azurerm_subnet.subnet.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.pubip[var.vm_key].id
What is the reason I get this error?
I have used each.key there before.

When you have a resource block without for_each set at all, referring to it in other expressions returns just a single object rather than a map of objects.
For that reason, once you factor out the for_each into the calling module block rather than each individual resource it's no longer correct to try to access individual instances of your resources, as you did in the expression azurerm_public_ip.pubip[var.vm_key].id. Instead, you should just refer directly to the single object that resource now represents:
public_ip_address_id = azurerm_public_ip.pubip.id
Notice that within the namespace of each particular module instance there is only one azurerm_public_ip.pubip, so you can just refer directly to it. Because the for_each is now on the module call itself rather than on the individual resources, Terraform will assign these resources addresses shaped like this:
module.architect["OSL-SPLK-HF01"].azurerm_public_ip.pubip
...whereas when you had the resources in the root with their own for_each arguments, Terraform would address them like this:
module.architect.azurerm_public_ip.pubip["OSL-SPLK-HF01"]
The index now exists in the calling module rather than in the called module, so the architect module itself has no awareness that it's being used with for_each, but any references to the outputs of that module in the caller will need to include a key like you were previously doing with the resource instances:
# the "foo" output value associated with the "OSL-SPLK-HF01"
# instance of the module.
module.architect["OSL-SPLK-HF01"].foo

Related

Azure/Terraform:Link subnets to NSGs(ERROR-for_each map includes keys derived from resource attributes that cannot be determined until apply)

Locked for 3 days. Comments on this question have been disabled, but it is still accepting new answers and other interactions. Learn more.
Objective:Link multiple subnets in the environment to corresponding NSGs using a module (NSGs and Subnets have been created using separate modules)
Root Module:
1.main.tf
resource "azurerm_subnet_network_security_group_association" "root_subnet_nsg_association" {
subnet_id = var.subnet_id
network_security_group_id = var.nsg_id
}
2.variables.tf
variable "subnet_id"{
type=number
description="ID of the subnet which is to be attached to NSG"
#default=""
}
variable "nsg_id"{
type=number
description="ID of the NSG which is to be associated with a subnet"
#default=""
}
Calling Module in Projects Folder:
(for_each used to iterate the module)
1.nsg_subnet_association.tf
module "nsg_subnet_asosciation_module"{
source="../../Modules/network/nsg_subnet_association"
#Variable names to be passed into the root module:
#Use for_each to loop the module:
#for_each accepts a set or map but not list as a value
for_each = local.nsg_subnet_association
subnet_id=each.key
nsg_id=each.value
}
2.locals block passing in values to the calling module:
NOTE:It is possible to have dynamic keys in the map using parenthesis ()
locals{ //Key in subnet name and NSG name for each element of the LIST
//Implicit dependence on Subnet and NSG being created before attempt to associate
#It is possible to have dynamic keys using parenthesis () as seen on left below
nsg_subnet_association={
(module.subnet_module["MGT-Subnet-1"].subnet_id)= module.nsg_module["HUB-NSG"].nsg_id
(module.subnet_module["MGT-Subnet-1"].subnet_id) = module.nsg_module["MGT-NSG"].nsg_id
(module.subnet_module["SEC-Subnet-1"].subnet_id) = module.nsg_module["SEC-NSG"].nsg_id
}
}
This ends up with the following error:
The "for_each" map includes keys derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys that will identify the instances of this resource.
When working with unknown values in for_each, it's better to define the map keys statically in your configuration and place apply-time results only in the map values.
Alternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge.
In Terraform , when dynamically getting the value of vnets or subnets , it may take time to create and the rest of the dependent resources cannot get desired values and so this error occurs.
Error:
The "for_each" map includes keys derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys that will identify the instances of this resource.
Use a code where the values are defined statically to resolve the error:
example:
Code:
Variables.tf:
variable "virtual_network_name" {
type = string
default = "my-virtual-network"
}
variable "subnet_address_prefixes" {
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24"]
}
variable "subnet_names" {
type = set(string)
default = ["subnet1", "subnet2"]
}
variable "nsg_names" {
type = set(string)
default = ["nsg1", "nsg2"]
}
variable "subnet_nsg_mappings" {
type = map(string)
default = {
"subnet1" = "nsg1"
"subnet2" = "nsg2"
}
}
Main.tf
resource "azurerm_virtual_network" "virtual_network" {
name = var.virtual_network_name
address_space = ["10.0.0.0/16"]
location = data.azurerm_resource_group.example.location
resource_group_name = data.azurerm_resource_group.example.name
}
resource "azurerm_network_security_group" "nsg" {
for_each = toset(var.nsg_names)
name = each.value
location = data.azurerm_resource_group.example.location
resource_group_name = data.azurerm_resource_group.example.name
security_rule {
name = "allow_http"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
resource "azurerm_subnet" "subnet" {
for_each = toset(var.subnet_names)
name = each.value
virtual_network_name = azurerm_virtual_network.virtual_network.name
address_prefixes = var.subnet_address_prefixes
resource_group_name = data.azurerm_resource_group.example.name
// enable_multiple_address_prefixes = true
}
# Associate each subnet with its corresponding NSG
resource "azurerm_subnet_network_security_group_association" "subnet_nsg" {
for_each = var.subnet_nsg_mappings
subnet_id = azurerm_subnet.subnet[each.key].id
network_security_group_id = azurerm_network_security_group.nsg[each.value].id
//subnet_id = azurerm_subnet.subnet[each.key].id
// network_security_group_id = azurerm_network_security_group.nsg[var.subnet_nsg_mappings[each.value]].id
}
Or
Define locals for mappings .
locals {
subnet_nsg_mappings = {
"subnet1" = "nsg1",
"subnet2" = "nsg2",
"subnet3" = "nsg3"
}
}
resource "azurerm_subnet_network_security_group_association" "subnet_nsg" {
for_each = toset(var.subnet_names)
subnet_id = azurerm_subnet.subnet[each.value].id
network_security_group_id = azurerm_network_security_group.nsg[local.subnet_nsg_mappings[each.value]].id
}
If dynamic values must be used for_each keys cannot be determined during apply time. In that case use the -target option to first apply vnet and subnet values i.e; the resources that the for_each value depends on and apply completely.
terraform apply -target="azurerm_virtual_network.virtual_network" -target="azurerm_subnet.subnet"
Reference:
azurerm_subnet_network_security_group_association | Resources | hashicorp/azurerm | Terraform Registry

How to pass certain attributes from resources that were created with for_each loop to other resources in Terraform?

I have been trying to figure out what would be the most ideal option to deploy some fundamental, mostly identical resources (vnet, subnet, bastion host, nsg, etc.) resources in Azure, using Terraform.
I have tried it with for_each and it was working just fine until I have faced a problem where I had to pass a value to an attribute from a resource which was created with for_each. Let me show you:
So this is obviously working, nothing wrong with the following resources:
resource "azurerm_subnet" "AzureBastionSubnet" {
for_each = var.bastion_subnet
name = each.value["name"]
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet[each.key].name
address_prefixes = each.value["address_prefixes"]
depends_on = [azurerm_virtual_network.vnet]
}
resource "azurerm_public_ip" "bastion_public_ip" {
for_each = toset(var.public_ip_location)
name = "bastion-public-ip-${each.value}"
location = each.value
resource_group_name = azurerm_resource_group.rg.name
allocation_method = "Static"
sku = "Standard"
depends_on = [azurerm_subnet.AzureBastionSubnet]
}
But the problem starts now when in the following resource I need to pass attribute values from resources which were created with for_each. How on earth do I pass the right attributes from the created bastion subnets and public IPs to the subnet_id and public_ip_address_id?
resource "azurerm_bastion_host" "bastion" {
for_each = toset(var.location_list)
name = "bastion-${each.value}"
location = each.value
resource_group_name = azurerm_resource_group.rg.name
ip_configuration {
name = "configuration"
subnet_id = azurerm_subnet.AzureBastionSubnet.id
public_ip_address_id = azurerm_public_ip.bastion_public_ip.id
}
depends_on = [azurerm_public_ip.bastion_public_ip]
}
Thanks!
I was looking into Terraform's lookup, and also the for loop and I am sure they could make it work but I just cannot seem to figure it out.
You might be creating multiple items of azurerm_subnet.AzureBastionSubnet as you are using for_each here
resource "azurerm_subnet" "AzureBastionSubnet" {
for_each = var.bastion_subnet
name = each.value["name"]
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet[each.key].name
address_prefixes = each.value["address_prefixes"]
depends_on = [azurerm_virtual_network.vnet]
}
So you may want to refer to your individual azurerm_subnet instance by passing your var.bastion_subnet set member, or its map key.
for example:
resource "azurerm_bastion_host" "bastion" {
..
ip_configuration {
...
subnet_id = azurerm_subnet.AzureBastionSubnet["subnet-1"].id
}
depends_on = [azurerm_public_ip.bastion_public_ip]
}
Where subnet-1 is a key in my var.bastion_subnet map.
From Terraform documentation:
Referring to Instances
When for_each is set, Terraform distinguishes between the block
itself and the multiple resource or module instances associated with
it. Instances are identified by a map key (or set member) from the
value provided to for_each.
<TYPE>.<NAME> or module.<NAME> (for example,
azurerm_resource_group.rg) refers to the block. <TYPE>.<NAME>[<KEY>]
or module.<NAME>[<KEY>] (for example,
azurerm_resource_group.rg["a_group"],
azurerm_resource_group.rg["another_group"], etc.) refers to individual
instances.

Terraform for_each?

I have been working on this terraform azure vm template and the goal is use for_each to make the module more dynamic in nature but i'm not able to figure out how to reference one resource_id in another resource block.
If you see in first resource block i'm creating NICs using "for_each" and i want to reference the "network_interface_id" in second resource which is associating Network Interface to outbound load balancer. Not sure how to do that but started to construct the string using variables. can anyone help me on how to reference the "network_interface_id" or any other resource id if required, any help is highly appreciated.
###################
# Network Interface
###################
resource "azurerm_network_interface" "this" {
for_each = var.vm_details
name = format(
"%s-${var.location}-%s-%s-nic-%s",
var.app_acronym,
var.env,
var.app_purpose,
each.value.vm_identifier
)
location = var.location
resource_group_name = var.resource_group_name
tags = var.tags
ip_configuration {
name = format(
"%s-${var.location}-%s-%s-ip-%s",
var.app_acronym,
var.env,
var.app_purpose,
each.value.vm_identifier
)
subnet_id = var.subnet_id
private_ip_address_allocation = var.private_ip_address_allocation != "" ? var.private_ip_address_allocation : "Dynamic"
}
enable_accelerated_networking = each.value.enable_accelerated_networking
}
###########################################################
# Asssociating Network Interface to outbound load balancer
###########################################################
resource "azurerm_network_interface_backend_address_pool_association" "this" {
for_each = var.olb_association
network_interface_id = "${var.rsrc_id_str_1}${var.subscription_id}${var.rsrc_id_str_2}${var.resource_group_name}${var.rsrc_id_str_nic_3}${var.app_acronym}${var.hifen}${var.location}${var.hifen}${var.env}${var.hifen}${var.app_purpose}${var.nic_abbrv}${each.value.vm_identifier}"
ip_configuration_name = format(
"%s-${var.location}-%s-%s-ip-%s",
var.app_acronym,
var.env,
var.app_purpose,
each.value.vm_identifier
)
backend_address_pool_id = each.value.backend_address_pool_id
depends_on = [azurerm_network_interface.this]
}
for_each creates a data structure that is referenced like the map you feed it. So if the name for an entry is "puppy", you would reference it as azurerm_network_interface.this["puppy"]

How to interpolate COUNT in Terraform?

I am creating a number of VMs in Azure with corrosponding NICs and PublicIPs.
I can create unique names for the VMs no problem:
resource "azurerm_virtual_machine" "workernode" {
count = "${var.nodeCount}"
name = "workernode-${count.index +1}"
and the public IPs:
resource "azurerm_public_ip" "AliasworkerPubIP" {
count = "${var.nodeCount}"
name = "workerpubip${count.index +1}"
and the NIC:
resource "azurerm_network_interface" "workerNIC" {
count = "${var.nodeCount}"
name = "workerNIC.${count.index +1}"
but I cant work out how to get it to work for then connecting the NIC to the PublicIP just created ...
Have tried various different ways and nothing is clicking ... I know I missing something or not understanding interpolation parsing correctly, but what?!
examples I have tried:
public_ip_address_id = "${azurerm_public_ip}.${format("Alias_WorkerIP%d.id", count.index +1)}"
public_ip_address_id = "${format("Alias_WorkerIP%d.id", count.index +1)}"
public_ip_address_id = "${format("azurerm_public_ip.workerpubip.%s.id", count.index +1)}"
any ideas of where I am going wrong?
The current recommended way to express this is:
public_ip_address_id = "${azurerm_public_ip.workerpubip.*.id[count.index]}"
Using this index operator ([ ... ]) allows Terraform to understand better the dependency this implies, so that if only one of the public IP instances needs to be replaced it can understand that only the one corresponding azurerm_network_interface needs to be updated.
When using the element function Terraform only "sees" the azurerm_public_ip.workerpubip.*.id expression and assumes, conservatively, that there is a dependency on all of the azurerm_public_ip ids.
This is how I solved it:
public_ip_address_id = "${element(azurerm_public_ip.workerpubip.*.id,count.index)}"
try this in azurerm_network_interface module
public_ip_address_id = "${element(azurerm_public_ip.main-rg__vm-ip.*.id, count.index)}"
in azurerm_public_ip module
resource azurerm_public_ip main-rg__vm-ip {
count = "${var.vm_count}"
name = "${var.environment}-vm${count.index+1}-ip"
location = "${var.location}"
resource_group_name = "${azurerm_resource_group.main-rg.name}"
sku = "Basic"
allocation_method = "Dynamic"
}

Accessing the output from module via index

I am trying to create 2 VMs on Azure using Terraform.
I create 2 NICs like
variable "internalips" {
description = "List of Internal IPs"
default = ["10.0.2.10", "10.0.2.11"]
type = "list"
}
resource "azurerm_network_interface" "helloterraformnic" {
count = 2
name = "nic-${count.index}"
location = "West US"
resource_group_name = "myrg"
ip_configuration {
name = "testconfiguration1"
subnet_id = "${azurerm_subnet.helloterraformsubnet.id}"
private_ip_address_allocation = "static"
private_ip_address = "${element(private_ip_address, count.index)}"
}
}
Now I want to use them in module azurerm_virtual_machine
resource "azurerm_virtual_machine" "helloterraformvm" {
count = 2
name = "${element(elasticmachines, count.index)}"
location = "West US"
resource_group_name = "myrg"
network_interface_ids = "${element(azurerm_network_interface.helloterraformnic, count.index)}"
....
}
This gives me an error
Failed to load root config module: Error loading azure/rg.tf: Error
reading config for azurerm_virtual_machine[helloterraformvm]:
azurerm_network_interface.helloterraformnic: resource variables must
be three parts: TYPE.NAME.ATTR in:
${element(azurerm_network_interface.helloterraformnic, count.index)}
How can I use the above created NICs using index ?
First thinking to use length function to get the counts more than hard coding it.
from
count = 2
change to
count = "${length(var.internalips)}"
For your problem, you need to tell the resource which attribute you want to get the value.
network_interface_ids = "${element(azurerm_network_interface.helloterraformnic.id, count.index)}"
Refer:
terraform Interpolation Syntax
terraform azurerm_virtual_machine Attributes Reference

Resources