How to interpolate COUNT in Terraform? - azure

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"
}

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.

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

Terraform - Invalid index error with multiple count

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

Variable in terraform resource referance

I'm trying create a resource in terraform that will create a number of subnets based on a list variable.
I'm having trouble with references to existing resources. For example in the following code network_security_group_id is hardcoded to azurerm_network_security_group.k8s.id:
variable "resources_large" {
description = "List of Large Networks"
default = [
"k8s",
"storm"
]
}
resource "azurerm_subnet" "large" {
name = "ue-${var.environment}-${var.resources_large[count.index]}-subnet-${replace("${cidrsubnet("${local.subnet_ranges["large"]}", "${var.newbit_size["large"] }", count.index )}", "/[./]/", "-" ) }"
resource_group_name = "ue-${var.environment}-${var.resources_large[count.index]}-rg"
virtual_network_name = "${azurerm_virtual_network.dev.name}"
address_prefix = "${cidrsubnet("${local.subnet_ranges["large"]}", "${var.newbit_size["large"] }", count.index )}"
network_security_group_id = "${azurerm_network_security_group.k8s.id}"
count = "${length(var.resources_large)}"
depends_on = ["azurerm_virtual_network.dev"]
}
This needs to reference existing security groups based on the name in the resources_large list.
What I'd like to have is something which looks likes this:
network_security_group_id = "${azurerm_network_security_group.${var.resources_large[count.index]}.id}"
Which doesn't work, I'm guessing due to the lack of variable interpolation support.
Is there any way to reference other resources based on variable?
Maybe something like this
locals {
sgs = {
k8s = "${azurerm_network_security_group.k8s.id}"
storm = "${azurerm_network_security_group.storm.id}"
}
}
...
network_security_group_id = "${lookup( locals.sgs, var.resources_large[count.index])}"
may work.
If you create the SG using the same counter, it can be just
network_security_group_id = "${element(azurerm_network_security_group.*.id, count.index)}"
HTH

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