I am pretty new to Terraform. I am having some trouble trying to understand what terraform is doing here:
output "subnet_ids" {
value = aws_subnet.private.*.id
}
In the aws_subnet resource block we have
resource "aws_subnet" "private" {
vpc_id = var.vpc_id
cidr_block = element(split(",", var.cidrs), count.index)
availability_zone = element(split(",", var.azs), count.index)
count = length(split(",", var.cidrs))
tags = {
Name = "${var.name}-${count.index == 0 ? "a" : "b"}"
}
lifecycle {
ignore_changes = [availability_zone]
}
}
What is being referred by aws_subnet.private.*.id?
You are creating the subnets using the count meta-argument [1]. This will result in having a list of aws_subnet resources. To access a single element of a list, you would usually have to specify an index in any other programming language. The same applies in terraform, so e.g., you can access a single element with aws_subnet.private[0].id. Terraform is providing you with a wildcard (*) also known as the splat expression [2] so you can fetch all the elements of a list instead of using the index to get one by one. I also think that is the old syntax and that aws_subnet.private[*].id should work as well. Basically, the splat expression is just a short version of a for loop which you would have to use otherwise to get all the elements of a list.
The .id part fetches the ID attribute of a subnet. You could do the same for any other attribute of that resource. So in short: the splat expression helps you get all the .id attributes from all the private subnets which were created using the count meta-argument.
[1] https://developer.hashicorp.com/terraform/language/meta-arguments/count
[2] https://developer.hashicorp.com/terraform/language/expressions/splat
Related
This is not related to aws but to a technique of extracting data from a resource collection; therefore the content is most likely not correct relative to aws provider. I just used some words from that provider to prove the idea.
Given that the aws_instance.web resources are created as a collection by use of a for_each loop like described below:
resource "aws_instance" "web" {
for_each = {for k,v in var.input_var: k => v if v.enabled}
name = each.key
ami = each.value.ami
instance_type = each.value.instance_type
}
resource "aws_db_instance" "db" {
for_each = var.another_map
aws_instance_id = aws_instance.web[index(aws_instance.web[*].name, each.value.name)].id
}
At creation of the first collection of resources, to each element is assigned a unique read-only id by terraform/provider. Given that var.input_var.key is always unique, results that also aws_instance.web.name will always be unique for each element created.
In the second resources block, I also use a for_each loop to cycle through all elements of var.another_map. I want to attribute to aws_instance_id, the generated id from the first resources collection. So I need to first find the element from aws_instance.web where the name of it is equal to each.value.name while creating aws_db_instance.db and than extract the id form it.
I tried several ways to achieve this. The closest one is the one exposed above: aws_instance.web[index(aws_instance.web[*].name, each.value.name)].id.
So there are two questions that arise from this:
What is the type of aws_instance.web (a list of objects, a map of objects, an object which contains a map)?
How would a correct syntax would look like for matching the element and extracting the id from it?
Researching on Matt Schuchard's answer and expanding on it, the output of first resource creation is object(map(object)) where the keys of the generated map are the same keys of the input variable var.input_var.
eg: given the input variable
input_var = {
"website1" = {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
other_var = "some value"
},
"webst2" = {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
other_var = "some other value"
}
}
resource "aws_instance" "web" block would produce a aws_instance.web variable with contents like:
aws_instance.web = {
"website1" = {
id = 43262 # (read-only) generated by `aws_instance` resource block
name = "website1"
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
# other entries generated by `aws_instance` resource block
},
"webst2" = {
id = 43263 # (read-only) generated by `aws_instance` resource block
name = "webst2"
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
# other entries generated by `aws_instance` resource block
}
}
So when trying to access a specific element from aws_instance.web in the second resource block (aws_db_instance.db), one can access it by its key, rather than trying to match its name attribute.
Therefore, the line aws_instance_id = aws_instance.web[index(aws_instance.web[*].name, each.value.name)].id should be replaced with aws_instance_id = aws_instance.web[each.value.name].id, if and only if the set of names is a subset of the keys of aws_instance.web (I will represent this like each.value.name[*] ⊆ aws_instance.web.keys[*])
I need to pass the list of VPC CIDR ranges to an aws_security_group_rule resource.
I'm using terraform version: v.15.0
Here is the code I'm using:
variable "list_of_vps" {
description = "CIDR covering kops compute nodes"
type = list
default = ["vpc-foo", "vpc-bar"]
}
data "aws_vpcs" "list_of_vpcs"{
count = length(var.list_of_vps)
filter {
name = "tag:Name"
values = ["vpc-${element(var.list_of_vps, count.index)}"]
}
}
data "aws_vpc" "get_vpc_id" {
count = length(data.aws_vpcs.list_of_vpcs.ids)
id = tolist(data.aws_vpcs.list_of_vpcs.ids)[count.index]
}
resource "aws_security_group_rule" "ingress" {
count = length(data.aws_vpcs.list_of_vpcs.ids)
type = "ingress"
protocol = "tcp"
from_port = 5432
to_port = 5432
cidr_blocks = [data.aws_vpc.get_vpc_id[count.index].cidr_block]
security_group_id = module.postgress.postgress_security_group_id
}
I am m getting this below error.
on data.tf line 10, in data "aws_vpc" "get_vpc_id":
10: count = length(data.aws_vpcs.list_of_vpcs.ids)
Because data.aws_vpcs.list_of_vpcs has "count" set, its attributes must be accessed
on specific instances.
For example, to correlate with indices of a referring resource, use:
data.aws_vpcs.list_of_vpcs[count.index]
Error: Missing resource instance key
on data.tf line 15, in data "aws_vpc" "get_vpc_id":
15: id = tolist(data.aws_vpcs.get_vpc_id.ids)[count.index]
Because data.aws_vpcs.prod has a "count" set, its attributes must be accessed
on specific instances.
For example, to correlate with indices of a referring resource, use:
data.aws_vpcs.list_of_vpcs[count.index]
Can someone help me with this, please?
Terraform seems to be returning this error because of your expression data.aws_vpcs.list_of_vpcs.ids. That expression isn't valid, because data.aws_vpcs.list_of_vpcs is a list of objects rather than a single object, and so you'd need to tell Terraform which element of the list you want to access the .id attribute from.
However, I imagine your goal here was instead to get the number of elements in the list, in which case you can get there by asking Terraform for the length of the list of objects itself, rather than of a hypothetical attribute of that list:
count = length(data.aws_vpcs.list_of_vpcs)
For your other error in the expression with the tolist call, I'm a little less sure what your intention was. It seems like your module is taking a set of names of single VPCs and your goal that for each one of those you want to find the corresponding VPC with that name and determine its CIDR block. Since you only expect to find one VPC per name in that list I don't think you need the data.aws_vpcs.list_of_vpcs at all: that is for finding multiple VPCs matching particular criteria. Instead, you can filter by the Name tag directly in the singlular data.aws_vpc data source. Perhaps like this:
variable "vpc_names" {
type = set(string)
}
data "aws_vpc" "selected" {
for_each = var.vpc_names
tags = {
Name = each.value
}
}
resource "aws_security_group_rule" "ingress" {
for_each = data.aws_vpc.selected
type = "ingress"
protocol = "tcp"
from_port = 5432
to_port = 5432
cidr_blocks = [each.value.cidr_block]
security_group_id = module.postgress.postgress_security_group_id
}
The above tells Terraform to look up one VPC per element of var.vpc_names, expecting to find exactly one VPC with the given name (it'll fail if there isn't exactly one). It then declares a security group rule for each of those VPCs, where each.value.cidr_block means to use the cidr_block attribute from the current element of aws_vpc.selected.
I'm defining a number of subnet resources:
resource "aws_subnet" "my_subnets" {
count = 8
cidr_block = cidrsubnet(var.cidr_block, 3, count.index)
vpc_id = var.vpc
}
I then have to pass a list of those subnet IDs to another resource. I know that the IDs are reachable on aws_subnet.my_subnets[count].id, but how do I loop through those and append all of the values to a list in order to pass it to the other resource?
The recommendation I've seen is to tag the subnets, then use a data attribute to look up those subnets, and they will be returned in a list format, but I have the IDs right there on the output of the resource.
Found what I was thinking of - splat expression: https://www.terraform.io/docs/language/expressions/splat.html
In my terraform script, I have
resource "azuread_application" "main" {
count = "${length(var.sp_names)}"
name = "${sp_prefix}-${var.sp_names[count.index]}"
available_to_other_tenants = false
}
resource "azuread_service_principal" "main" {
count = "${length(var.sp_names)}"
application_id = "${azuread_application.main.["${sp_prefix}"-"${var.sp_names[count.index]}"].application_id}"
}
when I ran terraform init I get the following error:
An attribute name is required after a dot.
what is the right way to use nested variables and a list object?
In order for a resource to be represented as a map of instances rather than a list of instances, you need to use for_each instead of count:
resource "azuread_application" "main" {
for_each = { for n in var.sp_names : n => "${var.sp_prefix}-${n}" }
name = each.value
available_to_other_tenants = false
}
The for_each expression above is a for expression that transforms your list or set of names into a mapping from the given names to the prefixed names. In the other expressions in that block, each.key would therefore produce the original given name and each.value the prefixed name.
You can then similarly use for_each to declare the intent "create one service principal per application" by using the application resource's map itself as the for_each expression for the service principal resource:
resource "azuread_service_principal" "main" {
for_each = azuread_application.main
application_id = each.value.application_id
}
In this case, the azuread_application.main value is a map from unprefixed names to objects representing each of the declared applications. Therefore each.key in this block is the unprefixed name again, but each.value is the corresponding application object from which we can access the application_id value.
If your var.sp_names had a string "example" in it, then Terraform would interpret the above as a request to create two objects named azuread_application.main["example"] and azuread_service_principal.main["example"], identifying these instances by the var.sp_names values. This is different to count where the instances would have addresses like azuread_application.main[0] and azuread_service_principal.main[0]. By using for_each, we ensure that adding and removing items from var.sp_names will add and remove corresponding instances from those resources, rather than updating existing ones that happen to share the same numeric indices.
I am assuming you are using a version older that 0.12.x. If not the answer from Martin is the best one.
You need to leverage the splatting.
resource "azuread_service_principal" "main" {
count = "${length(var.sp_names)}"
application_id = "${azuread_application.main.*.application_id}"
}
Just been trying to use the new terraform aws_route53_resolver_endpoint resource. It takes the subnet ids as a block type list. Unfortunately there appears to be no way to populate this from a list of subnets read from an output variable from the previous step.
Basically I have a set of subnets created using the count on the subnet resources in a previous step. Im trying to use these and setup aws_route53_resolver_endpoint in each of these subnets:
resource "null_resource" "management_subnet_list" {
count = "${length(var.subnet_ids)}"
triggers {
subnet_id = "${element(data.terraform_remote_state.app_network.management_subnet_ids, count.index)}"
}
}
resource "aws_route53_resolver_endpoint" "dns_endpoint" {
name = "${var.environment_name}-${var.network_env}-dns"
direction = "OUTBOUND"
security_group_ids = ["${var.security_groups}"]
ip_address = "${null_resource.management_subnet_list.*.triggers}"
}
The above when run, results in an error: ip_address: should be a list
If I modify the code as follow:
ip_address = ["${null_resource.management_subnet_list.*.triggers}"]
I get the error: ip_address: attribute supports 2 item as a minimum, config has 1 declared
I can't seem to figure out any other way to create the resource list dynamically from a list of subnets.
Any help will be appreciated.
Per the resource reference for aws_route53_resolver_endpoint, the subnet_id in the ip_address block is a single string value.
To specify multiple subnets, you need to have multiple ip_address blocks.
Since you state that you're creating subnets with a count argument, you could potentially reference each individually with the index like: aws_subnet.main[0].id, aws_subnet.main[1].id and so on, each in it's own ip_address block. (or for Terraform 0.11, I think it was "${aws_subnet.main.0.id}".)
However, a better way would be to use the Dynamic Blocks available in Terraform 0.12 +
Dynamic Blocks allow you to create repeatable nested blocks within top-level blocks.(resource, data, provider, and provisioner blocks currently support dynamic blocks).
A dynamic ip_address block within the aws_route53_resolver_endpoint resource could look like:
dynamic "ip_address" {
for_each = aws_subnet.main[*].id
iterator = subnet
content {
subnet_id = subnet.value
}
}
Which would result in a separate ip_address nested block for each subnet created in the aws_subnet.main resource.
The for_each argument is the complex value to iterate over. It accepts accepts any collection or structural value, typically a list or map with one element per desired nested block.
For complete info on the dynamic nested block expression, see the Terraform documentation at: https://www.terraform.io/docs/language/expressions/dynamic-blocks.html