So I start to explore Terraform, and so far Im able to get my network and VMs up in Azure.
Since I have seven VM`s that are created, I would like to get the IP-adress and the Hostname of this.
In my main.tf i have this:
provider "azurerm" {
features {}
# Configuration options
}
module "splunk_architect" {
source = "./modules/architect"
}
And just an example from my main.tf in /modules/architect
resource "azurerm_network_interface" "main" {
for_each = toset(var.vm_names)
name = "${each.value}-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[each.key].id
}
And my outputs.tf in /modules/architect
output "ip" {
value = azurerm_network_interface.main[each.key].private_ip_address
}
So when i run this, i get this error message:
Error: Reference to "each" in context without for_each
on modules\architect\outputs.tf line 6, in output "ip": 6: value = azurerm_network_interface.main[each.key].private_ip_address
The "each" object can be used only in "module" or "resource" blocks, and only when the "for_each" argument is set.
So I have tried to set the for_each in the module, but did not get that to work.
I have also been through the documentations without any success.
Any tips to what I can try to get the IP of each VM printed out?
Since you have multiple virtual machines (declared with for_each), you will also have multiple private IP addresses to return. You'll need to decide for your module what is the best way to return those IP addresses to the caller.
One common answer is to return a map whose keys are the elements from var.vm_names, so the caller can easily correlate the VM names it passed in to the IP addresses you're returning, like this:
output "ip" {
value = tomap({
for name, vm in azurerm_network_interface.main : name => vm.private_ip_address
})
}
This is a for expression, which constructs a new data structure from an existing data structure by evaluating expressions against each element. In this case, it's taking the keys from azurerm_network_interface.main -- which will match the values given in for_each -- and mapping them to the private_ip_address attribute for each object.
The result will therefore appear as a map from VM names to IP addresses, perhaps like this:
{
"example1" = "10.1.2.1"
"example2" = "10.1.2.45"
}
Not directly related to your question, but note also that if your module only ever uses var.vm_names as a set then it can be better to declare it as a set in the first place, rather than converting it at each use, because then it'll be clearer to users of your module that the order of the strings inside doesn't matter and that there can't be two elements with the same string:
variable "vm_names" {
type = set(string)
}
With that declaration, var.vm_names will already be a set of strings and so you don't need to explicitly convert it in for_each:
for_each = var.vm_names
Related
Here is my shortened code:
variable.tf:
variable "vm_avd" {
type = map(any)
default = {
avd-std = {
vm_count = "2"
### Interface Vars
subnet_id = "AD"
}
avd-gpu = {
vm_count = "2"
### Interface Vars
subnet_id = "AD"
}
}
}
variable "vm_server" {
type = map(any)
default = {
srv-std = {
vm_count = "2"
### Interface Vars
subnet_id = "AD"
}
}
}
if.tf:
resource "azurerm_network_interface" "if" {
count = var.vm_count
name = var.private_ip_address_allocation == "Static" ? "if_${var.if_name}_${replace(var.private_ip_address, ".", "")}_${format("%02d", count.index)}" : "if_${var.if_name}_${format("%02d", count.index)}"
location = var.location
resource_group_name = var.resource_group_name
ip_configuration {
name = "if_ipconfig"
subnet_id = var.subnet_id
private_ip_address_version = var.private_ip_address_version
private_ip_address_allocation = var.private_ip_address_allocation
private_ip_address = var.private_ip_address
}
}
output "if_name" {
value = azurerm_network_interface.if[*].name
}
output "if_id" {
value = azurerm_network_interface.if[*].id
}
main.tf:
module "vm" {
for_each = merge(var.vm_server, var.vm_avd)
vm_name = each.key
vm_count = each.value["vm_count"]
network_interface_ids = module.if[each.key].if_id
}
module "if" {
for_each = merge(var.vm_server, var.vm_avd)
vm_count = each.value["vm_count"]
if_name = each.key
subnet_id = module.snet[each.value["subnet_id"]].snet_id
}
Is there a possibility to reference in a for_each loop the correct count object for dependent resources?
I'm creating Terraform modules for Azure and I want to build a structure that is highly flexible and easy to fill for everyone.
Therefore I build a root file with a single module call for each resource and I have a single main variables file which should be filled with values which are looped through.
All related resource variables are defined in the same variables object, so for a VM a variable VM is created and filled with VM details, NIC details and extension details. The single modules run through the same variables for VM, NIC and extensions which is, in general, running fine.
I can reference the subnet_id for the network interfaces nicely but as soon as I use a count operator in the resource I don't know how the correctly get the network interface ID of the correct interface for the correct VM.
Now I ran into the problem that I use for_each in my module call and I want to have the option to use count in my resource definition too. It is working but when I build e.g. virtual machines, I get the problem that I cannot reference to the correct network interfaces.
Atm all my interfaces are connected to the same VM and the 2nd VM cannot be build. Same goes for extensions and any possible multiple objects tho.
I tried several module calls but the first VM got all the interfaces every time. I tried with the Star character in the NIC module call or with the network interface ID.
If this is not possible at all, I thought of a solution with building the network interface ID itself with its original form.
I also checked if there is a possibility to build an array with the for_each and count elements but I couldn't find any way to build arrays out of number variables in terraform, like the normal "for 1 to 5 do".
If you know of any way to do this, I would be grateful too.
Maybe it is a typo when you try to simplify the question, but in your variables you have:
variable vm_avd { default = { avd-std = {...}} }
variable vm_server { default = { avd-std = {...}} }
in your main tf you have:
merge(var.vm_server, var.vm_avd)
since the key is the same, one will override the other, if you are using the default value, it might result in the behaviour you are suggesting, consider changing the key name to server-std?
I got the solution with simply providing the whole element array, e.g. from VM, to the variable for the ID and the reference the correct VM with the count index:
virtual_machine_id = var.vm_id[count.index]
As i run through the same Count in other ressources, I get the correct order of the elements for referencing :)
I am creating an AWS VPC with a single public subnet in a brand-new Terraform project, consisting only of a main.tf file. In that file I am using two resource blocks, aws_vpc and aws_subnet. The second resource must be attached to the first using the vpc_id attribute. The value of this attribute is created only upon apply, so it cannot be hard-coded. How do I get the ID of the resource I just created, so I can use it in the subsequent block?
resource "aws_vpc" "my_vpc" {
cidr_block = "102.0.0.0/16"
tags = {
Name = "My-VPC"
}
}
resource "aws_subnet" "my_subnet" {
vpc_id = # what goes here?
cidr_block = "102.0.0.0/24"
tags = {
Name = "My-Subnet"
}
}
The docs give the example of data.aws_subnet.selected.vpc_id for vpc_id. The value of this appears to depend on two other blocks, variable and data. I am having a hard time seeing how to wire these up to my VPC. I tried copying them directly from the docs. Upon running terraform plan I get the prompt:
var.subnet_id
Enter a value:
This is no good; I want to pull the value from the VPC I just created, not enter it at the command prompt. Where do I specify that the data source is the resource that I just created in the previous code block?
I have heard that people create a separate file to hold Terraform variables. Is that what I should to do here? It seems like it should be so basic to get an ID from one resource and use it in the next. Is there a one-liner to pass along this information?
You can just call the VPC in the subnet block by referencing Terraform's pointer. Also, doing this tells Terraform that the VPC needs to be created first and destroyed second.
resource "aws_vpc" "my_vpc" {
cidr_block = "102.0.0.0/16"
tags = {
Name = "My-VPC"
}
}
resource "aws_subnet" "my_subnet" {
vpc_id = aws_vpc.my_vpc.id
cidr_block = "102.0.0.0/24"
tags = {
Name = "My-Subnet"
}
}
I want to pull the value from the VPC I just created,
You can't do this. You can't dynamically populate variables from data sources. But you could use local instead:
locals {
subnet_id = data.aws_subnet.selected.id
}
and refer to it as local.subnet_id.
I have a set of variables in terraform.tfvars:
resource_groups = {
cow = {
name = "Cow"
location = "eastus"
},
horse = {
name = "Horse"
location = "eastus"
},
chicken = {
name = "Chicken"
location = "westus2"
},
}
my main.tf looks like this:
...
module "myapp" {
source = "./modules/myapp"
resource_groups = var.resource_groups
}
variable "resource_groups" {}
...
./modules/myapp.main.tf look like this:
module "resource_group" {
source = "../myapp.resource_group"
resource_groups = var.resource_groups
for_each = {
for key, value in try(var.resource_groups, {}) : key => value
if try(value.reuse, false) == false
}
}
variable "resource_groups" {}
and ../myapp.resource_group looks like this:
resource "azurerm_resource_group" "resource_group" {
name = var.resource_groups.cow.name
location = var.resource_groups.cow.location
}
variable "resource_groups" {}
My hope is that after terraform plan I would see that three new RGs would be set for addition. Infact I do get three new ones, but they all use the name and location of the cow RG, because I specified var.resource_groups.cow.name The problem is I have tried all kinds of different iterators in place of .cow. and I can't get terraform to use the other variables in the terraform.tfvars file. I have tried square brackets, asterisks, and other wildcards. I am stuck.
I am looking to define a resource in one place and then use that to create multiple instances of that resource per the map of variables.
Guidance would be much appreciated.
Thanks.
Bill
For this situation you'll need to decide whether your module represents one resource group or whether it represents multiple resource groups. For a module that only contains one resource anyway that decision is not particularly important, but I assume you're factoring this out into a separate module because there is something more to this than just the single resource group resource, and so you can decide between these two based on what else this module represents: do you want to repeat everything in this module, or just the resource group resource?
If you need the module to represent a single resource group then you should change its input variables to take the data about only a single resource group, and then pass the current resource group's data in your calling module block.
Inside the module:
variable "resource_group" {
type = object({
name = string
location = string
})
}
resource "azurerm_resource_group" "resource_group" {
name = var.resource_group.name
location = var.resource_group.location
}
When calling the module:
variable "resource_groups" {
type = map(
object({
name = string
location = string
})
)
}
module "resource_group" {
source = "../myapp.resource_group"
for_each = var.resource_groups
# each.value is the value of the current
# element of var.resource_groups, and
# so it's just a single resource group.
resource_group = each.value
}
With this strategy, you will declare resource instances with the following addresses, showing that the repetition is happening at the level of the whole module rather than the individual resources inside it:
module.resource_group["cow"].azurerm_resource_group.resource_group
module.resource_group["horse"].azurerm_resource_group.resource_group
module.resource_group["chicken"].azurerm_resource_group.resource_group
If you need the module to represent the full set of resource groups then the module would take the full map of resource groups as an input variable instead of using for_each on the module block. The for_each argument will then belong to the nested resource instead.
Inside the module:
variable "resource_groups" {
type = map(
object({
name = string
location = string
})
)
}
resource "azurerm_resource_group" "resource_group" {
for_each = var.resource_groups
name = each.value.name
location = each.value.location
}
When calling the module:
variable "resource_groups" {
type = map(
object({
name = string
location = string
})
)
}
module "resource_group" {
source = "../myapp.resource_group"
# NOTE: No for_each here, because we need only
# one instance of this module which will itself
# then contain multiple instances of the resource.
resource_group = var.resource_groups
}
With this strategy, you will declare resource instances with the following addresses, showing that there's only one instance of the module but multiple instances of the resource inside it:
module.resource_group.azurerm_resource_group.resource_group["cow"]
module.resource_group.azurerm_resource_group.resource_group["horse"]
module.resource_group.azurerm_resource_group.resource_group["chicken"]
It's not clear from the information you shared which of these strategies would be more appropriate in your case, because you've described this module as if it is just a thin wrapper around the azurerm_resource_group resource type and therefore it isn't really clear what this module represents, and why it's helpful in comparison to just writing an inline resource "azurerm_resource_group" block in the root module.
When thinking about which of the above designs is most appropriate for your use-case, I'd suggest considering the advice in When to Write a Module in the Terraform documentation. It can be okay to write a module that contains only a single resource block, but that's typically for more complicated resource types where the module hard-codes some local conventions so that they don't need to be re-specified throughout an organization's Terraform configurations.
If you are just passing the values through directly to the resource arguments with no additional transformation and no additional hard-coded settings then that would suggest that this module is not useful, and that it would be simpler to write the resource block inline instead.
I have the code below in which I create vnets in for_each block:
provider "azurerm" {
features {}
}
variable "vnets" {
type = map(object({
name = string
address_space = list(string)
}))
default = {
"vnet1" = {
"name" = "vnet1",
"address_space" = ["10.0.0.0/16"]
},
"vnet2" = {
"name" = "vnet2",
"address_space" = ["10.1.0.0/16"]
}
}
}
resource "azurerm_resource_group" "vnets" {
name = "vnets"
location = "WestEurope"
}
resource "azurerm_virtual_network" "virtual_network" {
for_each = var.vnets
name = each.value.name
location = "West Europe"
resource_group_name = azurerm_resource_group.vnets.name
address_space = each.value.address_space
}
Everything works with the plan, virtual networks will be created,
but the problem is how to get to the created resources from the for_each block?
When I type the command to return the resources list:
terraform state list
Then I have the following output from console:
azurerm_resource_group.vnets
azurerm_virtual_network.virtual_network["vnet1"]
azurerm_virtual_network.virtual_network["vnet2"]
And when I want to use a vnet1 anywhere in the code using reference azurerm_virtual_network.virtual_network["vnet1"] then I'm getting an error.
For example, I want to view resource vnet1:
terraform state show azurerm_virtual_network.virtual_network["vnet1"]
Im getting such error:
Error parsing instance address: azurerm_virtual_network.virtual_network[vnet1]
This command requires that the address references one specific instance.
To view the available instances, use "terraform state list". Please modify
the address to reference a specific instance.
I tried the following commands to access a resource, but they don't work:
terraform state show azurerm_virtual_network.virtual_network["vnet1"]
terraform state show 'azurerm_virtual_network.virtual_network["vnet1"]'
terraform state show azurerm_virtual_network.virtual_network[vnet1]
terraform state show azurerm_virtual_network.virtual_network[0]
terraform state show azurerm_virtual_network.virtual_network.vnet1
Do you know how to solve it?
In addition, if you want to show a specific instance in the state file, you can use terraform state show 'azurerm_virtual_network.virtual_network[\"vnet1\"]'
I ran into this same issue with using modules. The quoted above is the correct answer and the following is how to perform it with a module:
terraform state show 'module.module_name[\"module_instance\"].resource.resource_name[\"resource_instance\"]
Example:
state show 'module.rsm_keyvault_solution_module_resource[\"development\"].azurerm_key_vault.keyvault_module_resource[\"rsm-playground-dev\"]'
In this case, you can use the values function to take a map and return a list containing the values of the elements in that map.
For example, to get the values of VNets in a map:
output "azurerm_vnets_names" {
value = values(azurerm_virtual_network.virtual_network)[*].name
}
Or get a specific VNet name like this:
output "azurerm_vnet1_name" {
value = values(azurerm_virtual_network.virtual_network)[0].name
}
In addition, if you want to show a specific instance in the state file, you can use
terraform state show 'azurerm_virtual_network.virtual_network[\"vnet1\"]'
For Azure Terraform:
If a variable is declared in a tf file will this value be applied to same variable in other tf files processed together? Why is there a default value associated with a variable statement?
If I made a tfvars file:
cidrs = [ "10.0.0.0/16", "10.1.0.0/16" ]
Can cidr be used as below for subnet id? Not really understanding usage syntax?
subnet_id = "${azurerm_subnet.subnet.id}"
subnet id = cidr
What exactly is the "Default" function when used with variables? See below:
variable "prefix" {
type = "string"
default = "my"
}
variable "tags" {
type = "map"
default = {
Environment = "Terraform GS"
Dept = "Engineering"
}
}
variable "sku" {
default = {
westus = "16.04-LTS"
eastus = "18.04-LTS"
}
}
There are several questions there. the easy one: default:
The variable declaration can also include a default argument. If
present, the variable is considered to be optional and the default
value will be used if no value is set when calling the module or
running Terraform. The default argument requires a literal value and
cannot reference other objects in the configuration.
For the other question, refer to this example: https://www.terraform.io/docs/providers/azurerm/r/subnet.html#attributes-reference
So in short, to use the existing subnet cidr, you need to refer to it like this:
azurerm_subnet.%subnetname%.address_prefix
subnet name, however, cannot be equals to a cidr, because it doesnt allow for / inside of the name. you can use something like this though: 10.0.0.0-24