Reputation: 19
I'm using provisioners in OpenTofu to firstly copy a script to a remote Linux machine:
provisioner "file" {
source = "scripts/base.sh"
destination = "/tmp/base.sh"
connection {
type = "ssh"
user = var.c_user
password = var.d_passwd_user
host = var.vm_ip
}
}
...and then using remote-exec to run some useful commands AND then execute the script with sudo (sudo is mandatory for what's in base.sh
).
provisioner "remote-exec" {
inline = [
"echo 'SSH connection successful' > /tmp/1ssh_test_flag",
"alias sudo='sudo -S <<< ${var.d_passwd_user}'",
"sudo chmod +x /tmp/base.sh",
"sudo /tmp/base.sh",
]
connection {
type = "ssh"
user = var.c_user
password = var.d_passwd_user
host = var.vm_ip
}
}
The variables you see in the command "alias sudo='sudo -S <<< ${var.d_passwd_user}", work perfectly fine in the inline command execution block so they are clearly made available to this command but the variables inside my script file base.sh
which refer to variables declared in variables.tf
and terraform.tfvars
are not seen by the script or bash and I get the error (bad substitution) when it tries to use them.
An example of whats in my base.sh
script file and what relies on the variables provided in terraform.tfvars
and variables provided by tofu apply -vars
:
#!/bin/bash
echo 'root:${var.new_root_passwd}' | chpasswd
subscription-manager register --org=XXX --activationkey=${var.satellite_key} && echo '${var.satellite_key}' > /opt/activationkey_satellite.txt
How does opentofu successfully pass variables to "alias sudo='sudo -S <<< ${var.d_passwd_user}'", but fail to pass variables to a remotely executing script?
Upvotes: 0
Views: 172
Reputation: 74594
OpenTofu's remote-exec
provisioner works by uploading the commands you provided into a script file on the remote system and then executing that script over SSH. The script executes in the environment of the shell on the remote system, and so it has no way to directly access any data from inside OpenTofu itself.
A typical way to deal with this situation is to dynamically generate a script using OpenTofu's template language, with the values you need already inserted into it, and then send that generated script to the server using remote-exec
.
However, generating a shell script by string concatenation can be tricky because the shell will interpret some characters as having a special meaning, rather than taking them literally. For example, if any of your variables contained spaces then you'd need to take care to quote or escape the spaces to make sure the shell doesn't treat them as the delimiter between two arguments.
I wrote a provider apparentlymart/bash
which understands Bash's syntax and knows how to escape values so you can use them in your generated script without worrying so much about special characters. Here's how I'd use that provider to solve your problem:
provisioner "remote-exec" {
inline = [
provider::bash::script(file("${path.module}/call_base.sh"), {
d_passwd_user = var.d_passwd_user
new_root_password = var.new_root_password
satellite_key = var.satellite_key
})
]
connection {
type = "ssh"
user = var.c_user
password = var.d_passwd_user
host = var.vm_ip
}
}
In call_base.sh
:
#!/bin/bash
set -efuo pipefail
# The provider::bash::script function will insert
# bash variables here matching the names and values
# given in the second argument, so you can refer
# to those variables (and ONLY those variables)
# using the normal bash interpolation syntax...
echo 'SSH connection successful' > /tmp/1ssh_test_flag
alias sudo='sudo -S <<< '"${d_passwd_user}"
sudo chmod +x /tmp/base.sh
# Since bash.sh is going to run as a child process, we
# need to "export" the variables it needs so it can
# inherit them from this process. Note that this will
# cause the root password to be visible in the child
# process's environment variable table, which is
# visible to any process running as the user which
# this outer script is running as.
export new_root_passwd
export satellite_key
sudo /tmp/base.sh
The provider::bash::script
function just takes the given script and inserts some variable declarations immediately after the interpreter line, so that substitutions like ${d_passwd_user}
will work. The variable declarations use Bash's variable declaration syntax, with quoting and escaping to make sure that Bash interprets the values correctly.
For the above to work you'd also need to declare a dependency on my provider, so that OpenTofu knows to install it:
terraform {
required_providers {
bash = {
source = "apparentlymart/bash"
version = "0.2.0"
}
}
}
The above tells OpenTofu which provider the bash
in provider::bash::script
refers to.
Upvotes: 0
Reputation: 18203
This happens because terraform/tofu knows nothing about things that are external to it. In other words, shell scripts (and other accompanying files) are not taken into account when doing variable substitution. To overcome this, you could use templatefile
built-in function. For example:
provisioner "file" {
source = templatefile("${path.module}/scripts/base.sh", {
satelite_key = var.satellite_key
new_root_passwd = var.new_root_passwd
})
destination = "/tmp/base.sh"
connection {
type = "ssh"
user = var.c_user
password = var.d_passwd_user
host = var.vm_ip
}
}
To make this work, you still have to edit the shell script to use the following:
#!/bin/bash
echo 'root:${new_root_passwd}' | chpasswd
subscription-manager register --org=XXX --activationkey=${satellite_key} && echo '${satellite_key}' > /opt/activationkey_satellite.txt
Upvotes: 2