aws_route_table_association with multiple subnets at runtime (AWS) - amazon-web-services

I am trying to create a aws_route_table_association resource for public subnets. The number of public subnets will be determined at runtime and hence the number of associations to be created.
While doing a terraform plan my code fails. Below is the source code and error i am getting. Anybody able to advise on a way to accomplish this.
// required subnets and their configurations
variable "required_subnets" {
description = "list of subnets required"
default = ["public-1a", "private-1a", "public-1b", "private-1b"]
}
#create public and provate subnets
resource "aws_subnet" "subnets" {
count = length(var.required_subnets)
vpc_id = aws_vpc.my_vpc.id
cidr_block = lookup(var.subnet_conf[var.required_subnets[count.index]], "cidr")
availability_zone = lookup(var.subnet_conf[var.required_subnets[count.index]], "availability_zone")
# enable public ip addresses in public subnet
map_public_ip_on_launch = false
tags = {
Name = var.required_subnets[count.index]
}
}
//fetch reference to public subnets
data "aws_subnets" "public_subnets" {
filter {
name = "vpc-id"
values = [data.aws_vpc.vpc.id]
}
tags = {
Name = "public-*"
}
}
#assosiate public route table with public subnet
resource "aws_route_table_association" "public" {
count = length(data.aws_subnets.public_subnets.ids)
subnet_id = data.aws_subnets.public_subnets.ids[count.index]
route_table_id = aws_route_table.my_public_route_table.id
}
the error is as below:
│ Error: Invalid count argument
│
│ on vpc.tf line 62, in resource "aws_route_table_association" "public":
│ 62: count = length(data.aws_subnets.public_subnets.ids)
│
│ The "count" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how
│ many instances will be created. To work around this, use the -target argument to first apply only the resources that the
│ count depends on.
I

If required_subnets is all you need, then there is no reason for your data.aws_subnets.public_subnets. Also it would be much better to use for_each, not count, as for_each does not depend on the order of items. Thus, you can simply your code as follows:
// required subnets and their configurations
variable "required_subnets" {
description = "list of subnets required"
default = ["public-1a", "private-1a", "public-1b", "private-1b"]
}
#create public and provate subnets
resource "aws_subnet" "subnets" {
for_each = toset(var.required_subnets)
vpc_id = aws_vpc.my_vpc.id
cidr_block = lookup(var.subnet_conf[each.key], "cidr")
availability_zone = lookup(var.subnet_conf[each.key], "availability_zone")
# enable public ip addresses in public subnet
map_public_ip_on_launch = false
tags = {
Name = each.key
}
}
#assosiate public route table with public subnet
resource "aws_route_table_association" "public" {
for_each = {for name, subnet in aws_subnet.subnets: name => subnet if length(regexall("public-", name)) > 0}
subnet_id = each.value.id
route_table_id = aws_route_table.my_public_route_table.id
}

Related

Terraform ENI Mapping error - does not fall within the subnet's address range

I am trying to map my ENI to my subnet and its throwing an error.
Because there is a for_each loop on the subnet the ENI pointing to it must also have a looped key/value added to it hence the problem
main.tf
# VPC
resource "aws_vpc" "main" {
cidr_block = local.json.vpc.cidr
tags = {
Name = "vpc"
}
}
# Subnet
resource "aws_subnet" "public" {
for_each = local.api
vpc_id = aws_vpc.main.id
cidr_block = each.value.subnet_cidr
availability_zone = each.value.subnet_az
}
# ENI
resource "aws_network_interface" "eni" {
for_each = local.api
subnet_id = aws_subnet.public[each.key].id
private_ips = ["172.16.10.100"] # Might need to add another IP
tags = {
Name = "primary_network_interface"
}
}
my locals look like this
locals {
json = jsondecode(file("API.json"))
api = merge([
for vpc in local.json : {
for subnet in vpc.subnets :
"${vpc.name}-${subnet.name}" => {
vpc_name = vpc.name
vpc_cidr = vpc.cidr
subnet_name = subnet.name
subnet_cidr = subnet.cidr
subnet_az = subnet.az
}
}
]...)
}
their output (local.api) from terraform console
{
"vpc-subnet-one" = {
"subnet_az" = "eu-central-1a"
"subnet_cidr" = "192.168.1.0/24"
"subnet_name" = "subnet-one"
"vpc_cidr" = "192.168.0.0/16"
"vpc_name" = "vpc"
}
"vpc-subnet-two" = {
"subnet_az" = "eu-central-1b"
"subnet_cidr" = "192.168.4.0/24"
"subnet_name" = "subnet-two"
"vpc_cidr" = "192.168.0.0/16"
"vpc_name" = "vpc"
}
}
error message
status code: 400, request id: 64e031e5-11ea-4f6d-a03c-9a36a1ff56af
with aws_network_interface.eni["vpc-subnet-one"],
on main.tf line 20, in resource "aws_network_interface" "eni":
20: resource "aws_network_interface" "eni" {
Error: creating EC2 Network Interface: InvalidParameterValue: Address does not fall within the subnet's address range
status code: 400, request id: 7842f089-08b4-4042-b928-7830a37ffe28
with aws_network_interface.eni["vpc-subnet-two"],
on main.tf line 20, in resource "aws_network_interface" "eni":
20: resource "aws_network_interface" "eni" {
I've followed this documenation and validated everything else is correct. I still can't seem to figure out what value should be set on subnet_id
Bonus cheeky points - I am trying to configure 2 EC2's, Should i give our eni a secondary private_ips?
The error means that your IP 172.16.10.100 is invalid for your subnet CIDR range 192.168.1.0/24 and 192.168.4.0/24. Obviously this is correct because your IP should be in the correct range. For example:
private_ips = ["192.168.1.100"] # for the first subnet
private_ips = ["192.168.4.100"] # for the second subnet

Error Calling a VPC Resource into other Resources

I deployed a VPC using a For loop in terraform
#All VPCs being deployed
resource "aws_vpc" "All_VPCs" {
for_each = var.All_VPCs
cidr_block = each.value.ip
instance_tenancy = each.value.instance_tenancy
tags = {
Name = each.key
}
}
This For loop referenced my Variable.tf file
#VPC CIDRs
variable "All_VPCs" {
type = map(any)
default = {
Dev_VPC = {
ip = "10.0.3.0/24"
instance_tenancy = "default"
}
Transit_VPC = {
ip = "10.0.4.0/23"
instance_tenancy = "default"
description = "Transit_VPC"
}
}
}
I tried to call it inside an Internet gateway resource, but it failed.
#Transit Internet Gateway
resource "aws_internet_gateway" "Transit_Internet_Gateway" {
vpc_id = var.All_VPCs.Transit_VPC
tags = {
Name = "Transit VPC Internet_Gateway"
}
}
Here is the error message
vpc_id = var.All_VPCs.Transit_VPC
│ ├
│ │ var.All_VPCs.Transit_VPC is map of string with 3 elements
│
│ Inappropriate value for attribute "vpc_id": string required.
It makes me wonder. Do I need to specify an ID when calling a Variable?
Or do I need to call the VPC resource in some way inside the Internet gateway resource, instead of calling its variable?
Okay, I just realized I simply had to refer to the VPC resource, but also a string of the Tier one value. This value is "Transit_VPC." I must also reference the VPCs ID, as this is a Resource and not a Variable.
#Transit Internet Gateway
resource "aws_internet_gateway" "Transit_Internet_Gateway" {
vpc_id = aws_vpc.All_VPCs["Transit_VPC"].id
tags = {
Name = "Transit VPC Internet_Gateway"
}
}

Terraform loop over subnet ids

I have a VPC module which creates private subnets
resource "aws_subnet" "private" {
for_each = { for index, az_name in local.az_names : index => az_name }
vpc_id = aws_vpc.network.id
cidr_block = cidrsubnet(aws_vpc.network.cidr_block, 8, each.key + 11)
availability_zone = local.az_names[each.key]
tags = {
Name = "${var.env}-private-${local.az_names[each.key]}"
Description = local.az_names[each.key]
Type = "private"
}
}
And an output
output "private_subnet" {
value = aws_subnet.private
}
Now I am trying to create an EFS module which will create a mount target for each private subnet
resource "aws_efs_mount_target" "efs-mt" {
count = length(var.private_subnet)
file_system_id = aws_efs_file_system.efs.id
subnet_id = var.private_subnet[count.index].id
security_groups = var.security_groups
}
main.tf
module "efs" {
source = "./modules/efs"
env = var.env
vpc_id = module.vpc.vpc_id
security_groups = [module.security.efs_sg_ids]
private_subnet = module.vpc.private_subnet
}
But I am getting these errors
│ Error: Invalid index
│
│ on modules/efs/main.tf line 14, in resource "aws_efs_mount_target" "efs-mt":
│ 14: subnet_id = var.private_subnet[count.index].id
│ ├────────────────
│ │ count.index is a number, known only after apply
│ │ var.private_subnet is a string, known only after apply
│
│ This value does not have any indices.
You are mixing count and for_each meta-arguments which is not a good idea. To fix that, you could do the following:
Leave the count meta-argument in the EFS resource but change the output of the VPC module
Use for_each with the EFS module as well
Even though I would suggest the second approach, I will provide both options. In the output in the VPC module, you can switch to:
output "private_subnet" {
value = values(aws_subnet.private)[*].id
}
The values built-in function [1] will return a list of subnet IDs which you can then use as the intended input for the EFS module. In this case, you would have to change the module input variable type from string to list(string):
variable "private_subnet" {
type = list(string)
description = "List of subnet IDs."
}
On the other hand, you could try switching the EFS resource to use for_each instead of count and passing it the output value of the VPC module the way it currently is:
resource "aws_efs_mount_target" "efs-mt" {
for_each = var.private_subnet
file_system_id = aws_efs_file_system.efs.id
subnet_id = each.value.id
security_groups = var.security_groups
}
In this case, you would have to change the module input variable type from string to map(any):
variable "private_subnet" {
type = map(any)
description = "Map of all the values returned by the subnet resource."
}
[1] https://www.terraform.io/language/functions/values

Attach each EIP to each Nat Gatway in Terraform

I'm creating two public subnets that will each contain a nat gateay. My code, attempts to create these nats per subnet, and then allocate the eip to each. However, since my for each starts the code block, it looks like the allocation id became us-east-* instead of the id of the eip.
Variables.tf:
variable "public_subnet_numbers" {
type = map(number)
description = "Map of AZ to a number that should be used for public subnets"
default = {
"us-east-1a" = 1
"us-east-1b" = 2
#"us-east-1c" = 3
}
}
variable "private_subnet_numbers" {
type = map(number)
description = "Map of AZ to a number that should be used for private subnets"
default = {
"us-east-1a" = 4
"us-east-1b" = 5
#"us-east-1c" = 6
}
}
variable "vpc_cidr" {
type = string
description = "The IP range to use for the VPC"
default = "192.168.0.0/16"
}
Main.tf:
resource "aws_eip" "nat" {
count = 2
vpc = true
lifecycle {
# prevent_destroy = true
}
tags = {
Name = "cf-${var.infra_env}-eip"
Project = "cf.io"
Environment = var.infra_env
VPC = aws_vpc.vpc.id
ManagedBy = "terraform"
Role = "private"
}
}
resource "aws_nat_gateway" "ngw" {
for_each = var.private_subnet_numbers
subnet_id = each.value.id #aws_subnet.public[each.key].id
allocation_id = aws_eip.nat[each.key].id
tags = {
Name = "cf-${var.infra_env}-ngw"
Project = "cf.io"
VPC = aws_vpc.vpc.id
Environment = var.infra_env
ManagedBy = "terraform"
Role = "private"
}
}
Error:
Error: Invalid index
│
│ on ../terraform/modules/networking/gateways.tf line 42, in resource "aws_nat_gateway" "ngw":
│ 42: allocation_id = aws_eip.nat[each.key].id
│ ├────────────────
│ │ aws_eip.nat is tuple with 2 elements
│ │ each.key is "us-east-1a"
│
│ The given key does not identify an element in this collection value: a number is required.
╵
╷
│ Error: Invalid index
│
│ on ../terraform/modules/networking/gateways.tf line 42, in resource "aws_nat_gateway" "ngw":
│ 42: allocation_id = aws_eip.nat[each.key].id
│ ├────────────────
│ │ aws_eip.nat is tuple with 2 elements
│ │ each.key is "us-east-1b"
│
│ The given key does not identify an element in this collection value: a number is required.
You're mixing count and for_each. The easiest way to solve this would be to use for_each in your EIP creation as well, which makes sense because you are creating an EIP for each NAT. That would also make your code work better if you decided to add another subnet later, you wouldn't need to go in and change the count from 2 to 3.
Otherwise, you need to use the index function to convert the each value to an index number.
As Mark B mentioned mixing the count and for_each is not recommended. In your current setup using exclusively for_each is the way to go based on the private_subnet_numbers variable.
In your aws_eip.nat resource change count to for_each
resource "aws_eip" "nat" {
for_each = var.private_subnet_numbers
vpc = true
}
Next in your resource aws_nat_gateway.ngw you should refer to subnet ids using each
resource "aws_nat_gateway" "ngw" {
for_each = var.private_subnet_numbers
subnet_id = aws_subnet.public[each.key].id
....
}
And the code as a whole for clarity
resource "aws_vpc" "vpc" {
... vpc configurations ...
}
resource "aws_subnet" "public" {
for_each = var.private_subnet_numbers
vpc_id = aws_vpc.vpc.id
... subnet configurations ...
}
resource "aws_eip" "nat" {
for_each = var.private_subnet_numbers
vpc = true
lifecycle {
# prevent_destroy = true
}
tags = {
Name = "cf-${var.infra_env}-eip"
Project = "cf.io"
Environment = var.infra_env
VPC = aws_vpc.vpc.id
ManagedBy = "terraform"
Role = "private"
}
}
resource "aws_nat_gateway" "ngw" {
for_each = var.private_subnet_numbers
subnet_id = aws_subnet.public[each.key].id
allocation_id = aws_eip.nat[each.key].id
tags = {
Name = "cf-${var.infra_env}-ngw"
Project = "cf.io"
VPC = aws_vpc.vpc.id
Environment = var.infra_env
ManagedBy = "terraform"
Role = "private"
}
}

Retrieve Subnet ids for map variables

I have created subnet as map variable for availability zone and CIDR block and
variable "public_subnets_list" {
type = map(any)
description = "Public Subnets"
default = {
"ap-south-1a" = "10.0.1.0/24"
"ap-south-1b" = "10.0.2.0/24"
}
}
This works fine for creating subnets under my custom VPC with below code
resource "aws_subnet" "public_subnet" {
depends_on = [
aws_vpc.terraform_vpc
]
for_each = tomap(var.public_subnets_list)
availability_zone = each.key
cidr_block = each.value
vpc_id = aws_vpc.terraform_vpc.id
tags = {
Name = "Public_Subnet_${each.key}"
}
}
How do I retrieve and display the subnet id created for respective AZs from output which i get from aws_subnet.public_subnet[*]
From the Terraform documentation, splat expressions do not work with resources that use the for_each argument.
To retrieve a list of subnet IDs using your Terraform configuration, you can do the following:
output "subnet_ids" {
value = [for subnet in aws_subnet.public_subnet : subnet.id]
}