The Grayzone

Using Terraform External Provider with Powershell

Background

I’m continuing to learn a lot while working on Terraform configurations with Azure. As I mentioned in my previous post there are a few Azure resources and data sources that are not yet supported by Terraform. I’ve looked at a few different methods of handling these and one that I’ve been using recently is the External Provider.

As well as fitting into the Terraform workflow, it allows you to pass variables to the script and receive variables back. This last point is the main reason I’ve been using it as it lets you expose the return values as output variables or use them elsewhere in your configuration.

I thought I’d write a brief overview of how to use an External Provider to call a Powershell script.

External Provider

The External Provider is intended for simple scripts to integrate into the Terraform workflow, as long as they adhere to a specific protocol. This protocol is relatively simple:

There are only 2 available arguments for that the External Provider takes:

A sample External Provider is shown below:

data "external" "powershell_test" {
  program = ["Powershell.exe", "./testScript.ps1"]

  query = {
    foo = "${var.someString}"
    bar = "Hardcoded"
  }
}

This will run testScript.ps1 and pass in a JSON string of { "foo": "<value from var>", "bar" : "Hardcoded" }. This is a primitive example using a hardcoded string and a variable but you can use any standard interpolated values to pass computed values etc to the script.

The Powershell Script

Reading stdin

One of the tricky parts, possibly because I’ve not done an awful lot with Powershell until recently, was how to read the values passed to it via stdin. It turns out that this can be acheived via a call to [Console]::In.ReadLine() which will read the “query” parameter as a string. This can then be converted to a JSON object:

# Read stdin as string
$jsonpayload = [Console]::In.ReadLine()

# Convert to JSON
$json = ConvertFrom-Json $jsonpayload

# Access JSON values 
$foo = $json.foo
$bar = $json.bar

Writing stdout

Writing to stdout is as simple as using the Write-Output cmdlet. This must be valid JSON or Terraform will throw an exception when trying to run a plan/apply command. For example:

Write-Output '{ "name" : "My Resource Name", "region" : "West Europe" }'

It’s worth noting that all values are treated as strings when returned

Writing stderr

As per the documentation around errors:

If the program encounters an error and is unable to produce a result, it must print a human-readable error message (ideally a single line) to stderr and exit with a non-zero status.

You can use code similar to the following to return an error if the script fails for whatever reason:

Write-Error "Something went wrong"
exit 1

By returning a non-0 return code execution will be stopped and an error similar to the following will be returned when running your Terraform command:

data.external.powershell_test: Refreshing state… Error refreshing state: 1 error(s) occurred: ERROR data.external.powershell_test: failed to execute “Powershell.exe”: G:\TF_ExternalProvider\testScript.ps1 : Something went wrong At line:1 char:1

  • ./testScript.ps1
  • ~~~~~~~~~~~~~~~~
    • CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
    • FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,testScript.ps1

Obviously you’ll have a more appropriate error message which will help you to debug the issue ;)

Accessing return values

As mentioned above one of the benefits of using the External Provider is that you can access any return values, either as output variables or computed vars on other resources.

Using the above provider declaration and stdout example, the return values can be accessed using standard data source syntax with the key in format: data.external.<name>.result.<jsonKey>. E.g.

# output var
output "firstValue" {
  value = "${data.external.powershell_test.result.name}"
}

# or 
resource "azurerm_resource_group" "production" {
    name     = "production"
    location = "${data.external.powershell_test.result.region}"
}

Drawbacks

Perhaps not a drawback but something to be aware of: as the external provider is more accurately a data source, it is expected that running the data source should have no observable side effects and as such the external application will be run each time the state is refreshed.

Do You Want to Know More?

Sample Repository

I’ve created a Sample Repository which shows a sample External Provider. It doesn’t do anything so there’s no need to configure any credentials for AWS, Azure etc.


Share this: