Terraform — The Awesome Value Type “any” and Loops

Hector
AWS Tip
Published in
5 min readDec 13, 2022

--

I recently started implementing (for_each) loops on my projects, this really simplifies the creation of multiple related resources, so in order to do that I just need to send an input variable with all the information required in a map data structure and then just loop over the values.

So lets take a simple input variable to create multiple “aws secrets”

variable "secrets" {
type = map(map(string))
default = {
"vendors/vendor1_api_key" = {
description = "Contains a vendors 1 API Key"
secret_string = "CHANGE ME"
recovery_window_in_days = 7
}
"vendors/vendor2_api_key" = {
description = "Contains a vendors 2 API Key"
secret_string = "xxxxx-yyyy"
recovery_window_in_days = 10
}
"vendors/vendor3_api_key" = {
description = "Contains a vendors 3 API Key"
secret_string = "abc-123"
recovery_window_in_days = 15
}
}
}

resource "aws_secretsmanager_secret" "this" {
for_each = var.secrets # Similar to { for k, v in var.secrets : k => v }
name = each.key
kms_key_id = var.kms_key_id
description = each.value.description
recovery_window_in_days = var.recovery_window_in_days
}

resource "aws_secretsmanager_secret_version" "this" {
for_each = var.secrets # Similar to { for k, v in var.secrets : k => v }
secret_id = aws_secretsmanager_secret.this[each.key].id
secret_string = each.value.secret_string
lifecycle {
ignore_changes = [secret_string]
prevent_destroy = false
}
}

This is okay, we just iterate over all the secrets with a for_each loop, then extract the information and done.

But…

What if we want to represent a more complex data structure, something that can grow and have different types of values?

In order to explain this, let’s try to attach some VPCs into a Transit Gateway, We require the following data from the VPCs to attach: vpc_id, vpc_cidr, route_table_ids, subnet_ids, etc.

So the data structure will look something like this:

variable "tg_vpc_attachments" {
type = map
default = {
vpc_egress = {
vpc_id = "vpc-12345001"
vpc_cidr = "10.0.0.0/16"
vpc_private_route_table_ids = ["rt-123001", "rt-123002", "rt-123003"]
vpc_public_route_table_ids = ["rt-111001", "rt-112002", "rt-113003"]
subnet_ids = ["sub-001", "sub-002", "sub-003"]
}
vpc_service = {
vpc_id = "vpc-12345002"
vpc_cidr = "10.1.0.0/16"
vpc_private_route_table_ids = ["rt-223001", "rt-223002", "rt-223003"]
subnet_ids = ["sub-101", "sub-102", "sub-103"]
}
vpc_api = {
vpc_id = "vpc-12345003"
vpc_cidr = "10.2.0.0/16"
vpc_private_route_table_ids = ["rt-323001", "rt-323002", "rt-323003"]
subnet_ids = ["sub-201", "sub-202", "sub-203"]
}
vpc_application = {
vpc_id = "vpc-12345004"
vpc_cidr = "10.3.0.0/16"
vpc_private_route_table_ids = ["rt-423001", "rt-423002", "rt-423003"]
subnet_ids = ["sub-301", "sub-302", "sub-303"]
}
}
}

Unfortunatelly When we try to send this input variable we get the following error:

This default value is not compatible with the variable's type constraint: element "vpc_service": 
all map elements must have the same type.

How can we fix this?

just change the value type to “any

variable "tg_vpc_attachments" {
type = any
default = {
vpc_egress = {
vpc_id = "vpc-12345001"
vpc_cidr = "10.0.0.0/16"
vpc_private_route_table_ids = ["rt-123001", "rt-123002", "rt-123003"]
vpc_public_route_table_ids = ["rt-111001", "rt-112002", "rt-113003"]
subnet_ids = ["sub-001", "sub-002", "sub-003"]
}
...
...
...
...

output "tg_vpc_attachments" {
value = var.tg_vpc_attachments
}

The Output will look like this:

Changes to Outputs:
+ tg_vpc_attachments = {
+ vpc_api = {
+ subnet_ids = [
+ "sub-201",
+ "sub-202",
+ "sub-203",
]
+ vpc_cidr = "10.2.0.0/16"
+ vpc_id = "vpc-12345003"
+ vpc_private_route_table_ids = [
+ "rt-323001",
+ "rt-323002",
+ "rt-323003",
]
}
+ vpc_application = {
+ subnet_ids = [
+ "sub-301",
+ "sub-302",
+ "sub-303",
]
+ vpc_cidr = "10.3.0.0/16"
+ vpc_id = "vpc-12345004"
+ vpc_private_route_table_ids = [
+ "rt-423001",
+ "rt-423002",
+ "rt-423003",
]
}
+ vpc_egress = {
+ subnet_ids = [
+ "sub-001",
+ "sub-002",
+ "sub-003",
]
+ vpc_cidr = "10.0.0.0/16"
+ vpc_id = "vpc-12345001"
+ vpc_private_route_table_ids = [
+ "rt-123001",
+ "rt-123002",
+ "rt-123003",
]
+ vpc_public_route_table_ids = [
+ "rt-111001",
+ "rt-112002",
+ "rt-113003",
]
}
+ vpc_service = {
+ subnet_ids = [
+ "sub-101",
+ "sub-102",
+ "sub-103",
]
+ vpc_cidr = "10.1.0.0/16"
+ vpc_id = "vpc-12345002"
+ vpc_private_route_table_ids = [
+ "rt-223001",
+ "rt-223002",
+ "rt-223003",
]
}
}

To iterate over these items in a resource is very straight forward :

resource "null_resource" "tg_vpc_attachments" {
for_each = var.tg_vpc_attachments
triggers = {
name = each.key
}

provisioner "local-exec" {
command = "echo \"[Attaching to TG] vpc_id: ${each.value.vpc_id}, subnets: ${join(", ", each.value.subnet_ids)}, route_table_ids=${join(", ", each.value.subnet_ids)}\""
}
}

Output:

Plan: 4 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.

Enter a value: yes

null_resource.tg_vpc_attachments["vpc_egress"]: Creating...
null_resource.tg_vpc_attachments["vpc_service"]: Creating...
null_resource.tg_vpc_attachments["vpc_api"]: Creating...
null_resource.tg_vpc_attachments["vpc_application"]: Creating...
null_resource.tg_vpc_attachments["vpc_egress"]: Provisioning with 'local-exec'...
null_resource.tg_vpc_attachments["vpc_application"]: Provisioning with 'local-exec'...
null_resource.tg_vpc_attachments["vpc_api"]: Provisioning with 'local-exec'...
null_resource.tg_vpc_attachments["vpc_service"]: Provisioning with 'local-exec'...
null_resource.tg_vpc_attachments["vpc_egress"] (local-exec): Executing: ["/bin/sh" "-c" "echo \"[Attaching to TG] vpc_id: vpc-12345001, subnets: sub-001, sub-002, sub-003, route_table_ids=sub-001, sub-002, sub-003\""]
null_resource.tg_vpc_attachments["vpc_api"] (local-exec): Executing: ["/bin/sh" "-c" "echo \"[Attaching to TG] vpc_id: vpc-12345003, subnets: sub-201, sub-202, sub-203, route_table_ids=sub-201, sub-202, sub-203\""]
null_resource.tg_vpc_attachments["vpc_application"] (local-exec): Executing: ["/bin/sh" "-c" "echo \"[Attaching to TG] vpc_id: vpc-12345004, subnets: sub-301, sub-302, sub-303, route_table_ids=sub-301, sub-302, sub-303\""]
null_resource.tg_vpc_attachments["vpc_service"] (local-exec): Executing: ["/bin/sh" "-c" "echo \"[Attaching to TG] vpc_id: vpc-12345002, subnets: sub-101, sub-102, sub-103, route_table_ids=sub-101, sub-102, sub-103\""]
null_resource.tg_vpc_attachments["vpc_api"] (local-exec): [Attaching to TG] vpc_id: vpc-12345003, subnets: sub-201, sub-202, sub-203, route_table_ids=sub-201, sub-202, sub-203
null_resource.tg_vpc_attachments["vpc_application"] (local-exec): [Attaching to TG] vpc_id: vpc-12345004, subnets: sub-301, sub-302, sub-303, route_table_ids=sub-301, sub-302, sub-303
null_resource.tg_vpc_attachments["vpc_application"]: Creation complete after 0s [id=1765475333981246219]
null_resource.tg_vpc_attachments["vpc_api"]: Creation complete after 0s [id=1759399662161796730]
null_resource.tg_vpc_attachments["vpc_service"] (local-exec): [Attaching to TG] vpc_id: vpc-12345002, subnets: sub-101, sub-102, sub-103, route_table_ids=sub-101, sub-102, sub-103
null_resource.tg_vpc_attachments["vpc_egress"] (local-exec): [Attaching to TG] vpc_id: vpc-12345001, subnets: sub-001, sub-002, sub-003, route_table_ids=sub-001, sub-002, sub-003
null_resource.tg_vpc_attachments["vpc_service"]: Creation complete after 0s [id=6525455966425835196]
null_resource.tg_vpc_attachments["vpc_egress"]: Creation complete after 0s [id=6051638192847399049]

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

So now let’s implement this for real in a “aws_ec2_transit_gateway_vpc_attachment” resource

resource "aws_ec2_transit_gateway_vpc_attachment" "this" {
for_each = var.tg_vpc_attachments

transit_gateway_id = "tg-111111111"
vpc_id = each.value.vpc_id
subnet_ids = each.value.subnet_ids

dns_support = try(var.dns_support, true) ? "enable" : "disable"
ipv6_support = try(var.ipv6_support, false) ? "enable" : "disable"
appliance_mode_support = try(var.appliance_mode_support, false) ? "enable" : "disable"
transit_gateway_default_route_table_association = try(var.transit_gateway_default_route_table_association, true)
transit_gateway_default_route_table_propagation = try(var.transit_gateway_default_route_table_propagation, true)

tags = merge(
var.tags,
{ Name = format("%s-%s", each.key, var.name) })
}

--

--