How to Safely Pass Variables to Terraform local-exec Scripts

How to Safely Pass Variables to Terraform local-exec Scripts

Including how to pass complex types

The local-exec provisioner in Terraform is a powerful tool for making your configurations do more than what is natively supported by a provider. One of the things that makes it powerful is the ability to pass values from your Terraform configuration to the script or commands being executed by local-exec.

There are two primary ways that I know of to pass values to local-exec commands. This article will introduce both and show why one should almost always be preferred over the other, I will also demonstrate how to safely pass complex types.

The examples in this article use PowerShell in the command block of local-exec, but the same concepts can be applied to other scripting languages.

Passing values to local-exec scripts

String interpolation (unsafe)

I have seen many examples, including some in official Terraform documentation, that interpolate values directly into executable lines of code in the command block using the interpolation syntax (${..}) like this...

Which results in the following output...

bad_example1_result.png

The output shows that the previous example works, the value of local.my_var was passed into the command, but it's not safe and it should probably be considered a bad practice.

Consider what happens if the value of my_var is something like "12\" = 1'"

You're gonna have a bad time... uh-oh.png

There are a couple of things to think about here:

  1. Terraform parses the escape sequence when it interpolates the string, so the value that's actually being injected is 12" = 1' resulting in a command that looks like:

    write-host "my_var value: 12" = 1'"
    

    Notice that the " in 12" = 1' is terminating the opening quote and leaving an unpaired ' that's not enclosed in double quotes, which is what causes the error.

  2. Even if Terraform didn't parse the escape sequence when it injected the value, Powershell has a completely different set of rules regarding how to escape characters and which characters need to be escaped. Using string interpolation, we would need to care for that in a manner specific to the language that the value is being passed to.

Fortunately, there is a safer way to do this that avoids both of these issues.

The environment argument (safe)

Terraform's local-exec provisioner has an argument called environment to define environment variables that are accessible by the script/commands being run by local-exec. The argument takes key value pairs where the key is the environment variable's name and the value is a string that will be assigned to the environment variable's value.

Here's how environment can be used to make the broken example from above work...

When we look at the output, we can see that the script was executed successfully and interpreted the value as we expected...

good_output1.png

Why does this work?

  1. Terraform created an environment variable, MyVar containing the value of the unescaped string: 12" = 1'
  2. PowerShell accessed the environment variable as $Env:MyVar. Because PowerShell is using a native variable, the value doesn't need to be escaped. PowerShell variables, including environment variables, are not interpreted as executable code unlike when we interpolated ${local.my_var} into the command block directly.

Accessing environment variables isn't a feature exclusive to PowerShell, most widely used scripting languages have some method for retrieving values from environment variables.

A note about environment variables

Environment variables are always cast as string types. If your script is expecting a different type (like an int), you may need to recast the variable to that type depending on the language you're using.

PowerShell will convert strings containing numbers to the appropriate type automagically when necessary, but you may encounter an error if the string contains a value that it cannot convert.

Passing complex types

Terraform has a number of complex types including lists, maps, and objects. In the example above, we passed a string, which is considered a primitive type, to our commands in the command block.

When using a scripting language like PowerShell that has its own robust support for complex types, we may encounter a circumstance where we want our command block to consume a complex type from Terraform.

We know that environment variables are always of type string, so how do we use them to pass a complex type like a list or an object?

Serialization

For our purposes, serialization can be defined as the process of converting a complex data type into a string that can be stored or sent to another application and can be deserialized back into a complex data type later. One popular and widely supported serialization format is JSON.

Fortunately, Terraform has a jsonencode that can be used to serialize its complex types into a JSON string.

PowerShell has its own commands for serializing and deserializing JSON, ConvertTo-Json and ConvertFrom-Json.

We can use Terraform's jsonencode to convert a complex type to a JSON string, which we can assign to an environment variable in local-exec's environment argument. Inside the command block, we can use PowerShell's ConvertFrom-Json command to deserialize the value of the environment variable. Here's an example of how this works to pass a list from Terraform to the command block...

From the output, we can see that our foreach command successfully iterated through each of the items in the list...

complex_output1.png

The same concept can be used to pass other complex types from Terraform to the local-exec command block.

While my example uses PowerShell and ConvertFrom-Json, most scripting languages with support for complex types (Python, Ruby, Perl, etc.) have their own mechanisms for deserializing JSON strings.