Creating subnets and assinging rout tables to them - terraform

I am new to terraform and I'm trying to create a VPC with multiple subnets and adding route tables to all those subnets in a for loop manner.
VPC: 10.207.0.0/16
There's number_of_subnets which will create subnets like this: 10.207.x.0/24
This code works fine:
variable "region" {
default = "us-east-1"
}
variable "availability_zone" {
default = "us-east-1a"
}
variable "cidr_block" {
default = "207"
}
variable "number_of_subnets" {
default = 5
}
provider "aws" {
region = var.region
}
resource "aws_vpc" "test_vpc" {
cidr_block = "10.${var.cidr_block}.0.0/16"
instance_tenancy = "default"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "test_vpc_${var.cidr_block}"
}
}
resource "aws_subnet" "test_subnets" {
count = var.number_of_subnets
vpc_id = aws_vpc.test_vpc.id
cidr_block = "10.${var.cidr_block}.${count.index+1}.0/24" # start from x.x.1.0/24
availability_zone = var.availability_zone
map_public_ip_on_launch = false
tags = {
Name = "test_subnet_${var.cidr_block}_${count.index+1}"
}
}
Now if I try to add this code to the bottom of the same file (everything is in one file called main.tf) to get the subnets and add route table to each:
# get all subnet IDs
data "aws_subnets" "q_subnets" {
filter {
name = "vpc-id"
values = [aws_vpc.test_vpc.id]
}
}
# add route table to all subnets
resource "aws_route_table_association" "rt_assoc_subnet" {
depends_on = [aws_subnet.test_subnets]
for_each = toset(data.aws_subnets.q_subnets.ids)
subnet_id = each.value
route_table_id = aws_route_table.test_rt.id
}
and run terraform apply it will give this error:
invalid for_each argument...
The "for_each" value depends on resource attribute that cannot be deteremined until apply,...
which doesn't make scense. First create vpc, then subnet, then get all subnets...
I also tried depends_on and didn't help.
How would I write this to make it work?
Any help is appreciated.
UPDATE1:
I tried to use aws_subnet.test_subnets.*.id instead of data and it still gives depencendy error:
variable "region" {
default = "us-east-1"
}
variable "availability_zone" {
default = "us-east-1a"
}
variable "cidr_block" {
default = "207"
}
variable "number_of_subnets" {
default = 5
}
provider "aws" {
region = var.region
}
resource "aws_vpc" "test_vpc" {
cidr_block = "10.${var.cidr_block}.0.0/16"
instance_tenancy = "default"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "test_vpc_${var.cidr_block}"
}
}
resource "aws_route_table" "test_rt" {
vpc_id = aws_vpc.test_vpc.id
route = []
tags = {
Name = "test_rt_${var.cidr_block}"
}
}
resource "aws_subnet" "test_subnets" {
count = var.number_of_subnets
vpc_id = aws_vpc.test_vpc.id
cidr_block = "10.${var.cidr_block}.${count.index+1}.0/24" # start from x.x.1.0/24
availability_zone = var.availability_zone
map_public_ip_on_launch = false
tags = {
Name = "test_subnet_${var.cidr_block}_${count.index+1}"
}
}
output "subnets" {
value = aws_subnet.test_subnets.*.id
}
# add route table to all subnets
resource "aws_route_table_association" "rt_assoc_subnet" {
depends_on = [aws_subnet.test_subnets]
for_each = toset(aws_subnet.test_subnets.*.id)
subnet_id = each.value
route_table_id = aws_route_table.test_rt.id
}
is there another way to pass the subnets to aws_route_table_association without getting dependency error?

Since you are using count, it is very hard to make count work with for_each. It would be better to continue using count for route table association as well. If you decide to go down that route, the only change you need is:
resource "aws_route_table_association" "rt_assoc_subnet" {
count = var.number_of_subnets
subnet_id = aws_subnet.test_subnets.*.id[count.index]
route_table_id = aws_route_table.test_rt.id
}
This will work as intended. However, if you must use for_each I would suggest defining a variable that could be used with it in all the resources you are now using count. If you really want to use for_each with the current code, then you can use the -target option [1]:
terraform apply -target=aws_vpc.test_vpc -target=aws_route_table.test_rt -target=aws_subnet.test_subnets
When running this command, this will be shown in the command output:
│ Warning: Resource targeting is in effect
│
│ You are creating a plan with the -target option, which means that the result of this plan may not represent all of the changes requested by the current
│ configuration.
│
│ The -target option is not for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when Terraform
│ specifically suggests to use it as part of an error message.
After the targeted resources are created, you could re-run terraform apply and it should create the route table associations.
[1] https://www.terraform.io/cli/commands/plan#target-address

Related

Azure/Terraform:Link subnets to NSGs(ERROR-for_each map includes keys derived from resource attributes that cannot be determined until apply)

Locked for 3 days. Comments on this question have been disabled, but it is still accepting new answers and other interactions. Learn more.
Objective:Link multiple subnets in the environment to corresponding NSGs using a module (NSGs and Subnets have been created using separate modules)
Root Module:
1.main.tf
resource "azurerm_subnet_network_security_group_association" "root_subnet_nsg_association" {
subnet_id = var.subnet_id
network_security_group_id = var.nsg_id
}
2.variables.tf
variable "subnet_id"{
type=number
description="ID of the subnet which is to be attached to NSG"
#default=""
}
variable "nsg_id"{
type=number
description="ID of the NSG which is to be associated with a subnet"
#default=""
}
Calling Module in Projects Folder:
(for_each used to iterate the module)
1.nsg_subnet_association.tf
module "nsg_subnet_asosciation_module"{
source="../../Modules/network/nsg_subnet_association"
#Variable names to be passed into the root module:
#Use for_each to loop the module:
#for_each accepts a set or map but not list as a value
for_each = local.nsg_subnet_association
subnet_id=each.key
nsg_id=each.value
}
2.locals block passing in values to the calling module:
NOTE:It is possible to have dynamic keys in the map using parenthesis ()
locals{ //Key in subnet name and NSG name for each element of the LIST
//Implicit dependence on Subnet and NSG being created before attempt to associate
#It is possible to have dynamic keys using parenthesis () as seen on left below
nsg_subnet_association={
(module.subnet_module["MGT-Subnet-1"].subnet_id)= module.nsg_module["HUB-NSG"].nsg_id
(module.subnet_module["MGT-Subnet-1"].subnet_id) = module.nsg_module["MGT-NSG"].nsg_id
(module.subnet_module["SEC-Subnet-1"].subnet_id) = module.nsg_module["SEC-NSG"].nsg_id
}
}
This ends up with the following error:
The "for_each" map includes keys derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys that will identify the instances of this resource.
When working with unknown values in for_each, it's better to define the map keys statically in your configuration and place apply-time results only in the map values.
Alternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge.
In Terraform , when dynamically getting the value of vnets or subnets , it may take time to create and the rest of the dependent resources cannot get desired values and so this error occurs.
Error:
The "for_each" map includes keys derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys that will identify the instances of this resource.
Use a code where the values are defined statically to resolve the error:
example:
Code:
Variables.tf:
variable "virtual_network_name" {
type = string
default = "my-virtual-network"
}
variable "subnet_address_prefixes" {
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24"]
}
variable "subnet_names" {
type = set(string)
default = ["subnet1", "subnet2"]
}
variable "nsg_names" {
type = set(string)
default = ["nsg1", "nsg2"]
}
variable "subnet_nsg_mappings" {
type = map(string)
default = {
"subnet1" = "nsg1"
"subnet2" = "nsg2"
}
}
Main.tf
resource "azurerm_virtual_network" "virtual_network" {
name = var.virtual_network_name
address_space = ["10.0.0.0/16"]
location = data.azurerm_resource_group.example.location
resource_group_name = data.azurerm_resource_group.example.name
}
resource "azurerm_network_security_group" "nsg" {
for_each = toset(var.nsg_names)
name = each.value
location = data.azurerm_resource_group.example.location
resource_group_name = data.azurerm_resource_group.example.name
security_rule {
name = "allow_http"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
resource "azurerm_subnet" "subnet" {
for_each = toset(var.subnet_names)
name = each.value
virtual_network_name = azurerm_virtual_network.virtual_network.name
address_prefixes = var.subnet_address_prefixes
resource_group_name = data.azurerm_resource_group.example.name
// enable_multiple_address_prefixes = true
}
# Associate each subnet with its corresponding NSG
resource "azurerm_subnet_network_security_group_association" "subnet_nsg" {
for_each = var.subnet_nsg_mappings
subnet_id = azurerm_subnet.subnet[each.key].id
network_security_group_id = azurerm_network_security_group.nsg[each.value].id
//subnet_id = azurerm_subnet.subnet[each.key].id
// network_security_group_id = azurerm_network_security_group.nsg[var.subnet_nsg_mappings[each.value]].id
}
Or
Define locals for mappings .
locals {
subnet_nsg_mappings = {
"subnet1" = "nsg1",
"subnet2" = "nsg2",
"subnet3" = "nsg3"
}
}
resource "azurerm_subnet_network_security_group_association" "subnet_nsg" {
for_each = toset(var.subnet_names)
subnet_id = azurerm_subnet.subnet[each.value].id
network_security_group_id = azurerm_network_security_group.nsg[local.subnet_nsg_mappings[each.value]].id
}
If dynamic values must be used for_each keys cannot be determined during apply time. In that case use the -target option to first apply vnet and subnet values i.e; the resources that the for_each value depends on and apply completely.
terraform apply -target="azurerm_virtual_network.virtual_network" -target="azurerm_subnet.subnet"
Reference:
azurerm_subnet_network_security_group_association | Resources | hashicorp/azurerm | Terraform Registry

Terraform - Can't access attributes on a primitive-typed value (string) when trying to add multiple disks

I'm a total newbie to Terraform and loving it so far. However, I'm a bit stuck with the below. I'm trying to add a disk to multiple machines using a dynamic block, but I'm getting the error 'Can't access attributes on a primitive-typed value (string)' whenever I run terraform plan. The config for my compute engine instance/disk looks like this:
resource "google_compute_instance" "vm_instance" {
count = 2
name = "test-instance${count.index + 1}"
machine_type = "e2-micro"
labels = {
"environment" = var.environment
}
boot_disk {
initialize_params {
image = var.image
}
}
network_interface {
# A default network is created for all GCP projects
network = "default"
}
lifecycle {
ignore_changes = [
resource_policies,
metadata,
attached_disk
]
}
}
resource "google_compute_disk" "default" {
for_each = toset(google_compute_instance.vm_instance.*.id)
name = each.value.name
type = "pd-ssd"
labels = {
environment = "dev"
}
physical_block_size_bytes = 4096
}
resource "google_compute_attached_disk" "default" {
for_each = google_compute_instance.vm_instance.*.id
disk = google_compute_disk.default[each.key].id
instance = each.key
}
It looks like the plan picks up two instances of the VM as expected, but Terraform is unable to access any of its attributes...
│ Error: Unsupported attribute
│
│ on main.tf line 50, in resource "google_compute_disk" "default":
│ 50: name = each.value.id
│ ├────────────────
│ │ each.value is "projects/blah/zones/europe-west2-a/instances/test-instance2"
Can anyone advise on where I'm going wrong, please? Thanks
If you use for_each over a set of string, you don't get access to properties, so this won't work.
resource "google_compute_disk" "default" {
for_each = toset(google_compute_instance.vm_instance.*.id)
name = each.value.name
# ...
}
If you want to use each of those id's, what you need is the set key.
resource "google_compute_disk" "default" {
for_each = toset(google_compute_instance.vm_instance.*.id)
name = each.key
# ...
}
Also, you're better off not using the Legacy Splat Expressions, and will find the code more intuitive I suspect formed something like this:
resource "google_compute_disk" "default" {
for_each = google_compute_instance.vm_instance
name = each.value.id
# ...
}

Get ARNs of subnets and iterate over them

I have created some subnets. I want to share those subnets with other accounts. For that I need to retrieve the ARN of the subnets.
I am able to get a list of ARNs like this
data "aws_subnets" "dev_subnet" {
filter {
name = "vpc-id"
values = [module.vpc.vpc_id]
}
tags = {
Environment = "dev-*"
}
}
data "aws_subnet" "dev_subnet" {
for_each = toset(data.aws_subnets.dev_subnet.ids)
id = each.value
}
output "dev_subnet_arns" {
value = [for s in data.aws_subnet.dev_subnet : s.arn]
}
This results in
+ dev_subnet_arns = [
+ "arn:aws:ec2:ca-central-1:0097747:subnet/subnet-013987fd9651c3545",
+ "arn:aws:ec2:ca-central-1:0477747:subnet/subnet-015d76b264280321a",
+ "arn:aws:ec2:ca-central-1:0091747:subnet/subnet-026cd0402fe283c33",
]
Now I want to take the list of arns of the subnets and associate them with the resource_share_arn
What Im trying is something like this
resource "aws_ram_resource_association" "example" {
for_each = toset(data.aws_subnets.dev_subnet.ids)
resource_arn = each.value
resource_share_arn = aws_ram_resource_share.share_subnets_with_dev_account.arn
}
But this fails since it only gets the subnets ids and thats wrong
error associating RAM Resource Share: MalformedArnException: The specified resource ARN subnet-0c4afd736c18b3c28 is not valid. Verify the ARN and try again.
This also fails
resource "aws_ram_resource_association" "example" {
for_each = toset(data.aws_subnets.dev_subnet.arn)
resource_arn = each.value
resource_share_arn = aws_ram_resource_share.share_subnets_with_dev_account.arn
}
since arn is not an attribute. What am I missing here ?
You need to loop over the ARNs of the subnets and pass the ARN value for the resource_arn:
resource "aws_ram_resource_association" "example" {
for_each = toset([for s in data.aws_subnet.dev_subnet : s.arn])
resource_arn = each.value
resource_share_arn = aws_ram_resource_share.share_subnets_with_dev_account.arn
}
Or another solution would be:
resource "aws_ram_resource_association" "example" {
for_each = toset(values(data.aws_subnet.dev_subnet)[*].arn)
resource_arn = each.value
resource_share_arn = aws_ram_resource_share.share_subnets_with_dev_account.arn
}

Terraform - A managed resource has not been declared in the root module

i'm trying create ec2 instance and setup load balancer using terraform but i'm facing follwing error. How to create instance and configure load balacer in a single main.tf file?
Error: Reference to undeclared resource
"aws_lb_target_group" "front-end":27: vpc_id = "${aws_vpc.terrafom-elb.id}"
A managed resource "aws_vpc" "terrafom-elb" has not been declared in the root
module.source`
code:
region = "us-east-1"
access_key = "*********************"
secret_key = "**********************"
}
resource "aws_instance" "terraform" {
ami = "ami-07ebfd5b3428b6f4d"
instance_type = "t2.micro"
security_groups = ["nodejs","default"]
tags = {
Name = "terrafom-elb"
}
}
resource "aws_lb" "front-end"{
name = "front-end-lb"
internal = false
security_groups = ["nodejs"]
}
resource "aws_lb_target_group" "front-end" {
name = "front-end"
port = 8989
protocol = "HTTP"
vpc_id = "${aws_vpc.terrafom-elb.id}"
depends_on = [aws_instance.terraform]
}
There's a typo where you're assigning the vpc_id:
vpc_id = "${aws_vpc.terrafom-elb.id}"
should be:
vpc_id = "${aws_vpc.terraform-elb.id}"
note the missing 'r' in the word 'terraform'
You can add a data structure to the top and pass VPC ID as variable:
data "aws_vpc" "selected" {
id = var.vpc_id
}
And reference it as vpc_id = data.aws_vpc.selected.id

Terraform on Azure - Deploy multiple subnet

I'm trying to implement a Terraform script to create multiple subnets.
resource "azurerm_subnet" "test_subnet" {
name = "testSUBNET"
resource_group_name = "${local.resource_group_name}"
virtual_network_name = "${azurerm_virtual_network.lab_vnet.name}"
address_prefix = "10.0.1.0/24"
}
Is there a way to do a for-each or a loop on a variable in order to create them at the same time?
You can achieve this using a variable and count index as follows:
variable "subnet_prefix" {
type = "list"
default = [
{
ip = "10.0.1.0/24"
name = "subnet-1"
},
{
ip = "10.0.2.0/24"
name = "subnet-2"
}
]
}
resource "azurerm_subnet" "test_subnet" {
name = "${lookup(element(var.subnet_prefix, count.index), "name")}"
count = "${length(var.subnet_prefix)}"
resource_group_name = "${local.resource_group_name}"
virtual_network_name = "${azurerm_virtual_network.lab_vnet.name}"
address_prefix = "${lookup(element(var.subnet_prefix, count.index), "ip")}"
}
There is also preview feature available for-each in the new version
If you are using Terraform 12 this can be achieved using the for-each capability or the count capability
count should be used if you are looking to create almost identical resources.
for-each should be used to create multiple of each instance based on a different map or set of values.
Using an list of strings and the toset() function to convert this is a neat way to achieve this
variable "subnet_ids" {
type = list(string)
}
resource "aws_instance" "server" {
for_each = toset(var.subnet_ids)
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
subnet_id = each.key # note: each.key and each.value are the same for a set
tags = {
Name = "Server ${each.key}"
}
}
Or you could achieve this by using something like the below:
resource "azurerm_resource_group" "rg" {
for_each = {
a_group = "eastus"
another_group = "westus2"
}
name = each.key
location = each.value
}
If you are looking to achieve this with Terraform 11 the count and variable capabilities are the only way other than code duplication. (Rajat Arora has mentioned)
I would strongly recommended using Terraform 12 as the providers for Terraform 11 will be unsupported in the not to far future and if you can save yourself from refactoring now, you should!

Resources