Creating multiple subnets in Azure Terraform using modules - terraform

I'm trying to deploy two subnets with different ip prefixes and different names and not create too much repeated code. I'm not sure how to approach it best. I tried the below but its not working. My terraform version is 0.14.11 so I think some of what I am doing below might be outdated
My main.tf in root module
module "deploy_subnet" {
source = "./modules/azure_subnet"
subscription_id = var.subscription_id
resource_group_name = var.resource_group_name
region = var.region
vnet_name = var.vnet_name
count_subnets = "${length(var.subnet_prefix)}"
subnet_name = "${lookup(element(var.subnet_prefix, count.index), "name")}"
address_prefix = "${lookup(element(var.subnet_prefix, count.index), "ip")}"
}
My variables.tf in root module (only pasting relevant part)
variable "subnet_prefix" {
type = "list"
default = [
{
ip = "10.0.1.0/24"
name = "aks-sn"
},
{
ip = "10.0.2.0/24"
name = "postgres-sn"
}
]
}
My main.tf in my child module folder
resource "azurerm_subnet" "obc_subnet" {
name = var.subnet_name
count = var.count_subnet
resource_group_name = var.resource_group_name
virtual_network_name = var.vnet_name
address_prefixes = var.address_prefix
}
My variables.tf in my child module folder (only relevant part)
variable "subnet_name" {
description = "Subnet Name"
}
variable "count_subnet" {
description = "count"
}
variable "address_prefix" {
description = "IP Address prefix"
}
I get the error below
Reference to "count" in non-counted context
on main.tf line 8, in module "deploy_subnet":
8: subnet_name = "${lookup(element(var.subnet_prefix, count.index),"name")}"
The "count" object can only be used in "module", "resource", and "data"
blocks, and only when the "count" argument is set
Reference to "count" in non-counted context
on main.tf line 9, in module "deploy_subnet":
9: address_prefix = "${lookup(element(var.subnet_prefix, count.index), "ip")}"
The "count" object can only be used in "module", "resource", and "data"
blocks, and only when the "count" argument is set.

It is exactly what it says. In the root module you try to reference a count.index but there is nothing being counted. All you do is pass a variable to the child module.
You should just pass subnet prefix to the child module, and in the child module set the count to the length of it, and reference count.index for the values of address_prefix and name
Alternatively, and probably more elegant you might look into for_each and each.value

Related

How to get value from module in another module - Terraform (Azure)

Im trying to get value from one module and use it in another module.
I have module - vnet
resource "azurerm_virtual_network" "vnet" {
name = var.vnet_name
resource_group_name = var.resource_group_name
location = var.location
address_space = var.address_space
}
resource "azurerm_subnet" "subnet" {
name = "${var.vnet_name}-subnet"
resource_group_name = var.resource_group_name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = var.subnet_prefixes
}
and the output is :
output "subnet_id" {
value = "${azurerm_subnet.subnet.id}"
}
output "vnet_name" {
value = "${azurerm_virtual_network.vnet.name}"
}
from this module i would like to get the vnet name and the subnet id for my other module that im using to create a nic.
nic module
module "vnet" {
source = "../vnet"
}
resource "azurerm_network_interface" "nic" {
name = "${module.vnet.vnet_name}-nic"
location = "east us 2"
resource_group_name = "null"
ip_configuration {
name = " "
subnet_id = module.vnet.subnet_id
private_ip_address_allocation = "Dynamic"
}
}
this way is working BUT the terraform plan , planning to create 2 resource per each resource because the way im using to get the values .
under nic module im using again the vnet module so its will create second vnet.
my main.tf is
resource "azurerm_resource_group" "rg" {
name = var.resource_group.name
location = var.resource_group.location
}
module "ib151w-vnet" {
source = "./modules/vnet"
resource_group_name = azurerm_resource_group.rg.name
vnet_name = "ib151w-vnet"
address_space = var.address_space
subnet_prefixes = var.subnet_prefixes
}
module "ib151w-nic" {
source = "./modules/nic"
name = "nic-test-123"
location = "east us 2"
resource_group_name = "ib151w"
}
the question is how can i get the vnet name and subnet id to use inside the nic module ?
i know there is alot of better ways to establish my request but im
just learning terraform and trying this specific way :)
how can i get the vnet name and subnet id to use inside the nic module
You have to explicitly pass those values in the root module:
module "ib151w-nic" {
source = "./modules/nic"
name = "nic-test-123"
location = "east us 2"
resource_group_name = "ib151w"
vnet_name = module.vnet.vnet_name
subnet_id = module.vnet.subnet_id
}
Also you have to modify your vnet module to make vnets and subents conditional. For example, add variable in the vent module:
variable "should_create_vnet_and_subnet" {
default = true
}
then make the resource conditional:
resource "azurerm_virtual_network" "vnet" {
count = should_create_vnet_and_subnet == true ? 1 : 0
name = var.vnet_name
resource_group_name = var.resource_group_name
location = var.location
address_space = var.address_space
}
resource "azurerm_subnet" "subnet" {
count = should_create_vnet_and_subnet == true ? 1 : 0
name = "${var.vnet_name}-subnet"
resource_group_name = var.resource_group_name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = var.subnet_prefixes
}
And the rest. Basically you have to rewrite your entire vnet module around conditional resources.
There are a lot of things to reconsider here but I would like to stick to your query only as you have requested.
How to get value from the module in another module - Terraform
As I can see you are already using two modules for vnet(which includes subnet) and nic and also using two modules interface calls to use them. You can simply use variables in your nic module and then at the interface level you can pass the outputs from vnet module as an attribute to your nic module.
Refer to the below code.
# main.tf or MODULE INTERFACES
## Default variables ##
variable "resource_group_name" {
type = string
description = "(optional) resource group name in which resources will created"
default = "stack-over-flow-query"
}
variable "location" {
type = string
description = "(optional) location where resources would be created."
default = "east us 2"
}
################################
resource "azurerm_resource_group" "rg" {
name = var.resource_group_name
location = var.location
}
module "ib151w-vnet" {
source = "./modules/vnet"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
vnet_name = "ib151w-vnet"
address_space = ["10.0.0.0/16"]
subnet_prefixes = ["10.0.1.0/24"]
}
module "ib151w-nic" {
source = "./modules/nic"
name = "${module.ib151w-vnet.vnet_name}-nic-test-123"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
subnet_id = module.ib151w-vnet.subnet_id
ip_configuration = "stackoverflow"
}
## NIC Module
resource "azurerm_network_interface" "nic" {
name = var.name
location = var.location
resource_group_name = var.resource_group_name
ip_configuration {
name = var.ip_configuration
subnet_id = var.subnet_id
private_ip_address_allocation = var.private_ip_address_allocation
}
}
## Required variable definitions with defaults(best practice installation in your situation)
Using the same outputs as yours in the vnet module.
output "vnet_name" {
value = azurerm_virtual_network.vnet.name
}
output "subnet_id" {
value = azurerm_subnet.subnet.id
}
Note: "${}"aka interpolation is not required when using terraform referencing without variables or any unknown value to terraform.
There can be a lot more ways to have a module like this but I would suggest at least a few things to try and do hands-on.
Use looping in your module to make multiple resources with one interface call [ only when necessary and make sense ]
https://developer.hashicorp.com/terraform/tutorials/configuration-language/for-each
Warning: extra looping can increase complexity.
Use conditions to control your module behavior.
https://developer.hashicorp.com/terraform/language/expressions/conditionals
And the most important when to use a module.
https://developer.hashicorp.com/terraform/language/modules/develop#when-to-write-a-module
I hope this helps and as I have stated this only answers your query, not some best practices or best vnet-nic module.

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

Terraform/HCL in Azure issues

I am new to HCL and Terraform and have having issues with associating a security group and a backend address pool to the network interface. I am creating 2 network interfaces in a single network interface block:
#Create network interface for 2 VMs
resource "azurerm_network_interface" "FrontNetworkInterface" {
count = 2
name = "niFront${count.index}"
location = azurerm_resource_group.PWSDevResourceGroup.location
resource_group_name = azurerm_resource_group.PWSDevResourceGroup.name
ip_configuration {
name = "ipconfFrontVM"
subnet_id = azurerm_subnet.PWSDevSubnet.id
private_ip_address_allocation = "dynamic"
}
}
I have tried associating in various ways that have produced different errors:
ATTEMPT 1:
#Connect security group to the network interface
resource "azurerm_network_interface_security_group_association" "PWSDevSecurityGroupAssoc" {
network_interface_id = azurerm_network_interface.FrontNetworkInterface.id
network_security_group_id = azurerm_network_security_group.PWSDevSecurityGroup.id
}
#Connect 2 backend ips to the load balancer
resource "azurerm_network_interface_backend_address_pool_association" "BackendIPAssoc" {
network_interface_id = azurerm_network_interface.FrontNetworkInterface.id
ip_configuration_name = "bipa"
backend_address_pool_id = azurerm_lb_backend_address_pool.BackendIpPool.id
}
ERRORS:
Error: Missing resource instance key
on front.tf line 85, in resource "azurerm_network_interface_security_group_association" "PWSDevSecurityGroupAssoc":
85: network_interface_id = azurerm_network_interface.FrontNetworkInterface.id
Because azurerm_network_interface.FrontNetworkInterface has "count" set, its
attributes must be accessed on specific instances.
For example, to correlate with indices of a referring resource, use:
azurerm_network_interface.FrontNetworkInterface[count.index]
Error: Missing resource instance key
on front.tf line 91, in resource "azurerm_network_interface_backend_address_pool_association" "BackendIPAssoc":
91: network_interface_id = azurerm_network_interface.FrontNetworkInterface.id
Because azurerm_network_interface.FrontNetworkInterface has "count" set, its
attributes must be accessed on specific instances.
For example, to correlate with indices of a referring resource, use:
azurerm_network_interface.FrontNetworkInterface[count.index]
ATTEMPT 2/3/4 (Using "[count.index]", "[count.index].id", or "[element(azurerm_network_interface.FrontNetworkInterface.*.id, count.index)]" as described in the previous error):
#Connect security group to the network interface
resource "azurerm_network_interface_security_group_association" "PWSDevSecurityGroupAssoc" {
network_interface_id = azurerm_network_interface.FrontNetworkInterface[count.index]
network_security_group_id = azurerm_network_security_group.PWSDevSecurityGroup.id
}
#Connect 2 backend ips to the load balancer
resource "azurerm_network_interface_backend_address_pool_association" "BackendIPAssoc" {
network_interface_id = azurerm_network_interface.FrontNetworkInterface[count.index]
ip_configuration_name = "bipa"
backend_address_pool_id = azurerm_lb_backend_address_pool.BackendIpPool.id
}
ERROR (Same result for [count.index].id and [element(azurerm_network_interface.FrontNetworkInterface.*.id, count.index)]):
Error: Reference to "count" in non-counted context
on front.tf line 85, in resource "azurerm_network_interface_security_group_association" "PWSDevSecurityGroupAssoc":
85: network_interface_id = azurerm_network_interface.FrontNetworkInterface[count.index]
The "count" object can only be used in "module", "resource", and "data"
blocks, and only when the "count" argument is set.
Error: Reference to "count" in non-counted context
front.tf line 91, in resource "azurerm_network_interface_backend_address_pool_association" "BackendIPAssoc":
network_interface_id = azurerm_network_interface.FrontNetworkInterface[count.index]
The "count" object can only be used in "module", "resource", and "data"
blocks, and only when the "count" argument is set.
Also, I am receiving this error on my azurerm_virtual_machine block:
line 162, in resource "azurerm_virtual_machine" "FrontEndVirtualMachines":
162: admin_ssh_key {
Blocks of type "admin_ssh_key" are not expected here.
I am following what is shown here:
https://learn.microsoft.com/en-us/azure/developer/terraform/create-linux-virtual-machine-with-infrastructure
As you can see, the admin_ssh_key block is provided. I tried using version 2.0 as used in the scripts; however, I experienced the same result.
Thanks for your help!! :)
When referencing a resource created with count you still need to add the .id. See the following example. For more information see this link.
provider "azurerm" {
version = "~>2.23.0"
features {}
}
resource "azurerm_resource_group" "example" {
name = "example-resources"
location = "East US"
}
resource "azurerm_virtual_network" "example" {
name = "vnet"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
address_space = ["10.0.0.0/16"]
dns_servers = ["10.0.0.4", "10.0.0.5"]
}
resource "azurerm_subnet" "example" {
name = "example"
resource_group_name = azurerm_resource_group.example.name
virtual_network_name = azurerm_virtual_network.example.name
address_prefixes = ["10.0.1.0/24"]
}
resource "azurerm_network_interface" "example" {
count = 2
name = format("int%s", count.index)
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
ip_configuration {
name = "ip"
subnet_id = azurerm_subnet.example.id
private_ip_address_allocation = "dynamic"
}
}
resource "azurerm_network_security_group" "example" {
name = "acceptanceTestSecurityGroup1"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
security_rule {
name = "test123"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
resource "azurerm_network_interface_security_group_association" "secgroup" {
count = length(azurerm_network_interface.example)
network_interface_id = azurerm_network_interface.example[count.index].id
network_security_group_id = azurerm_network_security_group.example.id
}
I will admit that I haven't read the whole story, but it looks like your attempt #2/3/4 was pretty close. Where you use [count.index], you need to specify a count, otherwise there's no count to index. So if you just add count = 2 to those two resource blocks, it should work.
Better yet, either have the 2 as a variable, or use
count = len(azurerm_network_interface.FrontNetworkInterface)
to ensure you don't end up with mismatched numbers when you change the 2 later on.

terraform apply from not generated resource

I'm trying to create a list of maps from my list of subnet names so I've created the following:
created a variable named subnet_names of type list of strings
created a null resource block to create a list of maps from this list, like this:
resource "null_resource" "subnet_mapping" {
count = "${length(var.subnet_names)}"
triggers = {
name = "${element(var.subnet_names, count.index)}"
number = "${count.index}"
}
}
if I only execute this block I have my list of maps correctly but when I try to use this list of maps with a dynamic block this is not working.
resource "azurerm_virtual_network" "virtual_network" {
address_space = "${var.cidr_network_range}"
location = "${var.location}"
name = "${var.virtual_network_resource_name}"
resource_group_name = "${var.resource_group_name}"
count = "${length(var.subnet_names)}"
dynamic "subnet"{
for_each = [for s in null_resource.subnet_mapping: {
name = s.name
prefix = cidrsubnet(element(var.cidr_network_range, 0),8 ,s.number)
}]
content {
name = subnet.value.name
address_prefix = subnet.value.prefix
}
}
depends_on = [null_resource.subnet_mapping]
}
resource "null_resource" "subnet_mapping" {
count = "${length(var.subnet_names)}"
triggers = {
name = "${element(var.subnet_names, count.index)}"
number = "${count.index}"
}
}
resource "azurerm_resource_group" "virtual_network_group" {
location = "${var.location}"
name = "${var.resource_group_name}"
}
it should be valid, but I still don't have the output of the null_resource so it fails
dynamic "subnet"{
for_each = [for s in null_resource.subnet_mapping: {
name = s.name
prefix = cidrsubnet(element(var.cidr_network_range, 0),8 ,s.number)
}]
content {
name = subnet.value.name
address_prefix = subnet.value.prefix
}
}
depends_on = [null_resource.subnet_mapping]
}
my error message:
Error: Unsupported attribute
on main.tf line 10, in resource "azurerm_virtual_network" "virtual_network":
10: name = s.name
This object does not have an attribute named "name".
Error: Unsupported attribute
on main.tf line 10, in resource "azurerm_virtual_network" "virtual_network":
10: name = s.name
This object does not have an attribute named "name".
Error: Unsupported attribute
on main.tf line 10, in resource "azurerm_virtual_network" "virtual_network":
10: name = s.name
This object does not have an attribute named "name".
Error: Unsupported attribute
on main.tf line 10, in resource "azurerm_virtual_network" "virtual_network":
10: name = s.name
This object does not have an attribute named "name".
Error: Unsupported attribute
on main.tf line 11, in resource "azurerm_virtual_network" "virtual_network":
11: prefix = cidrsubnet(element(var.cidr_network_range, 0),8 ,s.number)
This object does not have an attribute named "number".
Error: Unsupported attribute
on main.tf line 11, in resource "azurerm_virtual_network" "virtual_network":
11: prefix = cidrsubnet(element(var.cidr_network_range, 0),8 ,s.number)
This object does not have an attribute named "number".
Error: Unsupported attribute
on main.tf line 11, in resource "azurerm_virtual_network" "virtual_network":
11: prefix = cidrsubnet(element(var.cidr_network_range, 0),8 ,s.number)
This object does not have an attribute named "number".
Error: Unsupported attribute
on main.tf line 11, in resource "azurerm_virtual_network" "virtual_network":
11: prefix = cidrsubnet(element(var.cidr_network_range, 0),8 ,s.number)
This object does not have an attribute named "number".
From the message, I'm not clear how many Vnets do you want to create, because your code is a little confusing. For your issue with the null_resource, I don't think it's a good choice, I recommend the locals.
Here I assume you only want to create one Vnet with serial subnets and use a list to store the names of the subnets, then the example core code below:
locals {
subnets = [for sname in var.subnet_names: {
name = sname
# the index begin from 0, so you need to add 1
number = index(var.subnet_names, sname) + 1
}]
}
resource "azurerm_virtual_network" "virtual_network" {
address_space = "${var.cidr_network_range}"
location = "${var.location}"
name = "${var.virtual_network_resource_name}"
resource_group_name = "${var.resource_group_name}"
dynamic "subnet"{
for_each = [for s in local.subnets: {
name = s.name
prefix = cidrsubnet(var.cidr_network_range, 8 , s.number)
}]
content {
name = subnet.value.name
address_prefix = subnet.value.prefix
}
}
}
I solved this way, I've created a variable named
variable "subnets" {
type = list(map(string))
description = "A list of maps of names and network addresses bits for subnets that will be created inside this network (this is parallel to subnet_prefixes array)."
}
resource "azurerm_virtual_network" "virtual_network" {
address_space = var.cidr_network_range
location = var.location
name = var.virtual_network_resource_name
resource_group_name = var.resource_group_name
dynamic "subnet"{
for_each = [for subnet in var.subnets: {
name = subnet.name
prefix = cidrsubnet(element(var.cidr_network_range, 0),8 ,subnet.number)
}]
content {
name = subnet.value.name
address_prefix = subnet.value.prefix
}
}
}
resource "azurerm_resource_group" "virtual_network_group" {
location = var.location
name = var.resource_group_name
}
This way I already created the map, I would like to have created it automatically as I posted before but it seems to not be very common in terraform way.
In Charles Xu example the mapping will be created based on a sequence (1,2,3) and I would like to use it always in different ways like (21, 24, 26)
So the only solution I could find was to force the map before.
Anyway, thank you everyone

How Do I Avoid Repeating A Variable In Terraform?

Terraform doesn't allow you to interpolate variables within the variables file otherwise you get the error:
Error: Variables not allowed
on variables.tf line 9, in variable "resource_group_name": 9:
default = "${var.prefix}-terraform-dev_rg"
Variables may not be used here.
This then means I end up duplicating the value of the prefix in my variables.tf file when I try to create the name for the resource group.
Is there a nice way around this to avoid duplicating the value of the variable?
variables.tf
variable "prefix" {
description = "The prefix used for all resources in this plan"
default = "terraform-dev"
}
variable resource_group_name {
type = "string"
default = "terraform-dev_rg"
}
variable resource_group_location {
type = "string"
default = "eastus"
}
main.tf
# Configure the Microsoft Azure Provider
provider "azurerm" {
version = "=1.28.0"
}
# Create a resource group
resource "azurerm_resource_group" "resource-group" {
name = var.resource_group_name
location = var.resource_group_location
}
#Create an application gateway with web app firewall
module "firewall" {
source = "./firewall"
resource_group_name = var.resource_group_name
resource_group_location = var.resource_group_location
}
./firewall/variables.tf
#Passed down from the root variables.tf
variable "prefix" {}
variable "resource_group_name" {}
variable "resource_group_location" {}
./firewall/main.tf
# Create a virtual network for the firewall
resource "azurerm_virtual_network" "firewall-vnet" {
name = "${var.prefix}-waf-vnet"
address_space = ["10.0.0.0/16"]
resource_group_name = var.resource_group_name
location = var.resource_group_location
}
Try to use local values,
https://www.terraform.io/docs/configuration/locals.html
variable "prefix" {
description = "The prefix used for all resources in this plan"
default = "terraform-dev"
}
variable resource_group_location {
type = "string"
default = "eastus"
}
locals {
resource_group_name = "${var.prefix}_rg"
}
resource "azurerm_resource_group" "resource-group" {
name = local.resource_group_name
location = var.resource_group_location
}
Terraform does not support variables inside a variable.
If you want to generate a value based on two or more variables then you can try Terraform locals (https://www.terraform.io/docs/configuration/locals.html).
Locals should help you here to achieve goal.
something like
variables.tf
variable "prefix" {
description = "The prefix used for all resources in this plan"
default = "terraform-dev"
}
variable resource_group_location {
type = "string"
default = "eastus"
}
main.tf
locals {
resource_group_name = "${var.prefix}_rg"
}
# Configure the Microsoft Azure Provider
provider "azurerm" {
version = "=1.28.0"
}
# Create a resource group
resource "azurerm_resource_group" "resource-group" {
name = local.resource_group_name
location = var.resource_group_location
}
Hope this helps.
Please read similar discussion here -https://stackoverflow.com/questions/58841060/terraform-variables-within-variables/58841360?noredirect=1#comment129460631_58841360

Resources