How to loop through subnets in a resource using count - terraform

In Terraform 0.11.14 , the following was possible to loop through the different subnets retrieved earlier in a data variable (cf. https://www.terraform.io/docs/providers/aws/d/subnet_ids.html ):
data "aws_subnet_ids" "private" {
vpc_id = "${var.vpc_id}"
tags = {
Tier = "Private"
}
}
resource "aws_instance" "app" {
count = "3"
ami = "${var.ami}"
instance_type = "t2.micro"
subnet_id = "${element(data.aws_subnet_ids.private.ids, count.index)}"
}
However, since I migrated to Terreform 0.12, this syntax results in the following error:
Error: Error in function call
on ..\..\modules\elk\es-proxy-server.tf line 21, in resource "aws_spot_instance_request" "kibana_proxy":
21: subnet_id = "${element(data.aws_subnet_ids.private.ids, count.index)}"
|----------------
| count.index is 0
| data.aws_subnet_ids.private.ids is set of string with 2 elements
Call to function "element" failed: cannot read elements from set of string.
I tried to use the tolist function and to work out how to take benefit of the following https://www.terraform.io/upgrade-guides/0-12.html#working-with-count-on-resources without any success.

You should be able to do:
subnet_id = "${tolist(data.aws_subnet_ids.private.ids)[count.index]}"

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

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

Referencing outputs from a for_each module

I have a module which has a variable defined using for_each, and its output is as below:
output "nic_ids" {
value = [for x in azurerm_network_interface.nic : x.id]
}
nic_ids = [
"/subscriptions/*****/resourceGroups/test-rg/providers/Microsoft.Network/networkInterfaces/test-nic-1",
"/subscriptions/*****/resourceGroups/test-rg/providers/Microsoft.Network/networkInterfaces/test-nic-2",
]
My aim is to pass above NIC ids to the VM module and have 1:1 mapping between NIC id and VM (test-nic-1 should only be attached to vm-1, test-nic-2 to vm-2 etc.)
module "vm" {
source = "*****/vm/azurerm"
version = "0.1.0"
vms = var.vms
nic_ids = module.nic[each.value.id].nic_ids
}
I am getting below error:
Error: each.value cannot be used in this context
on main.tf line 58, in module "vm":
58: nic_ids = module.nic[each.value.id].nic_ids
A reference to "each.value" has been used in a context in which it
unavailable, such as when the configuration no longer contains the value in
its "for_each" expression. Remove this reference to each.value in your
configuration to work around this error.
I used this similar question as reference.
Can you please suggest?
You could pass the above NIC id list to the VM modules with the count.
module "vm" {
source = "./modules/vms"
vm_names = ["aaa","bbb"]
nic_ids = module.nic.network_interface_nics
}
module "nic" {
source = "./modules/nic"
nic_names = ["nic1","nic2"]
}
the main.tf in the VM module:
resource "azurerm_virtual_machine" "vm-windows" {
count = length(var.vm_names)
name = var.vm_names[count.index]
resource_group_name = data.azurerm_resource_group.vm.name
location = var.location
vm_size = var.vm_size
network_interface_ids = [ var.nic_ids[count.index] ]
...
}
The output.tf in the NIC module:
output "network_interface_nics" {
description = "ids of the vm nics provisoned."
value = [for x in azurerm_network_interface.nic : x.id]
}

How do you pass Terraform count from a resource to a module?

How can I pass a list of values created from a resource to a module that expects a list of items? I've tried running [count.index] and reffering to the first item in the list [0] but I get the message that you can only pass a count object to resource & data blocks.
resource "aws_subnet" "private_subnets" {
count = length(data.aws_availability_zones.available.names)
vpc_id = data.aws_vpc.selected.id
cidr_block = "192.168.${10 + count.index}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = false
tags = {
Name = "private-subnet"
}
module "my_module" {
source = "../../"
cluster_name = local.cluster_name
subnets = aws_subnet.public_subnets[count.index].id
Terraform version 0.12+ has all the goodies!
I updated my call to the resource to loop back like the following:
[ for subnet in aws_subnet.public_subnets: subnet.id ]

Terraform on Azure - Deploy multiple subnet

I'm trying to implement a Terraform script to create multiple subnets.
resource "azurerm_subnet" "test_subnet" {
name = "testSUBNET"
resource_group_name = "${local.resource_group_name}"
virtual_network_name = "${azurerm_virtual_network.lab_vnet.name}"
address_prefix = "10.0.1.0/24"
}
Is there a way to do a for-each or a loop on a variable in order to create them at the same time?
You can achieve this using a variable and count index as follows:
variable "subnet_prefix" {
type = "list"
default = [
{
ip = "10.0.1.0/24"
name = "subnet-1"
},
{
ip = "10.0.2.0/24"
name = "subnet-2"
}
]
}
resource "azurerm_subnet" "test_subnet" {
name = "${lookup(element(var.subnet_prefix, count.index), "name")}"
count = "${length(var.subnet_prefix)}"
resource_group_name = "${local.resource_group_name}"
virtual_network_name = "${azurerm_virtual_network.lab_vnet.name}"
address_prefix = "${lookup(element(var.subnet_prefix, count.index), "ip")}"
}
There is also preview feature available for-each in the new version
If you are using Terraform 12 this can be achieved using the for-each capability or the count capability
count should be used if you are looking to create almost identical resources.
for-each should be used to create multiple of each instance based on a different map or set of values.
Using an list of strings and the toset() function to convert this is a neat way to achieve this
variable "subnet_ids" {
type = list(string)
}
resource "aws_instance" "server" {
for_each = toset(var.subnet_ids)
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
subnet_id = each.key # note: each.key and each.value are the same for a set
tags = {
Name = "Server ${each.key}"
}
}
Or you could achieve this by using something like the below:
resource "azurerm_resource_group" "rg" {
for_each = {
a_group = "eastus"
another_group = "westus2"
}
name = each.key
location = each.value
}
If you are looking to achieve this with Terraform 11 the count and variable capabilities are the only way other than code duplication. (Rajat Arora has mentioned)
I would strongly recommended using Terraform 12 as the providers for Terraform 11 will be unsupported in the not to far future and if you can save yourself from refactoring now, you should!

Resources