Terraform on Azure - Deploy multiple subnet - azure

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!

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.

Azure Databricks workspace using terraform

Trying to create Databricks workspace using terraform but unsupported arguments:
resource "azurerm_databricks_workspace" "workspace" {
name = "testdata"
resource_group_name = "cloud-terraform"
location = "east us"
sku = "premium"
virtual_network_id = azurerm_virtual_network.vnet.id
public_subnet_name = "databrickpublicsubnet"
public_subnet_cidr = "10.0.0.0/22"
private_subnet_name = "databrickprivatesubnet"
private_subnet_cidr = "10.0.0.0/22"
tags = {
Environment = "terraformtest"
}
}
Error: An argument named "virtual_network_id" is not expected here. An argument named "public_subnet_name" is not expected here. An argument named "public_subnet_cidr" is not expected here.
I haven't tried to set up databricks via Terraform, but I believe (per the docs) you need add those properties in a block:
resource "azurerm_databricks_workspace" "workspace" {
name = "testdata"
resource_group_name = "cloud-terraform"
location = "east us"
sku = "premium"
custom_parameters {
virtual_network_id = azurerm_virtual_network.vnet.id
public_subnet_name = "databrickpublicsubnet"
private_subnet_name = "databrickprivatesubnet"
}
tags = {
Environment = "terraformtest"
}
}
The two cidr entries aren't part of the TF documentation.
true. you can add terraform commands to create the subnets (assuming vnet already exists, you can use data azurerm_virtual_network then create the two new subnets, then reference the names of the two new public/private subnets.
Then you run into what seems to be a chicken/egg issue though.
You get Error: you must define a value for 'public_subnet_network_security_group_association_id' if 'public_subnet_name' is set.
Problem is, the network security group is typically auto-generated on creation of the databrick workspace (like databricksnsgrandomstring), which works when creating it in the portal, but via terraform, I have to define it to create the workspace, but it doesn't yet exist until I create the workspace. The fix is to not let it generate it's own nsg name, but name it yourself with an nsg resource block.
below is code I use (dbname means databricks name!). here I'm
adding to an existing resource group 'qa' and existing vnet as well, only showing the public subnet and nsg association, you can easily add the private ones). just copy/modify in your own tf file(s). and you'll definitely need to change the address_prefix to your own CIDR values that works within your vnet and not stomp on existing subnets within.
resource "azurerm_subnet" "public" {
name = "${var.dbname}-public-subnet"
resource_group_name = data.azurerm_resource_group.qa.name
virtual_network_name = data.azurerm_virtual_network.vnet.name
address_prefixes = ["1.2.3.4/24"]
delegation {
name = "databricks_public"
service_delegation {
name = "Microsoft.Databricks/workspaces"
}
}
}
resource "azurerm_network_security_group" "nsg" {
name = "${var.dbname}-qa-databricks-nsg"
resource_group_name = data.azurerm_resource_group.qa.name
location= data.azurerm_resource_group.qa.location
}
resource "azurerm_subnet_network_security_group_association" "nsga_public" {
network_security_group_id = azurerm_network_security_group.nsg.id
subnet_id = azurerm_subnet.public.id
}
Then in your databricks_workspace block, replace your custom parameters with
custom_parameters {
public_subnet_name = azurerm_subnet.public.name
public_subnet_network_security_group_association_id = azurerm_subnet_network_security_group_association.nsga_public.id
private_subnet_name = azurerm_subnet.private.name
private_subnet_network_security_group_association_id = azurerm_subnet_network_security_group_association.nsga_private.id
virtual_network_id = data.azurerm_virtual_network.vnet.id
}

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.

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

Resources