Value of 'count' cannot be computed in Terraform - terraform

Here I'm trying to create one subnet per availability zone and then associate the route table with each of them.
locals {
aws_region = "${var.aws_regions[var.profile]}"
base_name = "${var.product}-${local.aws_region}"
aws_avzones = {
pro = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
dev = ["eu-west-2a", "eu-west-2b", "eu-west-2c"]
}
}
# ---
# Create VPC
resource "aws_vpc" "default" {
cidr_block = "${var.vpc_cidr_block}"
tags = {
Name = "${local.base_name}-vpc"
}
}
# ---
# Create public subnets - each in a different AZ
resource "aws_subnet" "public" {
count = "${length(local.aws_avzones[var.profile])}"
vpc_id = "${aws_vpc.default.id}"
cidr_block = "${cidrsubnet(var.vpc_cidr_block, 8, count.index)}"
availability_zone = "${element(local.aws_avzones[var.profile], count.index)}"
map_public_ip_on_launch = 1
tags = {
"Name" = "Public subnet - ${element(local.aws_avzones[var.profile], count.index)}"
}
}
# ---
# Create Internet gateway for inbound-outbound connections
resource "aws_internet_gateway" "default" {
vpc_id = "${aws_vpc.default.id}"
tags = {
"Name" = "${local.base_name}-igw"
}
}
# ---
# Create Internet gateway routes table
resource "aws_route_table" "pub" {
vpc_id = "${aws_vpc.default.id}"
route {
cidr_block = "0.0.0.0/0"
gateway_id = "${aws_internet_gateway.default.id}"
}
tags = {
Name = "${local.base_name}-rtb-igw"
}
}
# ---
# Associate public subnets with the public route table
resource "aws_route_table_association" "pub" {
count = "${length(aws_subnet.public.*.id)}"
subnet_id = "${element(aws_subnet.public.*.id, count.index)}"
route_table_id = "${aws_route_table.pub.id}"
}
Unfortunately terraform plan renders an error:
aws_route_table_association.pub: aws_route_table_association.pub: value of 'count' cannot be computed
Why it cannot be computed? Terraform did not complain about that when the infra. was all up and running, I discovered this error only after the destruction when attempting to to recreate the infra.
Currently my workaround is to comment out all the aws_route_table_association blocks, then terraform apply, uncomment and then finish the job. Obviously this is very far from ideal.
BTW, I also tried the explicit dependency declaration like so:
resource "aws_route_table_association" "pub" {
count = "${length(aws_subnet.public.*.id)}"
subnet_id = "${element(aws_subnet.public.*.id, count.index)}"
route_table_id = "${aws_route_table.pub.id}"
depends_on = ["aws_subnet.public"]
}
But it didn't help.
$ terraform --version
Terraform v0.11.11
+ provider.aws v1.52.0

Related

Referencing a created subnet and associate it with a given EIP for a NLB (aws provider)

I'm trying to parametrize the creation of a NLB, and provision in the same plan the necessary public subnets.
The subnets are specified as a variable of the plan:
variable "nlb_public_subnets" {
type = list(object({
name = string
network_number = number
availability_zone = string
elastic_ip = string
}))
default = [
{
name = "sftp_sub_A"
network_number = 1
availability_zone = "eu-west-1a"
elastic_ip = "X.Y.Z.T"
},
{
name = "sftp_sub_B"
network_number = 2
availability_zone = "eu-west-1b"
elastic_ip = "XX.YY.ZZ.TT"
}
]
}
variable "common_tags" {
description = "A map containing the common tags to apply to all resources"
type = map(string)
default = {}
}
locals {
vpc_id = "dummy"
base_cidr = "10.85.23.0/24"
publicSubnets = { for s in var.nlb_public_subnets :
s.name => {
name = s.name
cidr_block = cidrsubnet(var.base_public_subnet_cidr_block, 6,
s.network_number )
availability_zone = s.availability_zone
elastic_ip = s.elastic_ip
}
}
}
I'm specifying a name, a network number (to compute the cidr block), an availability zone, and an elastic IP to map to when creating the NLB.
Here I'm creating the subnets:
#Comment added after solution was given
#This will result in a Map indexed by subnet.name provided in var.nlb_public_subnets
resource "aws_subnet" "sftp_nlb_subnets" {
for_each = { for subnet in local.publicSubnets :
subnet.name => subnet
}
cidr_block = each.value.cidr_block
vpc_id = local.vpc_id
availability_zone = each.value.availability_zone
tags = {
Name = each.key
Visibility = "public"
Purpose = "NLB"
}
}
Now I need to create my NLB, and this is where I'm struggling on how to associate the freshly created subnets with the Elastic IP provided in the configuration:
resource "aws_lb" "sftp" {
name = var.name
internal = false
load_balancer_type = "network"
subnets = [for subnet in aws_subnet.sftp_nlb_subnets: subnet.id]
enable_deletion_protection = true
tags = merge(var.common_tags,{
Name=var.name
})
dynamic "subnet_mapping" {
for_each = aws_subnet.sftp_nlb_subnets
content {
subnet_id = subnet_mapping.value.id
allocation_id = ????Help???
}
}
}
Could I somehow look up the configuration object with the help of the subnet name in the tags?
UPDATE1
Updated the dynamic block, as it had a typo.
UPDATE2
#tmatilai nailed the answer!
Here's the modified aws_lb block:
#
#This will result in a Map indexed by subnet.name provided in var.nlb_public_subnets
data "aws_eip" "nlb" {
for_each = local.publicSubnets
public_ip = each.value.elastic_ip
}
resource "aws_lb" "sftp" {
name = var.name
internal = false
load_balancer_type = "network"
subnets = [for subnet in aws_subnet.sftp_nlb_subnets : subnet.id]
enable_deletion_protection = true
tags = merge(var.common_tags, {
Name = var.name
})
dynamic "subnet_mapping" {
#subnet_mapping.key will contain subnet.name, so we can use it to access the Map data.aws_eip.nlb (also indexed by subnet.name) to get the eip allocation_id
for_each = aws_subnet.sftp_nlb_subnets
content {
subnet_id = subnet_mapping.value.id
allocation_id = data.aws_eip.nlb[subnet_mapping.key].id
}
}
}
The trick is to realize that both aws_subnet.sftp_nlb_subnets and data.aws_eip.nlb are a Map, indexed by the key of local.publicSubnets. This allows us to use this common key (the subnet name) in the map aws_subnet.sftp to look up information in the data (data.aws_eip.nlb) obtained from the original input, local.publicSubnets.
Thanks. This is a neat trick.
Passing the IP address of the elastic IPs sounds strange. If you create the EIPs elsewhere, why not pass the (allocation) ID of them instead?
But with this setup, you can get the allocation ID with the aws_eip data source:
data "aws_eip" "nlb" {
for_each = local.publicSubnets
public_ip = each.value.elastic_ip
}
resource "aws_lb" "sftp" {
# ...
dynamic "subnet_mapping" {
for_each = aws_subnet.sftp_nlb_subnets
content {
subnet_id = subnet_mapping.value.id
allocation_id = data.aws_eip.nlb[subnet_mapping.key].id
}
}
}
But maybe it would make more sense to create the EIPs also here. For example something like this:
resource "aws_eip" "nlb" {
for_each = local.publicSubnets
vpc = true
}
resource "aws_lb" "sftp" {
# ...
dynamic "subnet_mapping" {
for_each = aws_subnet.sftp_nlb_subnets
content {
subnet_id = subnet_mapping.value.id
allocation_id = aws_eip.nlb[subnet_mapping.key].id
}
}
}

terraform for_each index for NAT GW with multiple subnets

I need to setup multiple private subnets at AWS per account and I need to have only one NAT GW per Account and traffic routed to this. The problem I guess is that the values are a map without an index. As I remember with count you have an index which can be simply accessed subnet_id = aws_subnet.private[0].id But I can't change the current setup. I need to create an idnex out of this map.
I have a yaml file with this values:
aws:
- accounts: ciss-goesaws-test
private_subnets:
-
az: eu-central-1a
short: a
cidr: 10.44.4.96/27
-
az: eu-central-1b
short: b
cidr: 10.44.5.128/27
-
az: eu-central-1c
short: c
cidr: 10.44.6.160/27
I have the following terraform code. But this creates one NAT GW per subnet. I need to NAT GW to be created in only one of the subnets.
locals {
private = flatten([
for a in var.aws : [
for ps in a.private_subnets : {
accounts = a.accounts
az = ps.az
cidr = ps.cidr
short = ps.short
}
]
])
}
resource "aws_eip" "this" {
vpc = true
}
resource "aws_nat_gateway" "this" {
for_each = {
for cidr_block in local.private : cidr_block.cidr => cidr_block
}
allocation_id = aws_eip.this.id
subnet_id = aws_subnet.private[each.key].id
}
resource "aws_subnet" "private" {
for_each = {
for cidr_block in local.private : cidr_block.cidr => cidr_block
}
availability_zone = each.value.az
cidr_block = each.value.cidr
vpc_id = aws_vpc.this.id
tags = {
Name = "${each.value.accounts}-private-${each.value.short}"
}
}
resource "aws_route_table" "private" {
for_each = {
for cidr_block in local.private : cidr_block.cidr => cidr_block
}
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.this[each.key].id
}
}
resource "aws_route_table_association" "private" {
for_each = {
for cidr_block in local.private : cidr_block.cidr => cidr_block
}
subnet_id = aws_subnet.private[each.key].id
route_table_id = aws_route_table.private[each.key].id
}
As the CIDRs are used as index for the subnet resource collection, you have to select one (maybe first) for the NAT GW.
For example:
locals {
nat_gw_subnet_cidr = var.aws[0].private_subnets[0].cidr
}
resource "aws_nat_gateway" "this" {
allocation_id = aws_eip.this.id
subnet_id = aws_subnet.private[local.nat_gw_subnet_cidr].id
}
# `aws_subnet` resource as in the question
# One route table should be enough, as all subnets share the same GW
resource "aws_route_table" "private" {
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.this.id
}
# No route for the VPC?
}
resource "aws_route_table_association" "private" {
# Simpler to use the subnets as index
for_each = aws_subnet.private
subnet_id = each.value.id
route_table_id = aws_route_table.private.id
}

Modules and Variables carrying over tfvar values

I am fairly new to TF and I have written some basic code. Enough to get a vpc up and running and add some subnets and deploy a simple ec2. I am starting to get to the point of wanting to use modules. I struggle with the "keeping generic" thing so I can reuse them over and over. I do not understand how values get passed into modules. For instance I have a module that deploys a vpc, within the same project I have a module that deploys a vpc endpoint. The questions becomes how do i get the value of the vpc_id created with vpc module into the vpc endpoint module? Does anyone have an example of this?
main.tf
provider "aws" {
region = var.aws_region
}
/*Module for VPC creation*/
module "vpc" {
source = "./modules/vpc"
vpc_cidr = var.vpc_cidr
environment = var.environment
tnt_public_subnets_cidr = var.tnt_public_subnets_cidr
availability_zones = var.availability_zones
}
/*Module for EC2 Webserver creation*/
module "webserver" {
source = "./modules/ec2/webserver"
count = var.instance_count
environment = var.environment
subnet_id = module.vpc.tnt_public_subnets_cidr.id
}
/*Module for VPC endpoint creation*/
module "s3-vpce"{
source = "git::https://github.com/tn-sts-cloudtn/sts-terraform-
modules.git//s3-vpce-module/modules//s3-vpce"
vpc_id = module.vpc.vpc_id
}
VPC Module TF File:
/*==== The VPC ======*/
resource "aws_vpc" "vpc" {
cidr_block = var.vpc_cidr
assign_generated_ipv6_cidr_block = true
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}_vpc"
Environment = var.environment
}
}
/*==== Internet Gateway for Public Subnets ======*/
/* Internet gateway for the public subnet */
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.environment}_igw"
Environment = var.environment
}
}
/* Elastic IP for NAT
resource "aws_eip" "instance_eip" {
count = 1
vpc = true
depends_on = [aws_internet_gateway.tnt_igw]
tags ={
Name = "sts_net_infra-mgmt_eip${count.index + 1}"
Environment = var.environment
}
}*/
/* Public subnet */
resource "aws_subnet" "public_subnets_cidr" {
vpc_id = aws_vpc.vpc.id
count = length(var.availability_zones)
cidr_block = var.public_subnets_cidr [count.index]
availability_zone = element(var.availability_zones, count.index)
map_public_ip_on_launch = true
tags ={
Name = "${var.environment}_mgmt_subnet_${count.index + 1}"
Environment = var.environment
}
}
/* Private subnet
resource "aws_subnet" "tnt_private_subnet" {
vpc_id = aws_vpc.tnt_vpc.id
count = length(var.tnt_private_subnets_cidr)
cidr_block = var.tnt_public_subnets_cidr [count.index]
availability_zone = element(var.availability_zones, count.index)
map_public_ip_on_launch = false
tags = {
#Name = var.environment-private-subnet
Environment = var.environment
}
*/
/* Routing table for private subnet
resource "aws_route_table" "tnt_private_rtb" {
vpc_id = aws_vpc.tnt_vpc.id
tags = {
Name = var.environment_private_route_table
Environment = var.environment
}
}*/
/* Routing table for public subnet */
resource "aws_route_table" "public_rtb" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.environment}_public_route_table"
Environment = var.environment
}
}
resource "aws_route" "public_internet_gateway" {
route_table_id = aws_route_table.public_rtb.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
/* Route table associations */
resource "aws_route_table_association" "public" {
count = length(var.public_subnets_cidr)
subnet_id = element(aws_subnet.public_subnets_cidr.*.id, count.index)
route_table_id = aws_route_table.public_rtb.id
}
So I need to output the VPC ID for the VPC endpoint, so I tried to use an output.
output.tf
output "vpc_cidr" {
value = aws_vpc.tnt_vpc.id
}
output "tnt_public_subnets_cidr"{
value = aws_subnet.tnt_public_subnets_cidr.*.id
}
output "vpc_id" {
description = "The ID of the VPC"
value = aws_vpc.vpc.id
}
output "vpc_arn" {
description = "The ARN of the VPC"
value = concat(aws_vpc.tnt_vpc.*.arn, [""])[0]
}
I know I am doing it incorrectly, but I am struggling to understand how outputs flow from module to module.
You can use one moule output into onether module by defining output in each module.

Terraform template for AWS route table

I have created a route table with routing rules that refers to an existing internet gateway (IGW) and the route table is associated to the a new VPC created via TF template. However same IGW is already attached to another VPC. When I apply template it throws the following error,
Error: Error creating route: InvalidParameterValue: route table "X" and network gateway "Y" belong to different networks
status code: 400, request id: ab91c2ab-ef1e-4905-8a78-b6759bc1e250
Is this because an internet gateway can be attached only to a single VPC and has to reside within the same VPC? Or is this error caused due to any other reason?
try below code with terraform.
it has VPC, IGW, Subnets, Route tables and NAT gateway.
and it works well.
variable "region" {
default = "us-east-1"
}
variable "service_name" {
default = "demo-service"
}
locals {
public_subnets = {
"${var.region}a" = "10.10.101.0/24"
"${var.region}b" = "10.10.102.0/24"
"${var.region}c" = "10.10.103.0/24"
}
private_subnets = {
"${var.region}a" = "10.10.201.0/24"
"${var.region}b" = "10.10.202.0/24"
"${var.region}c" = "10.10.203.0/24"
}
}
resource "aws_vpc" "this" {
cidr_block = "10.10.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.service_name}-vpc"
}
}
resource "aws_internet_gateway" "this" {
vpc_id = "${aws_vpc.this.id}"
tags = {
Name = "${var.service_name}-internet-gateway"
}
}
resource "aws_subnet" "public" {
count = "${length(local.public_subnets)}"
cidr_block = "${element(values(local.public_subnets), count.index)}"
vpc_id = "${aws_vpc.this.id}"
map_public_ip_on_launch = true
availability_zone = "${element(keys(local.public_subnets), count.index)}"
tags = {
Name = "${var.service_name}-service-public"
}
}
resource "aws_subnet" "private" {
count = "${length(local.private_subnets)}"
cidr_block = "${element(values(local.private_subnets), count.index)}"
vpc_id = "${aws_vpc.this.id}"
map_public_ip_on_launch = true
availability_zone = "${element(keys(local.private_subnets), count.index)}"
tags = {
Name = "${var.service_name}-service-private"
}
}
resource "aws_default_route_table" "public" {
default_route_table_id = "${aws_vpc.this.main_route_table_id}"
tags = {
Name = "${var.service_name}-public"
}
}
resource "aws_route" "public_internet_gateway" {
count = "${length(local.public_subnets)}"
route_table_id = "${aws_default_route_table.public.id}"
destination_cidr_block = "0.0.0.0/0"
gateway_id = "${aws_internet_gateway.this.id}"
timeouts {
create = "5m"
}
}
resource "aws_route_table_association" "public" {
count = "${length(local.public_subnets)}"
subnet_id = "${element(aws_subnet.public.*.id, count.index)}"
route_table_id = "${aws_default_route_table.public.id}"
}
resource "aws_route_table" "private" {
vpc_id = "${aws_vpc.this.id}"
tags = {
Name = "${var.service_name}-private"
}
}
resource "aws_route_table_association" "private" {
count = "${length(local.private_subnets)}"
subnet_id = "${element(aws_subnet.private.*.id, count.index)}"
route_table_id = "${aws_route_table.private.id}"
}
resource "aws_eip" "nat" {
vpc = true
tags = {
Name = "${var.service_name}-eip"
}
}
resource "aws_nat_gateway" "this" {
allocation_id = "${aws_eip.nat.id}"
subnet_id = "${aws_subnet.public.0.id}"
tags = {
Name = "${var.service_name}-nat-gw"
}
}
resource "aws_route" "private_nat_gateway" {
route_table_id = "${aws_route_table.private.id}"
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = "${aws_nat_gateway.this.id}"
timeouts {
create = "5m"
}
}
Refer to this repository : ecs-with-codepipeline-example-by-terraform
Thank you all, it turned out to be an issue with VPC which internet gateway is attached to. Internet gateway must chose a VPC which it must be created. You cannot route traffic to an internet gateway not within the same VPC as it wouldn't have access to it otherwise. Therefore my attempt to route traffic to an internet gateway external to the VPC is not allowed.
This issue is resolved by creating a new internet gateway within the new VPC I created. However this mean I cannot use existing internet gateway thereby introducing other issue such as need to inform external partners to add permission to the new public IP of the internet gateway.

Terraform - populate variable values from same script

I'm very green to terraform; infact this is part of my training.
I'm wondering; is there a way to get terraform to store a specific value (as variable) from the previous command within the same file.
Example:
resource "aws_vpc" "TestVPC"{
cidr_block = "192.168.0.0/16"
instance_tenancy = "default"
enable_dns_hostnames="True"
tags{
Name="TestVpc"
}
}
resource "aws_subnet" "TestSubnet"{
vpc_id = "${var.aws_vpc_id}" ##This is where I'd like to populate the aws_vpc_id from the VPC creation step above.
cidr_block = "192.168.0.0/24"
map_public_ip_on_launch="True"
availability_zone = "us-east-2a"
tags{
Name="TestSubnet"
}
}
Help is greatly appreciated.
Thanks.
You can use the output from the creation of the VPC, ${aws_vpc.TestVPC.id}
Like so:
resource "aws_vpc" "TestVPC" {
cidr_block = "192.168.0.0/16"
instance_tenancy = "default"
enable_dns_hostnames = "True"
tags {
Name = "TestVpc"
}
}
resource "aws_subnet" "TestSubnet" {
vpc_id = "${aws_vpc.TestVPC.id}"
cidr_block = "192.168.0.0/24"
map_public_ip_on_launch = "True"
availability_zone = "us-east-2a"
tags {
Name = "TestSubnet"
}
}

Resources