Using Terraform Outputs in Powershell
I have been using Terraform for more than five years, and in that time, I’ve never needed to consume outputs from executed Terraform until recently. It was a strange realization, and I found myself deciding the best way to do it. You may have noticed that I have been using Terraform with .NET, and I’ve also been using a project called Invoke-Build for my build scripting. I have found Invoke-Build and .NET tools an excellent combination for creating builds. It gives me that feeling of using Cake without using anything specifically bespoke. The scenario that led to the need was doing a deployment in Invoke-Build using the Azure PowerShell modules and needing the name of the resource group and app service that was auto-generated in the Terraform code. Oddly enough, most of the time, naming for me has been something static passed in, not something auto-generated with a high degree of entropy. I discovered after some light research that the terraform output
command can return the current output of the Terraform run as JSON. I combined that with the Convert-FromJson
commandlet will return me the output as a PowerShell object. A PowerShell object makes it convenient for use in my deploy task within my build script. Let’s create an example to see it all together.
Project Setup
First, let’s create a directory.
$ mkdir tf-ps
We will create a .NET tool manifest in that directory and install Invoke-Build.
$ dotnet new tool-manifest
The template "Dotnet local tool manifest file" was created successfully.
$ dotnet tool install --local ib
You can invoke the tool from this directory using the following commands: 'dotnet tool run ib' or 'dotnet ib'.
Tool 'ib' (version '5.10.4') was successfully installed. Entry is added to the manifest file ../tf-ps/.config/dotnet-tools.json.
Now I like to use the Invoke-Build project template for generating my invoke build script. You can install that with the following.
$ dotnet new --install Invoke-Build.template
The following template packages will be installed:
Invoke-Build.template
Success: Invoke-Build.template::1.0.4 installed the following templates:
Template Name Short Name Language Tags
------------------- ---------- ---------- -------------------
Invoke-Build script ib PowerShell Invoke-Build/Script
Now we can create our build script at the root of our project with the following command.
$ dotnet new ib
The template "Invoke-Build script" was created successfully.
Remove the build and clean tasks since we won’t be using those. Now we can do our Terraform setup. First, we need to install the dotnet-terraform
tool.
$ dotnet tool install --local dotnet-terraform
You can invoke the tool from this directory using the following commands: 'dotnet tool run dotnet-terraform' or 'dotnet dotnet-terraform'.
Tool 'dotnet-terraform' (version '1.5.3') was successfully installed. Entry is added to the manifest file ../tf-ps/.config/dotnet-tools.json.
Finally, we can create our Terraform file.
$ touch main.tf
We now have the basics in place to start creating our example Terraform and the outputs we want to read.
Creating our Terraform
The plan is to keep this simple. We will use the random provider to generate two names and create two outputs to return those names. Open the main.tf, and let’s get started.
terraform {
required_providers {
random = {
source = "hashicorp/random"
version = "3.5.1"
}
}
}
provider "random" {
# Configuration options
}
resource "random_string" "resource_group_name" {
length = 16
special = false
upper = false
}
resource "random_string" "app_service_name" {
length = 16
special = false
upper = false
}
output "resource_group_name" {
value = random_string.resource_group_name.result
}
output "app_service_name" {
value = random_string.app_service_name.result
}
Now let’s create a few Invoke-Build tasks for managing Terraform.
<#
.Synopsis
Build script, https://github.com/nightroman/Invoke-Build
#>
param(
[ValidateSet('Debug', 'Release')]
[string]$Configuration = 'Release'
)
# Synopsis: Format Terraform.
task fmt {
dotnet terraform fmt --recursive
}
# Synopsis: Format Terraform.
task init fmt, {
dotnet terraform init
}
# Synopsis: Validate Terraform
task validate init, {
dotnet terraform validate
}
# Synopsis: Apply Terraform
task apply validate, {
dotnet terraform apply --auto-approve
}
# Synopsis: Default task.
task . validate
We now have tasks for formatting, initializing, validating, and applying our Terraform. Let’s run our build script using our default task to ensure we have formatted and validated our TF.
$ dotnet ib
Build . ../tf-ps/tf-ps.build.ps1
Task /./validate/init/fmt
Done /./validate/init/fmt 00:00:00.2413400
Task /./validate/init
Initializing the backend...
Initializing provider plugins...
- Reusing previous version of hashicorp/random from the dependency lock file
- Using previously-installed hashicorp/random v3.5.1
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
Done /./validate/init 00:00:00.8462540
Task /./validate
Success! The configuration is valid.
Done /./validate 00:00:01.1795593
Done /. 00:00:01.1844631
Build succeeded. 4 tasks, 0 errors, 0 warnings 00:00:01.3428812
Now we can create our deploy
task that will read the output from Terraform once it is applied. We can do this in any task, I’m just doing this as part of my deploy task since that is where I will be using it. The key part is to ensure that you call terraform output
with the --json
option.
# Synopsis: Deploy something using TF outputs
task deploy {
$output = dotnet terraform output --json | ConvertFrom-Json
assert($output.Count -eq 1)
Write-Build Green "$($output.resource_group_name.value)"
Write-Build Green "$($output.app_service_name.value)"
}
The setup is now all complete. The major pieces are in place, so we can apply our Terraform and run our deploy task.
Apply and Deploy
The first step is to apply our Terraform using our task. The apply task we created will format, initialize, and validate the Terraform before executing the apply.
$ dotnet ib apply
Build apply ../tf-ps/tf-ps.build.ps1
Task /apply/validate/init/fmt
Done /apply/validate/init/fmt 00:00:00.2480703
Task /apply/validate/init
Initializing the backend...
Initializing provider plugins...
- Reusing previous version of hashicorp/random from the dependency lock file
- Using previously-installed hashicorp/random v3.5.1
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
Done /apply/validate/init 00:00:00.7502469
Task /apply/validate
Success! The configuration is valid.
Done /apply/validate 00:00:01.0750479
Task /apply
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# random_string.app_service_name will be created
+ resource "random_string" "app_service_name" {
+ id = (known after apply)
+ length = 16
+ lower = true
+ min_lower = 0
+ min_numeric = 0
+ min_special = 0
+ min_upper = 0
+ number = true
+ numeric = true
+ result = (known after apply)
+ special = false
+ upper = false
}
# random_string.resource_group_name will be created
+ resource "random_string" "resource_group_name" {
+ id = (known after apply)
+ length = 16
+ lower = true
+ min_lower = 0
+ min_numeric = 0
+ min_special = 0
+ min_upper = 0
+ number = true
+ numeric = true
+ result = (known after apply)
+ special = false
+ upper = false
}
Plan: 2 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ app_service_name = (known after apply)
+ resource_group_name = (known after apply)
random_string.app_service_name: Creating...
random_string.resource_group_name: Creating...
random_string.resource_group_name: Creation complete after 0s [id=z5w9oduvn7ghsqys]
random_string.app_service_name: Creation complete after 0s [id=u2krolx9mo9e5c8o]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
app_service_name = "u2krolx9mo9e5c8o"
resource_group_name = "z5w9oduvn7ghsqys"
Done /apply 00:00:01.4561331
Build succeeded. 4 tasks, 0 errors, 0 warnings 00:00:01.5694294
Great! Make a note of the outputs from the apply. We can now execute our deploy task to use them.
$ dotnet ib deploy
Build deploy ../tf-ps/tf-ps.build.ps1
Task /deploy
z5w9oduvn7ghsqys
u2krolx9mo9e5c8o
Done /deploy 00:00:00.2804354
Build succeeded. 1 tasks, 0 errors, 0 warnings 00:00:00.4142101
The outputs from the PowerShell object match the output from the apply task.
Wrapping Up
Such a simple discovery on my behalf that I thought I would share. It’s nice to work with output from a CLI tool as an object. That is one of the many things I appreciate about PowerShell over other shell environments.
Thanks for reading,
Jamie
If you enjoy the content, then consider buying me a coffee.