Search code examples
terraformterraform-provider-awsansible-inventoryterraform-template-file

format( ) function not not evaluating in Terraform template file within "for" directive


First a little context to question, so maybe someone can redirect me to correct answer/post. I have been learning Terraform for a few days, and now want to generate an "ansible inventory" as I deploy my tf files.

After lot of beginner searching I found data "template_file" is heavy to beginner like me, but I could SO-Frankenstein-monster my way to:

resource "local_file" "inventory" {
  content = templatefile("${path.module}/templates/hosts.tpl",
    {
      master        = aws_instance.master.*.public_ip
      nodes_ubuntu  = aws_instance.node.*.public_ip
     }
  )
  filename = "./inventory"
}

With a template file formatted like:

[master]
%{ for ip in master ~}
control ansible_host=${ip}
%{ endfor ~}

[nodes_ubuntu]

%{ for ip in nodes_ubuntu ~}

$${format{"withCurly-%02d",index + 1}} ansible_host=${ip}

$${format("WithRound-%02d",index + 1)} ansible_host=${ip}

format("PureFormat-%02s",index+1) ansible_host=${ip}

#These are possible combinations I used and failed for format()

%{ endfor ~}

Which results in confusing:

[master]
control ansible_host=ip.add.re.ss

[nodes_ubuntu]

${format{"withCurly-%02d",index + 1}} ansible_host=ip.add.re.ss

${format("WithRound-%02d",index + 1)} ansible_host=ip.add.re.ss

format("PureFormat-%02s",index+1) ansible_host=ip.add.re.ss

while,

${format("WithRound-%02d",index + 1)} ansible_host=${ip}

Generates error Invalid value for "vars" parameter: vars map does not contain key "index"

I was expecting it to be something like:

[nodes_ubuntu]
ubuntu-01 ansible_host= ip.add.re.ss

How can I correct the code, or is it Terraform bug, since I saw SIMILAR code worked but forgot to note down reference.


Solution

  • The first result you described, where the function calls remained in the output, is because you used $${ to escape the normal ${ template interpolation sequence, asking Terraform to insert a literal ${ instead. That would be an appropriate thing to do if you were generating source code for another language which uses ${ to mean something itself, but since you want Terraform to evaluate that interpolation you must use the non-escaped ${ form, as you tried in your second case.

    The problem you saw when you fixed the incorrect escaping is that you haven't declared any symbol index either in the call to templatefile or locally within the template.

    If your goal is to use the index of each element of nodes_ubuntu then you can achieve that by declaring a second symbol in the for directive. If you give two symbols then the first one represents the key/index while the second one represents the value:

    %{ for index, ip in nodes_ubuntu ~}
    ${format("WithRound-%02d", index + 1)} ansible_host=${ip}
    %{ endfor ~}
    

    This is a valid way to work with a list of resource objects declared using count, but before adopting this strategy be sure to consider how this will behave under future maintenance: Terraform is tracking these objects by their index in the count, and so if you increase the resource count later that'll add a new index to the end. If you decrease count then you'll always remove entries from the end of this list. If you replace any one of the hosts then the corresponding index will have its IP address change in this template result.

    Those restrictions are just fine if these multiple virtual machines are more like "cattle" than "pets", which is to say that if you need to destroy one of them they are all equally viable to destroy. It's not a good design if any of them have unique characteristics that might cause you to want to specifically destroy one in the middle of the list.

    In your case it looks like this will be appropriate, because you've already distinguished the "master" and "node" roles in separate resources and so the nodes are hopefully all fungible. I mention it only because it's a relatively common gotcha to write a Terraform config which implies that instances are fungible when they actually aren't, which can cause maintenance problems later.

    (More information in When to Use for_each Instead of count.)