How to interpolate in Terraform

Hi,
I am trying to execute a small bash command with local-exec in terraform

But I need to do the interpolation

This is how my code looks like:

resource "null_resource" "web3" {

 triggers  = {
    key = "${uuid()}
  provisioner "local-exec" {
    command = "num1=${var.minion-count} && num2=1 && num3=$[num1 - num2] && for i in $(seq 0 $num3); do echo ${aws_instance.kubernetes_minion.<value-of-i>.public_ip} > /etc/ansible/machan; done"
   
  }
 }

I need to pass the value of i to where it says “value-of-i”, so I did as below

resource "null_resource" "web3" {

 triggers  = {
    key = "${uuid()}
  provisioner "local-exec" {
    command = "num1=${var.minion-count} && num2=1 && num3=$[num1 - num2] && for i in $(seq 0 $num3); do echo ${aws_instance.kubernetes_minion.$i.public_ip} > /etc/ansible/machan; done"
   
  }
 }

And give the error:

Expected the start of an expression, but found an invalid expression token.

Can someone please help me how to properly do this?

Hi @iamdempa,

It looks like there are several syntax errors in your configuration even before you introduced the $i interpolation to the shell string. Here’s my attempt to fix the syntax of your first example:

resource "null_resource" "web3" {
  triggers  = {
    key = uuid()
  }

  provisioner "local-exec" {
    command = "num1=${var.minion-count} && num2=1 && num3=$[num1 - num2] && for i in $(seq 0 $num3); do echo IP_ADDRESS_HERE > /etc/ansible/machan; done"
  }
 }

The above would still not do what you intended of course, but Terraform should at least be able to parse it and run the local-exec provisioner and see the shell fail to process it.

With that said, I think there is a problem with your approach here: you are trying to iterate inside the shell but iterate over data that exists inside Terraform. The shell can’t see the data from Terraform and Terraform can’t see data from the shell, so this is not possible: your i variable exists only in the shell, and aws_instance.kubernetes_minion exists only inside Terraform.

It is possible in theory to have Terraform generate a shell script that contains multiple IP addresses in a variable and then have the shell iterate over the contents of that variable, but provisioners are a last resort so we should first consider if there is a different way to solve this problem without provisioners at all.

It looks like your underlying goal is to generate a file /etc/ansible/machan which contains a list of IP addresses. Using Terraform to manage system-level files is quite unusual – Terraform is primarily for managing remote infrastructure objects, not local system configuration – but in unusual situations where you need to generate files from Terraform the local_file resource type allows you to declare that a file ought to exist using a normal Terraform resource, without any provisioners:

resource "local_file" "example" {
  content  = join("\n", aws_instance.kubernetes_minion[*].public_ip)
  filename = "/etc/ansible/machan"
}

The above still has a problem: unless you are running Terraform as root (which would also be very unusual) it will likely not be able to create a file in /etc/ansible. I might instead prefer to generate a file in the local working directory (like "${path.cwd}/ansible-machan") and then copy that file into /etc/ansible/machan as a separate step once Terraform has succeeded, so that only that copy operation would be run with elevated permissions.

Wow! This answer is quite good. Thank you for your time. I will have to try this and get back to you. TY @apparentlymart

@apparentlymart is it possible to add a content as follows?

[kube-master]
<some-ips>
[kube-minion]
<some-ips>

Hi @iamdempa,

That seems like a good use-case for the templatefile function, which use Terraform’s string template syntax with templates in external files.

For example, you could create a new file roles.ini.tmpl (or whatever you want) in the same directory as your module’s .tf files with the following content:

[kube-master]
%{ for addr in master_ip_addrs ~}
${addr}
%{ endfor ~}
[kube-minion]
%{ for addr in minion_ip_addrs ~}
${addr}
%{ endfor ~}

Then replace your content expression with a call to render that template:

  content = templatefile("${path.module}/roles.ini.tmpl", {
    master_ip_addrs = aws_instance.kubernetes_master[*].public_ip
    minion_ip_addrs = aws_instance.kubernetes_minion[*].public_ip
  })

Terraform is getting really better…

So one question before I try this exciting feature. I don’t use any modules in my repo. So in the root directory I have main.tf and other .tf files. I don’t have a module directory. So my directory is like this:

/
  main.tf
  variable.tf
  terraform.tfvars
  src /
        Readme.md

So basically all .tf files are in the root directory and there is only one folder src which doesn’t contain anything related to the terraform

So how can I then give the path to my template if my template file is put in the root directory as below:

/
  main.tf
  variable.tf
  terraform.tfvars
  hosts.tmpl # new template
  src /
        Readme.md

So is this how this path should be?

templatefile("./roles.ini.tmpl", ...

Thank you for your time :slight_smile:

Hi @iamdempa,

The root directory containing .tf files is, in most ways, just another module – we call it the “root module” – and so it still makes sense to use path.module in there to refer to files in the same directory as the .tf file containing the expression.

You can specify the path to that hosts.tmpl file you showed as "${path.module}/hosts.tmpl". path.module will resolve just to . in this case, resulting in ./hosts.tmpl.

1 Like

Thank you @apparentlymart for this nice explanation. :slight_smile: