How to extract generated attribute from a previously defined resource? - terraform

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[*])

Related

What does *.id mean in terraform?

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

How to pass multiple VPC CIDRs to security_group_rule resource in terraform V.15

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.

Dynamic data source in Terraform 12

I'm creating alerts (azurerm_monitor_scheduled_query_rules_alert) in Azure using Terraform. You can include a list of action groups (i.e. the groups that you send the alerts to).
Within the TFVars file I will pass in a variable value of a list of the names of the action groups. However, the alert module needs the ID's of the resources, not the names. So I have a data source that would get the info of an action group. The Alert resource can then refer to the data source to acquire the azure resource id.
This works fine if I have just one action group, but the size of the list with action group names can vary. I'm trying to figure out how I can convert all action group names to id for ingestion by the resource.
resource "azurerm_monitor_scheduled_query_rules_alert" "tfTestAlertExample" {
for_each = {for alert in var.scheduled_query_alerts : alert.name => alert}
name = each.value["name"]
location = data.azurerm_resource_group.resource_group.location
resource_group_name = data.azurerm_resource_group.resource_group.name
action {
# --This part here. How do I get make this dynamic?--
action_group = [
data.azurerm_monitor_action_group.action_group.id
]
email_subject = each.value["email_subject"]
custom_webhook_payload = "{}"
}
data_source_id = ................ etc
So in the above example, there will only be one action{} block, but the Action_group list within that needs to be dynamic, with ID's retrieved from a data source. Or maybe there's another way of doing this that I've not considered.
Any help would be greatly appreciated.
If you just want to convert the list of action group names to its Ids, you can do it like this:
# declare the variables
variable "action_group_names" {
default = ["nancyAG1","nancyAG2"]
}
# retrieve the Id of action group
data "azurerm_monitor_action_group" "example" {
count = length(var.action_group_names)
resource_group_name = "existingRG"
name = element(var.action_group_names,count.index)
}
# output the result to the terminal
output "groups_id" {
value = data.azurerm_monitor_action_group.example[*].id
}
Then pass Ids to the resource like this:
resource "azurerm_monitor_scheduled_query_rules_alert" "example" {
name = format("%s-queryrule", var.prefix)
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
action {
action_group = data.azurerm_monitor_action_group.example[*].id
email_subject = "Email Header"
custom_webhook_payload = "{}"
}
Check the action_group Ids.

proper way to use nested variables in terraform

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}"
}

Terraform List of Maps

I currently have a single resource defined for
aws_ebs_volume & aws_volume_attachment
I use a count based on a variable to determine how many devices I want to created followed by a count on the attachment:
data_volumes = ["50"]
data_device = ["xvde"]
resource "aws_ebs_volume" "datavolumes" {
count = "${length(var.data_volumes)}"
size = "${var.data_volumes[count.index]}"
tags = "${var.instance_tags}"
encrypted = "true"
availability_zone = "us-east-2b"
kms_key_id = "${var.kms_key}"
}
resource "aws_volume_attachment" "attachvolumes" {
count = "${length(var.data_volumes)}"
device_name = "${var.data_device[count.index]}"
volume_id = "${aws_ebs_volume.datavolumes.*.id[count.index]}"
instance_id = "${aws_instance.general.id}"
}
I'm struggling with finding a way to assign unique tags to each of these volumes that get created, as you can see I'm using a static list of "instance_tags" for each of the volumes but I'd like to have unique tags applied to each of the volumes. I'm trying to avoid having to specify a resource/volume but might be easiest at this point.
Hoping someone can help me understand if its possible and an example of what it looks like.
I think I found an approach that works for what I'm trying to accomplish:
resource "aws_ebs_volume" "datavolumes" {
count = "${length(var.data_volumes)}"
size = "${var.data_volumes[count.index]}"
tags = "${merge(
var.instance_tags,
map(
"DriveLetter", "${var.data_letters[count.index]}",
"DriveLabel", "${var.data_labels[count.index]}"
)
)}"
data_volumes = ["50","50","50"]
data_device = ["xvde","xvdf","xvdg"]
data_letters = ["E:", "F:", "G:"]
data_labels = ["Data", "Logs", "TempDB"]
This gives me unique EBS volumes with Unique tag properties for each volumes and maintains my single resource

Resources