Terraform how to fix attribute "route": set of object required - amazon-web-services

I have this terraform module route_table.tf I need to use. It looks like below:
resource "aws_route_table" "aws_route_table" {
# route {
# cidr_block = "0.0.0.0/0"
# gateway_id = var.GATEWAY_ID
# }
route = var.ROUTE
tags = var.ROUTE_TABLE_TAGS
vpc_id = var.VPC_ID
}
and I have defined the variable ROUTE as below in the inputs.tf:
variable "ROUTE" {
type = object({ cidr_block=string, gateway_id=string })
}
And I am passing those values in the main.tf as below:
module "route_tables_public" {
source = "./modules/route_tables"
ROUTE = {
cidr_block = "0.0.0.0/0"
gateway_id = var.GATEWAY_ID
}
ROUTE_TABLE_TAGS = { "Name" : "mlb-rt-public" , "Project" : "mlb"}
VPC_ID = module.ecs_vpc.vpc_id
}
But I am getting this error:
Inappropriate value for attribute "route": set of object required.
Can someone help me on this?

Your var.ROUTE is a single object, but it should be list of objects. So you can try:
variable "ROUTE" {
type = list(object({ cidr_block=string, gateway_id=string }))
}
and then
module "route_tables_public" {
source = "./modules/route_tables"
ROUTE = [{
cidr_block = "0.0.0.0/0"
gateway_id = var.GATEWAY_ID
}]
ROUTE_TABLE_TAGS = { "Name" : "mlb-rt-public" , "Project" : "mlb"}
VPC_ID = module.ecs_vpc.vpc_id
}
UPDATE
Your aws_route_table should be:
resource "aws_route_table" "aws_route_table" {
dynamic "route" {
for_each = var.ROUTE
content {
cidr_block = route.value.cidr_block
gateway_id = route.value.gateway_id
}
}
tags = var.ROUTE_TABLE_TAGS
vpc_id = var.VPC_ID
}

I would recommend against directly assigning variable values to complex resource type arguments like this, because if the schema of the route argument were to grow to include additional attributes in the future then your module would fail validation.
Instead, better to let the variable's type be independent of the route block schema and translate explicitly between them, which should then allow this to work as long as a new version of the provider doesn't introduce a new required argument for that block.
variable "route" {
type = object({
cidr_block = string
gateway_id = string
})
}
resource "aws_route_table" "aws_route_table" {
# ...
route {
cidr_block = var.route.cidr_block
gateway_id = var.route.gateway_id
}
}
This can work because it's constructing a new object that conforms to the route block schema using two attributes of your object, whereas your example fails because you tried to assign the input object directly to that argument, which would require that you match the target type exactly.
Please note that naming variables all in uppercase is not idiomatic Terraform style, so in the above example I renamed the variable to be route instead. ROUTE would also work, but would be an unusual way to name a Terraform variable.

Related

Problems in creating aws route table using terraform

Running terraform v1.0.9 with AWS plugin v3.63.0 on a mac
Following hashicorp instructions for creating a route table (https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table), but getting the following error:
Error: Incorrect attribute value type
...
Inappropriate value for attribute "route": element 0: attributes "carrier_gateway_id", "destination_prefix_list_id", "egress_only_gateway_id",
│ "instance_id", "ipv6_cidr_block", "local_gateway_id", "nat_gateway_id", "network_interface_id", "transit_gateway_id", "vpc_endpoint_id", and
│ "vpc_peering_connection_id" are required.
Here is my main.tf:
provider "aws" {
region = "us-east-1"
}
resource "aws_vpc" "my-test-vpc" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "my-test-vpc"
}
}
resource "aws_internet_gateway" "gw" {
vpc_id = aws_vpc.my-test-vpc.id
}
resource "aws_route_table" "prod-route-table" {
vpc_id = aws_vpc.my-test-vpc.id
route = [
{
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw.id
}
]
tags = {
Name = "production"
}
}
I have run into something similar, without getting to the bottom of the root cause, so this is not the best possible answer, but moving the route out into its own explicit resource works for me:
resource "aws_route_table" "prod-route-table" {
vpc_id = aws_vpc.my-test-vpc.id
tags = {
Name = "production"
}
}
resource "aws_route" "prod-route-igw" {
route_table_id = aws_route_table.prod-route-table.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw.id
depends_on = [aws_route_table.prod-route-table]
}

AWS Terraform error: "expected cidr_block to contain a valid Value, got: with err: invalid CIDR address:"

Practicing with AWS security groups in Terraform. I'm moderately experienced with TF but SGs have been tricky. I have the following main.tf:
provider "aws" {
region = "us-east-1"
}
resource "aws_vpc" "test" {
cidr_block = var.cidr_block
}
resource "aws_security_group" "sg" {
name = var.name
vpc_id = aws_vpc.test.id
description = var.description
}
resource "aws_security_group_rule" "ingress_rule" {
type = "ingress"
from_port = var.from_port
to_port = var.to_port
protocol = var.protocol
cidr_blocks = var.cidr_blocks
security_group_id = aws_security_group.sg.id
}
I have variables for the above in a variables.tf file (can show on request), and my modules are below:
provider "aws" {
region = "us-east-1"
}
module "vpc" {
source = "../"
cidr_block = "10.0.0.0/16"
}
module "TestSG1" {
source = "../"
name = "Test"
description = "Test"
type = "ingress"
from_port = 0
to_port = 65535
protocol = "tcp"
cidr_blocks = ["192.168.0.0/16"]
}
When run Terraform Plan on the above configuration, I get this error:
Error: expected cidr_block to contain a valid Value, got: with err:
invalid CIDR address:
on ..\vpc.tf line 6, in resource "aws_vpc" "test":
6: cidr_block = var.cidr_block
Now, if I REMOVE the cidr_block line from the vpc module and instead hardcode that same cidr_block value into main.tf, like so:
resource "aws_vpc" "test" {
cidr_block = "10.0.0.0/16"
}
....then I can apply the code no problem, the two modules deploy successfully. So something only goes wrong when I insert the cidr_block line into the VPC module. If I run the VPC module in isolation - apart from the SG resources and module - it runs fine with the cidr_block variable in the module aws well. Something is wrong in the interplay between the two. Exhausted my research so turning to the Stackoverflow brain trust. Any ideas?
UPDATE: Sharing my variables too, for good measure.
variable "cidr_block" {
description = ""
type = string
default = ""
}
variable "name" {
description = ""
type = string
default = ""
}
variable "description" {
description = ""
type = string
default = ""
}
variable "type" {
description = ""
type = string
default = ""
}
variable "from_port" {
description = ""
type = number
default = 0
}
variable "to_port" {
description = ""
type = number
default = 0
}
variable "protocol" {
description = ""
type = string
default = ""
}
variable "cidr_blocks" {
description = ""
type = list(string)
default = []
}
It looks to me that you are invoking the same module (code) twice with different parameters:
once as module vpc
and one more time as module TestSG1
As you have default = "" set for more most of the vars it's taking the default value for cidr_block you have in variables.tf while invoking the module as TestSG1, and as it's empty you are seeing:
Error: expected cidr_block to contain a valid Value, got: with err: invalid CIDR address:=.
In order to fix it, you either need to invoke the module only once by passing all the required parameters properly or separating the VPC creation from the security group creation in a separate module.

Terrafrom dynamic block with dynamic content

I am trying to create a terraform module for aws_route_table creation, here is an example of this resource definition:
resource "aws_route_table" "example" {
vpc_id = aws_vpc.example.id
route {
cidr_block = "10.0.1.0/24"
gateway_id = aws_internet_gateway.example.id
}
route {
ipv6_cidr_block = "::/0"
egress_only_gateway_id = aws_egress_only_internet_gateway.example.id
}
tags = {
Name = "example"
}
}
I am trying to make it more dynamic by using dynamic blocks. The problem is that I always have to define the keys in content block
resource "aws_route_table" "example" {
...
dynamic "route" {
for_each = var.route
content {
cidr_block = route.value.cidr_block
gateway_id = route.value.gateway_id
}
}
...
}
So in this case, I will need to write two dynamic blocks, one for the content with cidr_block and gateway_id and one for the content with ipv6_cidr_block and egress_only_gateway_id.
Is there any way to do this without defining keys explicitly. Something like this:
dynamic "route" {
for_each = var.route
content {
var.route.map
}
}
Yes, you can create route dynamically, because block route acts as Attributes as Blocks. So you can do (example)
# define all your routes in a variable (example content)
variable "routes" {
default = [
{
cidr_block = "0.0.0.0/0"
gateway_id = "igw-0377483faa64bf010"
},
{
cidr_block = "172.31.0.0/20"
instance_id = "i-043fc97db72ad1b59"
}
]
}
# need to provide default values (null) for all possibilities
# in route
locals {
routes_helper = [
for route in var.routes: merge({
carrier_gateway_id = null
destination_prefix_list_id = null
egress_only_gateway_id = null
ipv6_cidr_block = null
local_gateway_id = null
nat_gateway_id = null
network_interface_id = null
transit_gateway_id = null
vpc_endpoint_id = null
instance_id = null
gateway_id = null
vpc_peering_connection_id = null
}, route)
]
}
resource "aws_route_table" "example" {
vpc_id = aws_vpc.example.id
# route can be attribute, instead of blocks
route = local.routes_helper
tags = {
Name = "example"
}
}
Docs do not recommend to use that in general, but I think route is a good example where this would be acceptable.
The general answer for a block type that doesn't employ the "Attributes as Blocks" backward-compatibility shim Marcin mentioned is to rely on the fact that for Terraform provider arguments there is no difference in meaning between omitting an argument or setting it explicitly to null.
That means that you can write an expression to decide whether to populate a particular argument or not, by making it return an explicit null in the appropriate cases. For example:
dynamic "route" {
for_each = var.route
content {
cidr_block = try(route.value.cidr_block, null)
gateway_id = try(route.value.gateway_id, null)
ipv6_cidr_block = try(route.value.ipv6_cidr_block, null)
# ...
}
}
A nested block like this is more like a fixed data structure than a collection, so there isn't any syntax to construct it dynamically.
With that said, in this particular case route is, as Marcin noted, actually really just an attribute of an object type, and Terraform Core is allowing to use it with block syntax as a concession to backward compatibility with older Terraform versions. For that reason, there's not really any significant difference between these two approaches in practice, but the "Attributes as Blocks" mechanism is only for certain situations where providers were design to assume Terraform v0.11 validation bugs and so the conditional null approach I described above is the more general answer, which should work for normal nested blocks too.

Terraform conditional for AWS route tables

Using Terraform, I need to create a route table for a public subnet and private subnet. The resource for a 'public' route table is using gateway_id
resource "aws_route_table" "rt-public" {
route {
cidr_block = var.cidr_block
gateway_id = var.gateway_id
}
}
The 'private' route table is using nat_gateway_id:
resource "aws_route_table" "rt-private" {
route {
cidr_block = var.cidr_block
nat_gateway_id = var.gateway_id
}
}
I pass from the main.tf file a variable var.tier that indicates whether I need a 'public' or 'private' route table.
I tried the following (in the module):
resource "aws_route_table" "rt-public" {
count = var.tier == "Public" ? 1 : 0
route {
cidr_block = var.cidr_block
gateway_id = var.gateway_id
}
}
resource "aws_route_table" "rt-private" {
count = var.tier == "Private" ? 1 : 0
route {
cidr_block = var.cidr_block
nat_gateway_id = var.gateway_id
}
}
This works. But in the main.tf I also need to access the route table id. So I created an outputs.tf file as follows:
output "aws_rt_public_id" {
value = aws_route_table.rt-public.id
}
output "aws_rt_private_id" {
value = aws_route_table.rt-private.id
}
The issue is that when a 'public' route table is created, then the 'private' output is empty. I get the error message: aws_route_table.rt-private is empty tuple. The given key does not identify an element in this collection value.
Similar error message for the public version.
How can this be fixed using conditionals?
You can also use your condition, and return only output that is valid:
output "aws_rt_public_id" {
value = var.tier == "Public" ? aws_route_table.rt-public[0].id : null
}
output "aws_rt_private_id" {
value = var.tier == "Private" ? aws_route_table.rt-private[0].id : null
}
In the above the null, when chosen, will result in a given output removed.

Terraform - how to use for_each loop on a list of objects to create resources

I have an object containing the list of subnets I want to create.
variable "subnet-map" {
default = {
ec2 = [
{
cidr_block = "10.0.1.0/24"
availability_zone = "eu-west-1a"
}
],
lambda = [
{
cidr_block = "10.0.5.0/24"
availability_zone = "eu-west-1a"
},
{
cidr_block = "10.0.6.0/24"
availability_zone = "eu-west-1b"
},
{
cidr_block = "10.0.7.0/24"
availability_zone = "eu-west-1c"
}
],
secrets = [
{
cidr_block = "10.0.8.0/24"
availability_zone = "eu-west-1a"
},
{
cidr_block = "10.0.9.0/24"
availability_zone = "eu-west-1b"
},
{
cidr_block = "10.0.10.0/24"
availability_zone = "eu-west-1c"
}
],
rds = [
{
cidr_block = "10.0.11.0/24"
availability_zone = "eu-west-1a"
},
{
cidr_block = "10.0.12.0/24"
availability_zone = "eu-west-1b"
},
{
cidr_block = "10.0.13.0/24"
availability_zone = "eu-west-1c"
}
]
}
}
Earlier I was using the count loop construct. So I used to flatten the above structure into a list of objects
locals {
subnets = flatten([
for resource in keys(var.subnet-map) : [
for subnet in var.subnet-map[resource] : {
resource = resource
cidr_block = subnet.cidr_block
availability_zone = subnet.availability_zone
}
]
])
}
And then I would create the resources by doing
resource "aws_subnet" "aws-subnets" {
count = length(local.subnets)
vpc_id = aws_vpc.aws-vpc.id
cidr_block = local.subnets[count.index].cidr_block
availability_zone = local.subnets[count.index].availability_zone
tags = {
Name = "subnet-${local.subnets[count.index].resource}-${local.subnets[count.index].availability_zone}"
}
}
Now I want to use the for_each loop. But I cannot figure out how to do it. This is what I've done so far.
resource "aws_subnet" "subnets-dev" {
for_each = var.subnet-map
vpc_id = aws_vpc.vpc-dev.id
cidr_block = each.value.cidr_block
availability_zone = each.value.availability_zone
tags = {
Name = "subnet-dev-${each.value.resource}-${each.value.availability_zone}"
environment = "dev"
}
}
But it keeps giving an error saying
Error: Unsupported attribute
on vpc/main.tf line 93, in resource "aws_subnet" "subnets-dev":
93: Name = "subnet-dev-${each.value.resource}-${each.value.availability_zone}"
|----------------
| each.value is tuple with 3 elements
This value does not have any attributes.
How could I fix this?
I'm not sure I fully follow all of what you tried here because your initial snippet of var.subnet-map shows it being a map of maps of lists of objects, but later on when you used for_each = var.subnet-map it seems to have treated it as a map of lists instead. Did you remove that extra level of maps (the "default" key) before trying for_each here?
Working with your original definition of variable "subnet-map", your first step with for_each will be similar to what you did with count: you need to flatten the structure, this time into a map of objects rather than a list of objects. The easiest way to get there is to derive a map from your existing flattened list:
locals {
subnets = flatten([
for resource in keys(var.subnet-map) : [
for subnet in var.subnet-map[resource] : {
resource = resource
cidr_block = subnet.cidr_block
availability_zone = subnet.availability_zone
}
]
])
subnets_map = {
for s in local.subnets: "${s.resource}:${s.availability_zone}" => s
}
}
Here I assumed that your "resource" string and your availability zone together are a suitable unique identifier for a subnet. If not, you can adjust the "${s.resource}:${s.availability_zone}" part to whatever unique key you want to use for these.
Now you can use the flattened map as the for_each map:
resource "aws_subnet" "subnets-dev" {
for_each = local.subnets_map
vpc_id = aws_vpc.vpc-dev.id
cidr_block = each.value.cidr_block
availability_zone = each.value.availability_zone
tags = {
Name = "subnet-dev-${each.value.resource}-${each.value.availability_zone}"
environment = "dev"
}
}
This will give you instances with addresses like aws_subnet.subnets-dev["ec2:eu-west-1a"].
Note that if you are migrating from count with existing subnets that you wish to retain, you'll need to also do a one-time migration step to tell Terraform which indexes from the existing state correspond to which keys in the new configuration.
For example, if (and only if) index 0 was previously the one for ec2 in eu-west-1a, the migration command for that one would be:
terraform state mv 'aws_subnet.subnets-dev[0]' 'aws_subnet.subnets-dev["ec2:eu-west-1a"]'
If you're not sure how they correlate, you can run terraform plan after adding for_each and look at the instances that Terraform is planning to destroy. If you work through each one of those in turn, taking the address Terraform currently knows along with the resource and availability zone names shown in the Name tag, you can migrate each of them to its new address so that Terraform will no longer think you're asking for it to destroy the numbered instances and replace them with named ones.