Create a dynamic terraform resource - terraform

All the examples of terraform for_each and dynamic that I can find are nested within a resource. I want to create a dynamic list of resources themselves out of a 'set' input variable.
For example, the azurerm_mssql_firewall_rule is a descrete resource, so in order to have many firewall rules, I'd need something like
Parent Module:
module "AzureSqlServer" {
allowedIPs = local.azureSqlAllowedIPs
}
variable "azureSQLAllowedIPs" {
type = map(object({
name = string
ipAddress = string
}))
}
Child module:
resource "azurerm_mssql_firewall_rule" "azureSQLFirewall"{
for_each var.azureSQLAllowedIPs
name = each.value["name"]
server_id = azurerm_mssql_server.azureSqlServer.id
start_ip_address = each.value["ipAddress"]
end_ip_address = each.value["ipAddress"]
}
Is there some way to do dynamic outside the block?

Turns out I was close. Thanks to this post I was able to figure it out:
variable "azureSQLAllowedIPs" {
type = map(object({
name = string
ipAddress = string
}))
}
resource "azurerm_mssql_firewall_rule" "azureSQLFirewall" {
for_each = var.azureSQLAllowedIPs
name = each.value.name
server_id = azurerm_mssql_server.sql_server.id
start_ip_address = each.value.ipAddress
end_ip_address = each.value.ipAddress
}
(missing a "=" after the for_each and reference the map element by dot notation instead of string reference.

Related

Tag variable with list in aws_s3_bucket terraform resource

I am trying to create a tag variable as part of a module for aws_s3_bucket terraform resource. One of the tag keys should allow a list(string) value. What data type should I use?
I tried the following in variables.tf:
variable "name" {
type = string
}
variable "tags" {
type = object({
Owner = string
DataCategory = list(string)
}))
With this implementation in main.tf:
resource "aws_s3_bucket" "bucket" {
bucket = var.name
tags = var.tags
}
And trying to create a bucket in ./example/main.tf:
module "s3_bucket_test" {
source = "../"
name = "example-bucket"
tags = {
Owner = "My Team"
DataCategory = ["Customer","Finance"]
}
}
But I am getting the error:
Inappropriate value for attribute "tags": element
"DataCategory": string required.

Terraform error: Expected an attribute value, introduced by an equals sign ("=")

I'm really new to terraform and I'm having an issue doing something very basic.
I will only put in the relevant codes to help save time
In my variables.tf file, I have the following:
variable "keys_location" {
type = string
description = "keys are here"
}
I have kept the keys that are referenced in my wrkspace.vars, I have the following:
keys_location = {
"./keys/testing/certificate1.cer",
"./keys/testing/certificate2.cer",
}
And in my main.tf, I have this
resource "azurerm_virtual_network_gateway" "gw" {
name = "testing-${terraform.workspace}"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
type = "Vpn"
vpn_type = "RouteBased"
active_active = false
enable_bgp = false
sku = "VpnGw1"
ip_configuration {
name = "config"
public_ip_address_id = azurerm_public_ip.ip.id
private_ip_address_allocation = "Dynamic"
subnet_id = azurerm_subnet.subnet.id
}
test_config {
test_protocols = ["IkeV2","SSTP"]
address_space = [var.vpn_test_address_space]
dynamic "keys_location" {
for_each = var.keys_location
root_certificate {
name = "Root-Cert"
public_cert_data = file(var.keys_location)
}
}
}
When I do a terraform plan, the error I get is:
Error: Missing attribute value
on vars/wrkspace.vars line 5:
4: keys_location = {
5: "./keys/testing/certificate1.cer",
Expected an attribute value, introduced by an equals sign ("=").
How can this issue be fixed?
The Terraform for_each meta-argument operates on maps and sets, and iteration is done over either keys and values (in the case of maps) or values (in the case of sets). In order to iterate over the values in a set, one should use each.key.
In your case, because you're actually iterating over the values in the path_to_keys variable, the keys_location variable is unnecessary. You should instead reference each of the values inside the path_to_keys variable.
One possible solution would be changing your main.tf to the following:
resource "azurerm_virtual_network_gateway" "gw" {
name = "testing-${terraform.workspace}"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
type = "Vpn"
vpn_type = "RouteBased"
active_active = false
enable_bgp = false
sku = "VpnGw1"
ip_configuration {
name = "config"
public_ip_address_id = azurerm_public_ip.ip.id
private_ip_address_allocation = "Dynamic"
subnet_id = azurerm_subnet.subnet.id
}
test_config {
test_protocols = ["IkeV2","SSTP"]
address_space = [var.vpn_test_address_space]
dynamic "keys_location" {
for_each = var.path_to_keys
root_certificate {
name = "Root-Cert"
public_cert_data = file(each.key)
}
}
}
your variables.tf to:
variable "path_to_keys" {
type = set
description = "keys are here"
}
and your wrkspace.tfvars to:
path_to_keys = [
"./keys/testing/certificate1.cer",
"./keys/testing/certificate2.cer",
]
I can see two problems with what you shared, and one of them is the cause of this error.
The error itself is reporting that you've used braces { }, which are delimiters used for writing object/map values, but the content of the braces looks like you intended to define a list instead.
If you did intend to define a list then you should use square brackets [ ] to indicate that:
keys_location = [
"./keys/testing/certificate1.cer",
"./keys/testing/certificate2.cer",
]
If you actually did intend to declare a map then you'll need to choose a unique key for each element, which is what this error message is trying to tell you (because it's assuming you wanted to declare a map). I can't predict what would be good map keys so I've just used some placeholder ones for example here:
keys_location = {
cert1 = "./keys/testing/certificate1.cer",
cert2 = "./keys/testing/certificate2.cer",
}
If you make a change like I suggested above then I expect you will encounter the second error, which is that you've declared your variable as type = string but you've assigned it a list or map value. To make that work you'll need to specify a more appropriate type constraint for the variable.
If you intended to provide a list (using square brackets [ ]) then you could declare the variable as follows:
variable "keys_location" {
type = list(string)
}
If you intended to provide a map (using braces { }) then you could declare the variable as follows:
variable "keys_location" {
type = map(string)
}

Understanding Terraform for_each loop iteration

I am learning terraform and trying to understand the for_each loop iteration in terraform.
I am iterating through a loop for creating RGs in Azure cloud and what I want to understand is the difference between accessing the value of an instance using . or [""].
So for example, below is my tfvar file:
resource_groups = {
resource_group_1 = {
name = "terraform-apply-1"
location = "eastus2"
tags = {
created_by = "vivek89#test.com"
}
},
resource_group_2 = {
name = "terraform-apply-2"
location = "eastus2"
tags = {
created_by = "vivek89#test.com"
}
},
resource_group_3 = {
name = "terraform-apply-3"
location = "eastus2"
tags = {
created_by = "vivek89#test.com"
contact_dl = "vivek89#test.com"
}
}
}
and below is my terraform main.tf file:
resource "azurerm_resource_group" "terraformRG" {
for_each = var.resource_groups
name = each.value.name
location = each.value.location
tags = each.value.tags
}
I am confused with the expression in for_each in RG creation block. Both the below codes works and create RGs:
name = each.value.name
name = each.value["name"]
I want to understand the difference between the two and which one is correct.
They are equivalent as explained in the docs:
Map/object attributes with names that are valid identifiers can also be accessed using the dot-separated attribute notation, like local.object.attrname. In cases where a map might contain arbitrary user-specified keys, we recommend using only the square-bracket index notation (local.map["keyname"]).
The main difference is that dot notation requires key attributes to be valid identifiers. In contrast, the square-bracket notation works with any identifiers.

Iterating on type object in terraform

I have the following terraform:
The idea is that I can pass in volumes and either pass binary_data or data and this module will handle accordingly.
However it is not liking the nested loop. I have a feeling it's not liking iterating on the object.
When I run this I get the following error
#variable.tf
variable "volumes" {
type = map(object({
data = map(string)
binary_data = map(string)
}))
description = "configmap backed volume"
}
#main.tf
resource "kubernetes_config_map" "volume" {
for_each = var.volumes
metadata {
name = each.key
namespace = var.namespace
}
dynamic "data" {
for_each = each.value["data"]
content {
each.key = each.value
}
}
dynamic "binary_data" {
for_each = each.value["binary_data"]
content {
each.key = each.value
}
}
}
Error: Argument or block definition required
On ../../../terraform-modules/helm_install/main.tf line 45: An argument or
block definition is required here. To set an argument, use the equals sign "="
to introduce the argument value.
data and binary_data are arguments, not blocks. dynamic blocks apply only to blocks, not to arguments.
This means that you can't create multiple data and binary_data in a single kubernetes_config_map. You would have to apply for_each at the resource level:
resource "kubernetes_config_map" "volume" {
for_each = var.volumes
metadata {
name = each.key
namespace = var.namespace
}
data = each.value["data"]
binary_data = each.value["binary_data"]
}
I haven't verified the above code, thus threat it as an example only.

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.

Resources