proper way to use nested variables in terraform - 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}"
}

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 extract generated attribute from a previously defined resource?

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

Conditional creation of parent/child resources

I have a Terraform parent-resource that gets created conditionally, by using the count meta arg. This works fine. However, if the parent-resource doesn't get created because count is set to 0, and it has dependent child-resources, Terraform will fail. Is there a practical way to tell Terraform to ignore the children-resources, if the parent doesn't get created? The only way I can think to do it is to perform a count operation on each resource, and this seems cumbersome.
Something like this:
create_dev_compartment = 0
create_dev_subnet *skip creation*
create_dev_instance *skip creation*
create_mgt_compartment = 1
create_mgt_subnet *create resource*
create_mgt_instance *create resource*
The Terraform documentation has a section Chaining for_each between resources which describes declaring chains of resources that have the same (or derived) for_each expressions so that they can all repeat based on the same source information.
The documentation doesn't include an explicit example of the equivalent pattern for count, but it follows a similar principle: the count expression for the downstream resource will derive from the value representing the upstream resource.
Since you didn't include any Terraform code I can only show a contrived example, but here's the general idea:
variable "manage_network" {
type = bool
}
resource "compartment" "example" {
count = var.manage_network ? 1 : 0
}
resource "subnet" "example" {
count = length(compartment.example)
compartment_id = compartment.example[count.index].id
}
resource "instance" "example" {
count = length(subnet.example)
subnet_id = subnet.example[count.index].id
}
In the case of chained for_each, the full object representing the corresponding upstream resource is temporarily available as each.value inside the downstream resource block. count can't carry values along with it in the same way, so the equivalent is to refer to the upstream resource directly and then index it with count.index, which exploits the fact that these resources all have the same count value and will thus all have the same indices. Currently the only possible index will be zero, because you have a maximum count of 1, but if you change count in future to specify two or more instances then the downstream resources will all grow in the same way, creating several correlated instances all at once.

How to add a resource using the same module?

Terraform newbie here. I've a module which creates an instance in GCP. I'm using variables and terraform.tfvars to initialize them. I created one instance successfully - say instance-1. But when I modify the .tfvars file to create a second instance (in addition to the first), it says it has to destroy the first instance. How can I run the module to 'add' an instance, instead of 'replacing the instance'? I know the first instance which was created is in terraform.tfstate. But that doesn't explain the inability to 'add' an instance.
Maybe I'm wrong, but I'm looking at 'modules' (and its config files) as functions- such that I can call them anytime with different parameters. That does not appear to be the case.
Terraform will try to maintain the deployed resources matching your resources definition.
If you want two instances at the same time, then you should describe them both in your .tf file.
Ex. same instances, add a count to your definition
resource "some_resource" "example" {
count = 2
name = "example-${count.index}"
}
Ex. two different resources with specific values
resource "some_resource" "example-1" {
name = "example-1"
size = "small"
}
resource "some_resource" "example-2" {
name = "example-2"
size = "big"
}
Better you can set the specific values in tfvars for each resource
resource "some_resource" "example" {
count = 2
name = "example-${count.index}"
size = ${vars.mysize[count.index]}
}
variable mysize {}
with tfvars file:
mysize = ["small", "big"]

terraform route53 resolver setup

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

Resources