Variable in terraform resource referance - azure

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

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

Using terraform, how to create multiple resources of same type with unique and unidentical names using list/count for azure?

Here is a basic example for what I am trying to achieve. I have two files (main.tf) and (variable.tf), I want to create two resource groups and in the variables file is a list of names which I want the resource groups to occupy. First name of the first resource group and similarly going forward.
So help me out on how to achieve it. I am using terraform v0.13.
main.tf file:
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "test" {
count = 2
name = var.resource_group_name
location = var.location
}
variable.tf file:
variable "resource_group_name" {
description = "Default resource group name that the network will be created in."
type = list
default = ["asd-rg","asd2-rg"]
}
variable "location" {
description = "The location/region where the core network will be created.
default = "westus"
}
You just need to change the resource group block like this:
resource "azurerm_resource_group" "test" {
count = 2
name = element(var.resource_group_name, count.index)
location = var.location
}
You can use the for_each syntax to create multiple resource of similar type. It requires a set (of unique values) to iterate over, hence convert your variable resource_group_name to set.
variable.tf
variable "resource_group_name" {
description = "Default resource group name that the network will be created in."
type = list(string)
default = ["asd-rg","asd2-rg"]
}
variable "location" {
description = "The location/region where the core network will be created.
default = "westus"
}
main.tf
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "test" {
name = each.value // value from iteration
location = var.location
for_each = toset(var.resource_group_name) // convert list to set and iterate over it
}
Edit: variable can be of type string, list or map. This needs to be converted to set to be used with for_each
for you can use the combination of length and count to create multiple resources of the same type with unique and unidentical names.
You just need to change the resource group block like this:
resource "azurerm_resource_group" "test" {
count = length(var.resource_group_name)
name = element(concat(var.resource_group_name, [""]), count.index)
location = var.location
}

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!

Iterate over nested data with for / for_each at resource level

I am trying to work out how to iterate over nested variables from a complex object given in the following tfvars file using Terraform 0.12.10:
example.tfvars
virtual_network_data = {
1 = {
product_instance_id = 1
location = "somewhere"
address_space = ["192.168.0.0/23"]
dns_servers = []
custom_tags = {"test":"test value"}
subnets = [
{
purpose = "mgmt"
newbits = 4
item = 0
},
{
purpose = "transit"
newbits = 4
item = 1
}
]
}
}
example.tf
variable "virtual_network_data" {} #Data comes from example.tfvars
variable "resource_group_name" {
default = "my_resource_group"
}
variable "virtual_network_name" {
default = "my_virtual_network"
}
####
resource "azurerm_subnet" "pool" {
for_each = var.virtual_network_data
name = format("%s%s%02d", "subnet_", s.purpose, s.item)
resource_group_name = var.resource_group_name
virtual_network_name = var.virtual_network_name
address_prefix = cidrsubnet(each.value["address_space"], s.newbits, s.item)
}
In example.tf I can use each.value["address_space"] to get to the top level variables, but I can't work out how to get to the items in subnets (s.purpose, s.item & s.newbits).
I have used dynamic blocks, as part of a parent resource (below), which works but in this case, I need to move the subnet into its own resource. Simply put, how do I get the first for_each to behave like the second for_each in the dynamic block?
resource "azurerm_virtual_network" "pool" {
for_each = var.virtual_network_data
name = format("%s%02d", local.resource_name, each.key)
resource_group_name = var.resource_group_name
location = each.value["location"]
address_space = each.value["address_space"]
dns_servers = each.value["dns_servers"]
tags = merge(local.tags, each.value["custom_tags"])
dynamic "subnet" {
for_each = [for s in each.value["subnets"]: {
name = format("%s%s%02d", "subnet_", s.purpose, s.item)
prefix = cidrsubnet(element(each.value["address_space"],0), s.newbits, s.item)
}]
content {
name = subnet.value.name
address_prefix = subnet.value.prefix
}
}
}
Cheeky bonus, is there a way to replace s.item with something like each.key or count.index?
TIA
The technique in this situation is to use other Terraform language features to transform your collection to be a suitable shape for the for_each argument: one element per resource instance.
For nested data structures, you can use flatten in conjunction with two or more for expressions to produce a flat data structure with one element per nested object:
locals {
network_subnets = flatten([
for network_key, network in var.virtual_network_data : [
for subnet in network.subnets : {
network_key = network_key
purpose = subnet.purpose
parent_cidr_block = network.address_space[0]
newbits = subnet.newbits
item = subnet.item
}
]
])
}
Then you can use local.network_subnets as the basis for repetition:
resource "azurerm_subnet" "pool" {
# Each instance must have a unique key, so we'll construct one
# by combining the network key, the subnet "purpose", and the "item".
for_each = {
for ns in local.network_subnets : "${ns.network_key}.${ns.purpose}${ns.item}" => ns
}
name = format("%s%s%02d", "subnet_", each.value.purpose, each.value.item)
resource_group_name = var.resource_group_name
virtual_network_name = var.virtual_network_name
address_prefix = cidrsubnet(each.value.parent_cidr_block, each.value.newbits, each.value.item)
}
There's a similar example in the flatten documentation, as some additional context.
As alternative to flatten trick, you may for_each resource by first parameter inside nested module, then for_each this module by second parameter.

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

Resources