Can anyone give me a tip?
I am creating an inventory file for Ansible using Terraform and would like to filter out a specific hostname.
Instances that have “ansible” in their name should not end up in the list.
There are two different ways this could be done, with the filtering either being in the call to the template or in the template itself.
Personally I tend to prefer to put non-formatting logic in the calling module and keep the template itself as just a straightforward projection of some data, so I would do it like this:
resource "local_file" "ansible_inventory" {
filename = "../Ansible/inventory.ini"
content = templatefile("./ansible_inventory.tpl", {
instances = [
for inst in module.openstack-instances.instance_info : inst
if !strcontains(inst.name, "ansible")
]
})
}
This derives a new list with some of the elements filtered out.
(FWIW, I also removed the depends_on because it was redundant. Terraform can already see the dependency due to the reference in the content argument. depends_on is only for “hidden dependencies”.)
This already seems relatively “simple” to me, but of course it depends on what you mean by “simple”.
One possible other way to write it would be to generalize it so that it works for any number of “classes” of network dynamically. I would consider that to be a more complicated solution rather than a simpler solution, but it does trade some additional complexity for less repetition:
locals {
network_classes = tomap({
internal = {
net_name_substring = "internal"
}
public = {
net_name_substring = "public"
}
})
instances_by_class = tomap({
for key, class in local.network_classes : key => tolist([
for inst in module.openstack-instances.instance_details : {
name = inst.name
ip_address_v4 = join("", [
for net in inst.networks : net.ip_address_v4 if strcontains(net.name, class.net_name_substring)
])
ip_address_v6 = join("", [
for net in inst.networks : net.ip_address_v6 if strcontains(net.name, class.net_name_substring)
])
}
])
})
}
This would then produce local.instances_by_class["internal"] and local.instances.by_class.["public"], with similar content as your two separate local values, and could grow to include an arbitrary number of other classes in future.
I’m not meaning to suggest that the above is better than what you wrote. If you don’t expect to ever need any more than these two fixed classes then I’d personally stick with your approach because it’s more straightforward and easier to read to understand what it’s producing.
I’m not sure I’m understanding correctly what you are intending with those join("", ...) parts.
Are you expecting these for expressions to yield either zero or one results and you want to assign the one result to the attribute? If so, that sort of thing is what the one function is aimed at:
ip_address_v4 = tostring(one([
for net in inst.networks : net.ip_address_v4 if strcontains(net.name, class.net_name_substring)
]))
In this example there are three possible outcomes:
if the for expression produces one string after filtering, ip_address_v4 will be set to that one string.
if the for expression produces zero strings after filtering, ip_address_v4 will be set to null, which is the usual way to represent “nothing” or “not set” in Terraform.
(the tostring type conversion there is to help hint to Terraform that the null is a placeholder for a string, which isn’t strictly necessary but will ensure a consistently-typed result if all of the objects end up having null here, and so Terraform would otherwise not have a hint for what type the attribute is supposed to have.)
if the for expression produces two or more strings after filtering, the one function will return an error so you can see clearly that something unexpected has happened.
This is the main advantage over join("", ...): with join this invalid case would succeed with an invalid string containing multiple IP addresses concatenated together, whereas one catches the problem and raises an error about it so you can then clearly see that something’s gone wrong and decide how to proceed.