Terraform fail due output for_each values - amazon-web-services

When I trying to dynamically allocate subnet ids by running this tf file:
data "aws_availability_zones" "available" {}
resource "aws_vpc" "wpl_vpc" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
tags = {
Name = "WPL-VPC"
CreatedBy = var.created_by
}
}
resource "aws_subnet" "wpl_public_subnet" {
for_each = { for index, az_name in data.aws_availability_zones.available.names : index => az_name }
vpc_id = aws_vpc.wpl_vpc.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, each.key + 10)
availability_zone = data.aws_availability_zones.available.names[each.key]
map_public_ip_on_launch = true
tags = {
Name = "WPL-PublicSubnet"
CreatedBy = var.created_by
}
}
output "wpl_public_subnet_ids" {
value = [aws_subnet.wpl_public_subnet.*.id]
}
I get an error:
Error: Unsupported attribute
on ../modules/vpc/outputs.tf line 5, in output "wpl_public_subnet_ids":
5: value = [aws_subnet.wpl_public_subnet.*.id]
This object does not have an attribute named "id".
However, everything works like charm, if I replace the output with this one:
aws_subnet.wpl_public_subnet.0.id
How could I output all subnet ids values?

Since you are using for_each, you will have a map, not a list. Thus, you have to get values first, before getting the ids. Also you don't need extra square brackets:
output "wpl_public_subnet_ids" {
value = values(aws_subnet.wpl_public_subnet)[*].id
}

Related

Dynamically getting values from Terraform output and pass them by index

Terraform greenhorn here. I want to output and dynamically add only the first 2 subnets in the vpc_zone_identifier below. But I can't get it by index, is asking for a name.
vpc_zone_identifier = [module.subnet[0].subnet_id, module.subnet[1].subnet_id]
Here is what I tried.
Any suggestion is appreciated.
resource "aws_subnet" "this" {
vpc_id = var.vpc_id
cidr_block = var.cidr_block_subnet
map_public_ip_on_launch = var.map_public_ip_on_launch
tags = var.tags
}
module "subnet" {
source = "./aws_modules/subnet"
for_each = {for key, value in var.subnet_settings: value.cidr_block_subnet => value}
vpc_id = module.vpc.vpc_id
cidr_block_subnet = each.key
map_public_ip_on_launch = each.value.map_public_ip_on_launch
tags = var.tags
depends_on = [module.vpc]
}
variable "subnet_settings" {
type = list(object({
cidr_block_subnet = string
map_public_ip_on_launch = bool
}))
}
subnet_settings = [
{
cidr_block_subnet = "10.1.1.0/24"
map_public_ip_on_launch = false
},
{
cidr_block_subnet = "10.1.2.0/24"
map_public_ip_on_launch = false
},
{
cidr_block_subnet = "10.1.3.0/24"
map_public_ip_on_launch = false
}
]
output "subnet_id" {
value = aws_subnet.this.id
}
resource "aws_autoscaling_group" "autosys" {
desired_capacity = var.node-count
max_size = var.node-count
min_size = var.node-count
name = var.asg-name
vpc_zone_identifier = [module.subnet[0].subnet_id, module.subnet[1].subnet_id]
launch_template {
id = aws_launch_template.autosys.id
version = "$Latest"
}
}
In a for expression, you can initialize two variables within the scope of the lambda when iterating on a list type. In this situation, the first variable will be assigned the index of the iteration. Therefore, we can modify the for_each meta-argument of your module:
module "subnet" {
source = "./aws_modules/subnet"
for_each = {for idx, subnet in var.subnet_settings: idx => subnet}
vpc_id = module.vpc.vpc_id
cidr_block_subnet = each.value.cidr_block_subnet
map_public_ip_on_launch = each.value.map_public_ip_on_launch
tags = var.tags
depends_on = [module.vpc]
}
and now the module key will be the index of the var.subnet_settings. Correspondingly, the module output namespace will now be module.<declared_name>["<index>"].<output_name>. Therefore, we can modify the value for the vpc_zone_identifier argument to have keys of string type instead of number type:
vpc_zone_identifier = [module.subnet["0"].subnet_id, module.subnet["1"].subnet_id]
and now the passed value to the argument will be the first two subnets as desired.

Access the index of a map in for_each

I have a map that looks like this
variable "mysubnets" {
type = map(string)
default = {
"subnet1" = "10.1.0.0/24"
"subnet2" = "10.1.1.0/24"
}
}
In my module I'm trying to place subnets in different availability zones in the same vpc
data "aws_availability_zones" "azs" {
state = "available"
}
resource "aws_subnet" "test-subnets" {
for_each = var.mysubnets
cidr_block = "${each.value}"
vpc_id = aws_vpc.myvpc.id
availability_zone = data.aws_availability_zones.azs.names[index("${each.value}")]
tags = {
Name = "${each.key}"
}
}
I can get the key and value from the map no problem, but when trying to pick an availability zone I can't find how to change the value. Is there a way to get the index of a map, or create a counter for a number that increments?
Your data source is called azs, not available. So it should be:
availability_zone = data.aws_availability_zones.azs.names[index("${each.value}")]
Update:
To use index with your var.mysubnets you can do as follows:
resource "aws_subnet" "test-subnets" {
for_each = {for idx, subnet in keys(var.mysubnets):
idx => {
name = subnet
cidr = var.mysubnets[subnet]
}
}
cidr_block = each.value.cidr
vpc_id = aws_vpc.myvpc.id
availability_zone = element(data.aws_availability_zones.azs.names, each.key)
tags = {
Name = each.value.name
}
}

Use output value in terraform object

I have multiple output variable, I want to make one parent out variable and then put other outputs into it. I have searched about it and found that we can user terraform object for it but can't get the syntax right.
Output.tf
output "public_subnet" {
value = "${module.my_vpc.public_subnets_ids}"
}
output "vpc_id" {
value = "${module.my_vpc.vpc_id}"
}
output "private_subnet" {
value = "${module.my_vpc.private_subnets_ids}"
}
I want my output to be in a object or you can say parent output variable that have all child output vales, I have come up with few line which I know is not right syntax wise but will get you a picture of what I am thinking of.
output "vpc" {
value = {
vpc_id = "${module.my_vpc.vpc_id}"
public_subnet = "${module.my_vpc.public_subnets_ids}"
private_subnet = "${module.my_vpc.private_subnets_ids}"
}
type = object({ vpc_id = string, public_subnet = string, private_subnet = string })
}
Terraform output does not have type. Therefore, your vpc should be:
output "vpc" {
value = {
vpc_id = "${module.my_vpc.vpc_id}"
public_subnet = "${module.my_vpc.public_subnets_ids}"
private_subnet = "${module.my_vpc.private_subnets_ids}"
}
}
But the issue is that a child module has no access to its parrent's outputs. Thus, I'm not exactly sure what do you want to achieve with your outputs. Normally, you would pass variables from parent to child using variable, and then you could make new output from those variables in the child module.
Update
Based on your previous questions, there is main.tf with
module "my_vpc" {
source = "./modules/vpc"
vpc_cidr = var.vpc_cidr
public_subnet = var.public_subnet
private_subnet = var.private_subnet
availability_zone = data.aws_availability_zones.azs.names
}
Therefore, you must have a folder ./modules/vpc. In the folder, there may be a file called ./modules/vpc/vpc.tf. The file will have something like this in it (variables could be in separate file as well):
variable "vpc_cidr" {}
variable "public_subnet" {}
variable "private_subnet" {}
variable "availability_zone" {}
# the rest of the VPC definition. Since the file is not given,
# i can only speculate on the exact details of the content
resource "aws_subnet" "public" {
count = length(var.public_subnet)
vpc_id = aws_vpc.my_vpc.id
# other attributes
}
resource "aws_subnet" "private" {
count = length(var.private_subnet)
vpc_id = aws_vpc.my_vpc.id
# other attributes
}
If so, then you can create a new file, called ./modules/vpc/output.tf with the content:
output "vpc" {
value = {
vpc_id = my_vpc.vpc_id
public_subnet = aws_subnet.public.*.id
private_subnet = aws_subnet.private.*.id
}
}

For Each availability zone within an AWS region

I am trying to create a subnet within the availability zones within a region using Terraform. I have the below code and am having some trouble getting my subnet to read into the availability zone for the region. Below is also my error.
data "aws_availability_zones" "azs" {
state = "available"
}
locals {
az_names = data.aws_availability_zones.azs.names
}
resource "aws_vpc" "main" {
for_each = var.environment
cidr_block = var.vpc_cidr
tags = {
Name = var.vpc_tags
}
}
resource "aws_subnet" "public" {
for_each = var.public_sub_cidr
vpc_id = aws_vpc.main[each.key].id
cidr_block = cidrsubnet(var.vpc_cidr, 0, each.value.public_subnet)
availability_zone = local.az_names[each.key]
map_public_ip_on_launch = true
tags = {
Name = "${var.vpc_tags}-PubSubnet"
}
}
Errors
Error: Unsupported attribute
on vpc.tf line 17, in resource "aws_subnet" "public":
17: cidr_block = cidrsubnet(var.vpc_cidr, 0, each.value.public_subnet)
|----------------
| each.value is ""
This value does not have any attributes.
Error: Invalid index
on vpc.tf line 18, in resource "aws_subnet" "public":
18: availability_zone = local.az_names[each.key]
|----------------
| each.key is ""
| local.az_names is list of string with 3 elements
The given key does not identify an element in this collection value: a number
is required.
Any advice on reading from availability zones and also assigning these public subnets would be appreciated.
UPDATE
I made some progress, updated my code to use for each with the "length" of my data resource. See code below and error:
resource "aws_subnet" "public" {
for_each = length(local.az_names)
vpc_id = aws_vpc.tableau[each.key].id
cidr_block = cidrsubnet(var.vpc_cidr, 8, each.value)
availability_zone = local.az_names[each.key]
map_public_ip_on_launch = true
tags = {
Name = "${var.vpc_tags}-PubSubnet"
}
}
Error: Invalid for_each argument
on vpc.tf line 15, in resource "aws_subnet" "public":
15: for_each = length(local.az_names)
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
number.
make: *** [apply] Error 1
Not sure what are you trying to achieve, but this is incorrect:
for_each = length(local.az_names)
it should be:
for_each = toset(local.az_names)
Since local.az_names is set now, you can only use each.key (each.value will be same as each.key). For example:
availability_zone = each.key
The following code creates a subnet in each AZ:
provider "aws" {
# your details
}
data "aws_availability_zones" "azs" {
state = "available"
}
locals {
az_names = data.aws_availability_zones.azs.names
}
variable "vpc_cidr" {
default = "10.0.0.0/16"
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
}
resource "aws_subnet" "public" {
for_each = {for idx, az_name in local.az_names: idx => az_name}
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, each.key)
availability_zone = local.az_names[each.key]
map_public_ip_on_launch = true
}

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.