Dynamic interpolation of all components of cidrsubnet function in Terraform - terraform

Is there a way in Terraform where I can interpolate all three components of cidrsubnet function ? The reason I'm asking this is because I want to create VPCs with different CIDR prefix and accordingly the cidrsubnet function takes care of creating subnets for me.
Supposingly, VPC "prod" has CIDR 10.10.0.0/16 and VPC "dev" has CIDR 10.20.0.0/24. I'm looking for a solution wherein the same code will create subnets for me by interpolating values of cidrsubnet function something like below:
# map for different CIDR
variable "VPC_CIDR" {
type = "map"
default = {
"dev" = "10.10.0.0/24"
"prod" = "10.10.0.0/16"
}
}
variable "PRI_SUBNET_COUNT" {
default = "1"
}
# intended logic which interpolates netnum (impractical code)
if var.VPC_CIDR = "prod"
netnum = 4
elif var.VPC_CIDR = "dev"
netnum = 3
else
netnum = 2
resource "aws_subnet" "sub-node-private" {
count = var.PRI_SUBNET_COUNT
cidr_block = cidrsubnet(var.VPC_CIDR, var.netnum, count.index + 2) #all three components interpolated
The above code will create subnet CIDR 10.10.0.0/20 for prod and 10.10.0.0/28 for dev. This way my code remains same, only the variables get interpolated.
N.B: This code is for demonstration purpose. It is known that this is not the practical code for if/else in Terraform.

If the keys of var.VPC_CIDR are selectable by the caller then it may be best to combine the CIDR prefix and the number of new bits to use for its subnets together in the variable. Since the conventional way to name Terraform variables is in lowercase, I'm going to also rename it to vpc_cidr in the following examples.
variable "vpc_cidr" {
type = map(object({
cidr_block = string
subnet_bits = number
}))
default = {
dev = {
cidr_block = "10.10.0.0/24"
subnet_bits = 3
}
prod = {
cidr_block = "10.10.0.0/16"
subnet_bits = 4
}
}
}
variable "pri_subnet_count" {
type = number
default = 1
}
locals {
vpc_subnets = flatten([
for name, vpc in var.vpc_cidr : [
for i in count(var.pri_subnet_count) : {
name = "${name}-${i}"
vpc_name = name
cidr_block = vpc.cidr_block
subnet_bits = vpc.subnet_bits
network_num = i + 2
}
]
])
}
resource "aws_vpc" "example" {
for_each = var.vpc_cidr
cidr_block = each.value.cidr_block
}
resource "aws_subnet" "private" {
for_each = { for s in local.vpc_subnets : s.name => s }
vpc_id = aws_vpc.example[each.value.vpc_name].id
cidr_block = cidrsubnet(each.value.cidr_block, each.value.subnet_bits, each.value.network_num)
# ...
}
If the names "prod" and "dev" are fixed and thus your module will assume they will always be specified, you can derive the subnet_bits values automatically in a way similar to what you described, like this:
variable "vpc_cidr" {
type = object({
# Force caller to provide "dev" and "prod" values, so
# that it will match up with the attributes in
# local.subnet_bits defined below.
dev = string
prod = string
})
value = {
dev = "10.10.0.0/24"
prod = "10.10.0.0/16"
}
}
variable "pri_subnet_count" {
type = number
default = 1
}
locals {
subnet_bits = {
dev = 3
prod = 4
}
vpcs = {
for name, cidr_block in var.vpc_cidr : name => {
cidr_block = cidr_block
subnet_bits = local.subnet_bits[name]
}
}
vpc_subnets = flatten([
for name, vpc in local.vpcs : [
for i in count(var.pri_subnet_count) : {
name = "${name}-${i}"
vpc_name = name
cidr_block = vpc.cidr_block
subnet_bits = vpc.subnet_bits
network_num = i + 2
}
]
])
}
resource "aws_vpc" "example" {
for_each = local.vpcs
cidr_block = each.value.cidr_block
}
resource "aws_subnet" "private" {
for_each = { for s in local.vpc_subnets : s.name => s }
vpc_id = aws_vpc.example[each.value.vpc_name].id
cidr_block = cidrsubnet(each.value.cidr_block, each.value.subnet_bits, each.value.network_num + 2)
# ...
}
The general pattern illustrated above is building the data structure local.vpc_subnets which contains one element for each subnet you want to create. That then allows repeating aws_subnet.private for each element, and gathers together all of the values required to populate the vpc_id and cidr_block arguments on the subnet.

Related

Terraform how to access resource local name as a variable inside the resource block itself

resource "aws_subnet" "goodsubnet" {
vpc_id = "VPCID"
cidr_block = "x.x.x.x/x"
availability_zone = "xyz"
tags =
{
tagname1 = "$something"
}
}
I want the tag "tagname1" to dynamically have the value of the resource local name i.e "goodsubnet" Is there a variable I can use?
Thanks
That is not possible the way you want to do it. However, Terraform has a concept of variables, which you can use to assign values to arguments. That could be either a local value [1] or an input variable [2]. For example, an input variable definition:
variable "subnet_name_tag" {
type = string
description = "Tag name for a subnet."
}
Then, in your code you would do:
resource "aws_subnet" "goodsubnet" {
vpc_id = "VPCID"
cidr_block = "x.x.x.x/x"
availability_zone = "xyz"
tags =
{
tagname1 = var.subnet_name_tag
}
}
Alternatively, you could define a local value:
locals {
subnet_tag_name = "goodsubnet"
}
Followed by:
resource "aws_subnet" "goodsubnet" {
vpc_id = "VPCID"
cidr_block = "x.x.x.x/x"
availability_zone = "xyz"
tags =
{
tagname1 = local.subnet_name_tag
}
}
[1] https://developer.hashicorp.com/terraform/language/values/locals
[2] https://developer.hashicorp.com/terraform/language/values/variables

Terraform count to for_each refactor

Trying to refactor count to for_each and having hard time with this.
local = {
routes = [
{
CIDR = "192.225.0.0/16"
Name = "rt-1"
},
{
CIDR = "192.225.0.0/16"
Name = "rt-2"
},
{
CIDR = "192.225.1.0/16"
Name = "rt-1"
},
{
CIDR = "192.225.1.0/16"
Name = "rt-2"
},
{
CIDR = "192.225.0.0/16"
Name = "rt-3"
}
]
}
Works:
resource "aws_route" "sample" {
count = length(local.routes)
route_table_id = local.routes[count.index].Name
destination_cidr_block = local.routes[count.index].CIDR
}
Don't Work:
resource "aws_route" "sample" {
for_each = local.routes
route_table_id = local.routes[count.index].Name
destination_cidr_block = local.routes[count.index].CIDR
}
Getting this error
The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, or set of strings, and you have provided a value of type tuple.
If I change my local to map then it complains about duplicate, any help.
The for_each meta-argument accepts a map or a set of strings and not list and you are trying to pass a list. You could do something like this:
resource "aws_route" "sample" {
for_each = { for entry in local.routes : "${entry.Name}.${entry.CICR}" => entry }
route_table_id = each.value.Name
destination_cidr_block = each.value.CIDR
}
Read more on for_each: https://www.terraform.io/language/meta-arguments/for_each#basic-syntax

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: Retrieving each subnet gateway address from a list. GCP

Had a question answered earlier to help with creating a set of subnets off a list. Now I am trying to output each assigned ip address.
module "subnets" {
source = "../../../Modules/subnets-test/"
network_name = module.vpc.network_name
subnet_region = "europe-west2"
subnets = {
lister = "192.2.128.0/18",
kryten = "192.2.0.0/17",
rimmer = "192.2.208.0/20",
cat = "192.2.192.0/20",
holly = "192.2.224.0/20"
}
}
I can successfully output the list of subnets and their values
output "private_subnets" {
description = "List of IDs of private subnets"
value = ["${module.subnets.subnets}"]
}
Giving me all of the subnet outputs (one as example below)
"rimmer" = {
"creation_timestamp" = "2020-06-06T03:13:30.244-07:00"
"description" = ""
"gateway_address" = "192.2.208.1"
"id" = "projects/red-dwarf/regions/europe-west2/subnetworks/rimmer"
"ip_cidr_range" = "192.2.208.0/20"
"log_config" = []
"name" = "rimmer"
"network" = "https://www.googleapis.com/compute/v1/projects/red-dwarf/global/networks/red-dwarf-vpc"
"private_ip_google_access" = false
"project" = "red-dwarf"
"region" = "europe-west2"
"secondary_ip_range" = []
"self_link" = "https://www.googleapis.com/compute/v1/projects/red-dwarf/regions/europe-west2/subnetworks/rimmer"
But now I just want to extract the gateway address as a single output. But anything I try to do to the module gives me an error saying the list has 5 attributes.
How can I pull the attributes out of the created subnets when they've been provisioned via map(string) ?
Edit - Subnet Module
resource "google_compute_subnetwork" "subnet" {
network = var.network_name
for_each = var.subnets
name = each.key
ip_cidr_range = each.value
}
Edit - Subnet Output - This works to output everything as a whole.
output "subnets" {
value = google_compute_subnetwork.subnet
description = "The created subnet resources"
}
Here is what I would do:
locals {
subnets = {
cow = "10.0.208.0/20",
cat = "10.0.192.0/20",
dog = "10.0.224.0/20"
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_vpc" "myvpc" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "subnet" {
vpc_id = aws_vpc.myvpc.id
for_each = local.subnets
cidr_block = each.value
tags = { Name = each.key }
}
output "subnets" {
value = aws_subnet.subnet
}
output "subnets_arn" {
value = { for k, v in aws_subnet.subnet : k => v.arn }
}
The key there is the for loop:
value = { for k, v in aws_subnet.subnet : k => v.arn }
that creates a new object with key the name and value any property we want.
The terraform output of an apply is:
subnets_arn = {
"cat" = "arn:aws:ec2:us-east-1:841836440307:subnet/subnet-046fff167cdc81e9f"
"cow" = "arn:aws:ec2:us-east-1:841836440307:subnet/subnet-00217a1ec0531d2c6"
"dog" = "arn:aws:ec2:us-east-1:841836440307:subnet/subnet-0ac82ef0fd87bcee2"
}
In my case I'm using AWS (that is what I got access to right now) but the same should translate to GCP, just use the property you need, educated guess should be something like:
output "subnets_gateway_address" {
value = { for k, v in aws_subnet.subnet : k => v.gateway_address }
}

Terraform: How to set variables in a module based on a conditional?

I would like to pass a variable that will allow me to specify the list of VPC and subnet settings for an AWS instance. There are fixed VPC and subnet settings that make sense so I just want to allow a user to pick one using a single variable, i.e. use A or B.
For instance, let's say I have two available VPCs, and these are specified in a variables.tf file for a module my_instance:
variable "a_vpc_cidr_block" { default = "105.191.44.0/22" }
variable "a_vpc_id" { default = "id_a"}
variable "a_vpc_name" { default = "vpc_a" }
variable "a_subnet_availability_zone" { default = "us-east-1a" }
variable "a_subnet_cidr_block" { default = "105.191.25.0/25" }
variable "a_subnet_name" { default = "instance_A" }
variable "b_vpc_cidr_block" { default = "105.191.45.0/22" }
variable "b_vpc_id" { default = "id_b"}
variable "b_vpc_name" { default = "vpc_b" }
variable "b_subnet_availability_zone" { default = "us-east-1a" }
variable "b_subnet_cidr_block" { default = "105.191.35.0/25" }
variable "b_subnet_name" { default = "instance_B" }
The my_instance module will take a single input variable that an environment will specify, with a value of either 'A' or 'B' (is there a way to limit options for a variable to a list of values such as options=['A', 'B']?), and will be called like so in the terraform.tf for a Terraform configuration with a single instance:
module "my_instance" {
source = "../../modules/my_instance"
option = "A"
}
I want to now implement some logic within the module's main file (modules/my_instance/my_instance.tf) where it decides on which of the two collections of VPC and subnet settings it should use from the ones in modules/my_instance/variables.tf. I want to something like this (pseudocode):
if var.option == 'A'
vpc_cidr_block = var.a_vpc_cidr_block
vpc_id = var.a_vpc_id
vpc_name = var.a_vpc_name
subnet_availability_zone = var.a_subnet_availability_zone
subnet_cidr_block = var.a_subnet_cidr_block
subnet_name = var.a_subnet_name
else if var.option == 'B'
vpc_cidr_block = var.b_vpc_cidr_block
vpc_id = var.b_vpc_id
vpc_name = var.b_vpc_name
subnet_availability_zone = var.b_subnet_availability_zone
subnet_cidr_block = var.b_subnet_cidr_block
subnet_name = var.b_subnet_name
else
raise an error
# get a data resource identified by the VPC variables
data "aws_vpc" "instance_vpc" {
cidr_block = var.vpc_cidr_block
tags = {
Name = var.vpc_name
}
}
# get a data resource identified by the VPC variables
data "aws_subnet" "instance_subnet" {
vpc_id = var.vpc_id
cidr_block = var.subnet_cidr_block
availability_zone = var.subnet_availability_zone
tags = {
Name = var.subnet_name
}
}
# create an AWS key pair resource
resource "aws_key_pair" "instance_aws_key_pair" {
key_name = "component_key_${terraform.workspace}"
public_key = file("~/.ssh/terraform.pub")
}
# create the AWS EC2 instance
resource "aws_instance" "my_aws_instance" {
key_name = aws_key_pair.instance_aws_key_pair.key_name
ami = "ami-b12345"
instance_type = "t2.micro"
subnet_id = data.aws_subnet.instance_subnet.id
connection {
type = "ssh"
user = "terraform"
private_key = file("~/.ssh/terraform")
host = self.public_ip
}
tags = {
"Name" : "my_instance_name"
"Terraform" : "true"
}
}
Is this a matter of somehow using a count, something like this:
count = var.option == 'A'? 1 : 0
Is there a way to do this, or is there a better approach? I am very new to Terraform so I may be missing something obvious.
You have a couple of questions here.
Firstly, you should be able to use the newer, experimental custom validation rules to assert that a value is in a specific list of values.
Secondly, for determining which set of variables to use, I'd recommend going with a good old map in a local value.
For example,
locals {
vpc_info = {
"A" = {
vpc_cidr_block = var.a_vpc_cidr_block
vpc_id = var.a_vpc_id
vpc_name = var.a_vpc_name
subnet_availability_zone = var.a_subnet_availability_zone
subnet_cidr_block = var.a_subnet_cidr_block
subnet_name = var.a_subnet_name
}
"B" = {
vpc_cidr_block = var.b_vpc_cidr_block
vpc_id = var.b_vpc_id
vpc_name = var.b_vpc_name
subnet_availability_zone = var.b_subnet_availability_zone
subnet_cidr_block = var.b_subnet_cidr_block
subnet_name = var.b_subnet_name
}
}
}
Then you should be able to reference a specific field, within the chose option like the following
local.vpc_info[var.option].vpc_name
Let me know if this hits all your questions.

Resources