Test and debug your Terraform expressions locally

Radu Zidarescu
5 min readApr 11, 2023

--

Terraform is an open-source IaC (infrastructure as code) tool that enables the provisioning, management, and modifying of infrastructure resources in a declarative manner. Terraform allows you to write code in a human-readable format, which makes it easy to understand and manage the infrastructure. It supports multiple cloud providers, such as AWS, Azure, Google Cloud, and many other providers, as well as other types of infrastructure such as Kubernetes, DNS, and more.

One of the biggest advantages of using Terraform is the ability to deliver infrastructure quickly. Terraform allows DevOps engineers to automate the provisioning process of infrastructure resources, which leads to faster delivery times. In addition, Terraform helps in reducing manual errors by providing a consistent and repeatable infrastructure deployment process.

However, like any tool, Terraform has its downsides. One of the main drawbacks of Terraform is its steep learning curve. Terraform has a lot of concepts to grasp, such as providers, resources, and modules. Moreover, the Terraform syntax can be quite verbose and requires a solid understanding of the underlying infrastructure provider. This can make it difficult for beginners to get started.

Another disadvantage of Terraform is the difficulty of testing expressions live in code. While Terraform provides built-in testing capabilities, such as the ability to perform dry runs and plan operations, it can be challenging to test expressions live in code. This is because Terraform evaluates expressions during runtime, which can lead to issues that only surface during actual deployment. As a result, it can be challenging to debug and fix issues that arise during deployment.

But… what if you could test all your expressions locally? Without having to plan or apply your code?

Enter: terraform console

The terraform console command provides an interactive console for evaluating expressions.

Usage

Usage: terraform console [options]

This command provides an interactive command-line console for evaluating and experimenting with expressions. You can use it to test interpolations before using them in configurations and to interact with any values currently saved in state. If the current state is empty or has not yet been created, you can use the console to experiment with the expression syntax and built-in functions. The console holds a lock on the state, and you will not be able to use the console while performing other actions that modify state.

Example

Let’s think of the following scenario: You want to create some Azure Storage Accounts through Terraform and each Storage Account will have a random number of Containers inside of it.

The most elegant approach would be to design your own custom object to contain the names of both the storage accounts and the containers therein. Something along these lines:

locals {
sa = {
sa1: {
name = "hi"
list = ["a", "b", "c", "z"]
},
sa2: {
name = "hello"
list = ["e", "f", "g"]
}
}
}

Creating the Storage Accounts is a piece of cake:

resource "azurerm_storage_account" "example" {

for_each = local.sa

name = each.value.name
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
account_tier = "Standard"
account_replication_type = "LRS"

tags = {
environment = "staging"
}
}

Using a simple for_each loop you can iterate through the sa object and create a storage account for sa1 and one for sa2.

The difficulty, however, increases when you want to create the containers, because each container needs the name of the owning storage account and its own name, so you need to tweak your object a bit… and for these kinds of tests, terraform console shines!

First, based on the resource example, we see that we need to know both the Storage Account name and the container name inside of each container resource:

resource "azurerm_storage_container" "example" {
name = "vhds"
storage_account_name = azurerm_storage_account.example.name
container_access_type = "private"
}

So, we need a new, custom object that has the Storage Account and container names side by side… something like this:

[
{
"cont" = "a"
"name" = "hi"
},
...
{
"cont" = "g"
"name" = "hello"
},
]

Now, we can start by extracting just the Storage Account names from the sa object, and that’s fairly easy:

locals {
try1 = [for object in local.sa : object.name ]
}

But how does this new object look like?
Well, terraform console to the rescue:

IaC> terraform console
> local.try1
[
"hi",
"hello",
]

And here we have it, a visual of our object. But it’s still too simple, we need to also iterate through the container names and add those in for our new object, something like a for in for:

locals {
try2 = [for object in local.sa : [
for c in object.list : {
name = object.name
cont = c
}
]]
}

This almost works like a charm, but, if we leverage terraform console one mroe time to take a sneak peek at the resulting object, we’ll see it’s not quite what we want:

IaC> terraform console
> local.try2
[
[
{
"cont" = "a"
"name" = "hi"
},
{
"cont" = "b"
"name" = "hi"
},
{
"cont" = "c"
"name" = "hi"
},
{
"cont" = "z"
"name" = "hi"
},
],
[
{
"cont" = "e"
"name" = "hello"
},
{
"cont" = "f"
"name" = "hello"
},
{
"cont" = "g"
"name" = "hello"
},
],
]

As you can see, it’s a tuple of tuples… and we just wanted one tuple. If only there was something to help us out… Unless… THERE IS

So now, on our final iteration, we also flatten the output and this is what we have:

locals {
final = flatten([for object in local.sa : [
for c in object.list : {
name = object.name
cont = c
}
]])
}

Which, through the terraform console lens, looks like this:

IaC> terraform console
> local.final
[
{
"cont" = "a"
"name" = "hi"
},
{
"cont" = "b"
"name" = "hi"
},
{
"cont" = "c"
"name" = "hi"
},
{
"cont" = "z"
"name" = "hi"
},
{
"cont" = "e"
"name" = "hello"
},
{
"cont" = "f"
"name" = "hello"
},
{
"cont" = "g"
"name" = "hello"
},
]

Just what we wanted. The final step being to use this new objects to create the containers:

resource "azurerm_storage_container" "example" {

for_each = local.final

name = each.value.cont
storage_account_name = each.value.name
container_access_type = "private"
}

And that’s it!

If you’re new to Terraform, I highly recommend giving the console a try. It’s an excellent way to get started and explore the capabilities of Terraform without committing to a full deployment. With the console, you can experiment with different resource configurations, try out different expressions, and test your code before deploying it to production.

In conclusion, Terraform is a powerful tool for managing infrastructure as code, and the console is just one of many features that make it a valuable addition to any toolkit. By leveraging the console, you can streamline your infrastructure deployment process, reduce manual errors, and deliver infrastructure faster than ever before. So, what are you waiting for? Give the Terraform console a try and see for yourself how it can help you manage your infrastructure more effectively!

--

--

Radu Zidarescu
0 Followers

DevOps engineer who enjoys sharing tips and tricks