johnmacward
johnmacward

Reputation: 19

Terraform / OpenTofu provided variables are not available to remotely executed scripts

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

Answers (2)

Martin Atkins
Martin Atkins

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

Marko E
Marko E

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

Related Questions