Terraform Template - create ansible inventory - Filter Hostname

Hello,

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.

main.tf:

...
resource "local_file" "ansible_inventory" {
  depends_on = [module.openstack-instances]
  filename   = "../Ansible/inventory.ini"
  content = templatefile("./ansible_inventory.tpl", {
    instances = module.openstack-instances.instance_info
  })
}

ansible_inventory.tpl:

# Ansible Inventory generated by Terraform
#
%{ for inst in instances ~}
${inst.name} ansible_host=${inst.internal_ip}
%{ endfor ~}

Hi @EddyMaestroDev,

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”.)

1 Like

Thanks :slight_smile: :smile:

I have had to make an extension in the meantime. The instances will have multiple networks and addresses. How could it be filtered here?

I have changed the output for this. Now I want to use the IP address only from a specific network.

output "instance_info" {
  value = [
    for idx, inst in openstack_compute_instance_v2.instances : {
      name              = inst.name
      networks          = [
        for net in inst.network : {
          name          = net.name
          ip_address_v4 = net.fixed_ip_v4
          ip_address_v6 = net.fixed_ip_v6
        }
      ]
      floating_ip       = try(openstack_networking_floatingip_v2.floating_ip[idx].address, null)
      tags              = inst.tags
    }
  ]
}

I have solved it as follows. Is there an easier way?
I could only convert the tuples to a string using “join”.

locals {
  internal_instances = [
    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, "internal")])
      ip_address_v6 = join("", [for net in inst.networks : net.ip_address_v6 if strcontains(net.name, "internal")])
    }
  ]

  public_instances = [
    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, "public")])
      ip_address_v6 = join("", [for net in inst.networks : net.ip_address_v6 if strcontains(net.name, "public")])
    }
  ]
}



## Create Ansible.cfg and a Inventory File
resource "local_file" "ansible_config" {
  depends_on = [module.openstack-instances]
  filename   = "../Ansible/ansible.cfg"
  content = templatefile("./ansible_cfg.tpl", {
    instance = [
      for inst in local.public_instances : inst
      if strcontains(lower(inst.name), "lb")
    ]
  })
}

resource "local_file" "ansible_inventory" {
  depends_on = [module.openstack-instances]
  filename   = "../Ansible/inventory.ini"
  content = templatefile("./ansible_inventory.tpl", {
    instances = [
      for inst in local.internal_instances : inst
      if !strcontains(inst.name, "ansible")
    ]
  })
}

Hi @EddyMaestroDev,

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.

1 Like