Terraform - Invalid index error with multiple count - terraform

I am trying to configure 2 Public IP address, with 2 Network interface. What I have written so far is :
resource "azurerm_public_ip" "example" {
name = "test-pip${count.index}"
count = 2
location = "${azurerm_resource_group.rc.location}"
resource_group_name = "${azurerm_resource_group.rc.name}"
allocation_method = "Dynamic"
idle_timeout_in_minutes = 30
}
output "public_ip_address" {
value = "${azurerm_public_ip.example.*.id}"
}
resource "azurerm_network_interface" "main" {
name = "test${count.index}"
count = 2
location = "${azurerm_resource_group.rc.location}"
resource_group_name = "${azurerm_resource_group.rc.name}"
ip_configuration {
name = "testconfiguration1${count.index}"
subnet_id = "${azurerm_subnet.internal.id}"
private_ip_address_allocation = "Dynamic"
public_ip_address_id = "${azurerm_public_ip.example[count.index].id}"
}
}
Later I will use these two IP, and NI to assign them to 2 VM machines.
When I run terraform plan , I get an error saying :
Terraform version is "v0.12.3" and azure provider version is "v1.40.0"

Actually, I think there is nothing wrong in the Terraform code that you provided in the question and all things work fine on my side.
The error also said:
The given key does not identity an element this collection value.
It probably because that your public IPs do not created before the network interface. It's strange. Terraform will sequence all the resources in the right sequence. Maybe you can try to upgrade the Terraform version. What I used is the newest version:
Terraform v0.12.19
+ provider.azurerm v1.41.0
Or you can try to change the code like this:
public_ip_address_id = "${element(azurerm_public_ip.example.*.id, count.index)}"

Given that this is Terraform 0.12 and not Terraform 0.11 as the question syntax implies, the actual error is in the specific exported attribute. To access the ip address exported by the azurerm_public_ip.example resource, we would need to use the ip_address exported attribute and not the id. This is why the error is being thrown for an invalid key, although the specific reference in the error is indeed misleading.
We can update your code to fix this by:
ip_configuration {
name = "testconfiguration1${count.index}"
subnet_id = "${azurerm_subnet.internal.id}"
private_ip_address_allocation = "Dynamic"
public_ip_address_id = "${azurerm_public_ip.example[count.index].ip_address}"
}

you can try to change the code like this,It works for me:
public_ip_address_id = "${element(azurerm_public_ip.example.*.id, count.index)}"

Related

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.

Referencing properties with for_each in modules and count in the resource

Here is my shortened code:
variable.tf:
variable "vm_avd" {
type = map(any)
default = {
avd-std = {
vm_count = "2"
### Interface Vars
subnet_id = "AD"
}
avd-gpu = {
vm_count = "2"
### Interface Vars
subnet_id = "AD"
}
}
}
variable "vm_server" {
type = map(any)
default = {
srv-std = {
vm_count = "2"
### Interface Vars
subnet_id = "AD"
}
}
}
if.tf:
resource "azurerm_network_interface" "if" {
count = var.vm_count
name = var.private_ip_address_allocation == "Static" ? "if_${var.if_name}_${replace(var.private_ip_address, ".", "")}_${format("%02d", count.index)}" : "if_${var.if_name}_${format("%02d", count.index)}"
location = var.location
resource_group_name = var.resource_group_name
ip_configuration {
name = "if_ipconfig"
subnet_id = var.subnet_id
private_ip_address_version = var.private_ip_address_version
private_ip_address_allocation = var.private_ip_address_allocation
private_ip_address = var.private_ip_address
}
}
output "if_name" {
value = azurerm_network_interface.if[*].name
}
output "if_id" {
value = azurerm_network_interface.if[*].id
}
main.tf:
module "vm" {
for_each = merge(var.vm_server, var.vm_avd)
vm_name = each.key
vm_count = each.value["vm_count"]
network_interface_ids = module.if[each.key].if_id
}
module "if" {
for_each = merge(var.vm_server, var.vm_avd)
vm_count = each.value["vm_count"]
if_name = each.key
subnet_id = module.snet[each.value["subnet_id"]].snet_id
}
Is there a possibility to reference in a for_each loop the correct count object for dependent resources?
I'm creating Terraform modules for Azure and I want to build a structure that is highly flexible and easy to fill for everyone.
Therefore I build a root file with a single module call for each resource and I have a single main variables file which should be filled with values which are looped through.
All related resource variables are defined in the same variables object, so for a VM a variable VM is created and filled with VM details, NIC details and extension details. The single modules run through the same variables for VM, NIC and extensions which is, in general, running fine.
I can reference the subnet_id for the network interfaces nicely but as soon as I use a count operator in the resource I don't know how the correctly get the network interface ID of the correct interface for the correct VM.
Now I ran into the problem that I use for_each in my module call and I want to have the option to use count in my resource definition too. It is working but when I build e.g. virtual machines, I get the problem that I cannot reference to the correct network interfaces.
Atm all my interfaces are connected to the same VM and the 2nd VM cannot be build. Same goes for extensions and any possible multiple objects tho.
I tried several module calls but the first VM got all the interfaces every time. I tried with the Star character in the NIC module call or with the network interface ID.
If this is not possible at all, I thought of a solution with building the network interface ID itself with its original form.
I also checked if there is a possibility to build an array with the for_each and count elements but I couldn't find any way to build arrays out of number variables in terraform, like the normal "for 1 to 5 do".
If you know of any way to do this, I would be grateful too.
Maybe it is a typo when you try to simplify the question, but in your variables you have:
variable vm_avd { default = { avd-std = {...}} }
variable vm_server { default = { avd-std = {...}} }
in your main tf you have:
merge(var.vm_server, var.vm_avd)
since the key is the same, one will override the other, if you are using the default value, it might result in the behaviour you are suggesting, consider changing the key name to server-std?
I got the solution with simply providing the whole element array, e.g. from VM, to the variable for the ID and the reference the correct VM with the count index:
virtual_machine_id = var.vm_id[count.index]
As i run through the same Count in other ressources, I get the correct order of the elements for referencing :)

for_each throws an error after it has been "moved"

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

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