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