Going to AWS Summit London? 🇬🇧🇬🇧

Meet us there →

Terraform

Terraform Null Resource – What It is & How to Use It

terraform null

In this blog post, we’ll delve into the null_resource type in Terraform, examining its purpose and distinguishing characteristics from other resources within the Terraform framework. We’ll explore the concept of triggers in null_resource, providing examples of its implementation with diverse provisioners, such as local and remote provisioners. We’ll also introduce a novel resource type known as terraform_data and explore its potential usage as an alternative to null_resource.

  1. Resources in Terraform
  2. What is a null resource in Terraform?
  3. What is the trigger inside a null resource?
  4. How to use Terraform null resource
  5. Using terraform_data instead of null resource

Resources in Terraform

Before explaining the concept of a null_resource in Terraform, we need to understand the fundamental concept of a resource inside the Terraform framework

In Terraform, a resource represents a specific entity or component of infrastructure you want to manage, provision, or configure. These resources can range from virtual machines, networks, and storage accounts to more specialized services from cloud providers.

resource "azurerm_windows_function_app" "monitor" {
  # Configuration settings for the resource
  attribute1 = value1
  attribute2 = value2
  # ...
}

The block above is a declaration for a resource that is a function app in Azure. When executed, it communicates with the remote resource provider (Azure) and creates an Azure Function App resource based on the specifications of its configuration.

What is a null resource in Terraform?

The null_resource in Terraform is similar to a standard resource. It adheres to the resource lifecycle model and serves as a placeholder for executing arbitrary actions within Terraform configurations without actually provisioning any physical resources. However, it does not perform any further actions beyond initialization

The null_resource is useful for executing standard operations that do not require provisioning an actual resource. It can be declared as a simple resource block and used in Terraform modules and other resources that depend on null resources. 

Below is the syntax for declaring a null_resource:

resource "null_resource" "example" {
  provisioner "local-exec" {
    command = "echo This command will execute whenever the configuration changes"
  }
}
  • “resource” indicates the declaration of a Terraform resource
  • “null_resource” specifies the type of resource being declared
  • “provisioner” specifies the type of provisioner (example: local, remote, etc.)
  • “triggers” — specifies what triggers this null_resource to execute

What is a trigger inside a null resource?

The default behavior of a null_resource in Terraform is that it will execute only once during the first run of the terraform apply command.

Below is the sample code:

resource "null_resource" "example" {
  provisioner "local-exec" {
    command = "echo This command will execute only once during apply"
  }
}

Execution results during first run:

terraform null resource local exec

To test this, add the word “specific” to the above command.

terraform null resource

See the results after we run terraform apply –auto-approve:

terraform null resource triggers

As you can see, it will run only the first time you execute your Terraform code.

If you want the null_resource to run every time you perform an apply, regardless any changes in the plan, you can use the “triggers” keyword in your Terraform code. 

In the code below, always_run = timestamp()  triggers a change in plan output everytime the code is run, making the null resource execute during each run.

resource "null_resource" "example" {
  # Using triggers to force execution on every apply
  triggers = {
    always_run = timestamp()
  }
  provisioner "local-exec" {
    command = "echo This specific command will execute every time during apply as triggers are used"
  }
}

In this example, the null_resource “example” has a trigger defined using the “triggers” keyword. The trigger in this case is based on the current timestamp, ensuring it changes every time an apply is performed. 

As a result, the null_resource will execute during every apply, as the timestamp changes during every run as shown below.

null resource terraform

How to use a Terraform null resource

We’ll now explore a few examples of how to use a null resource across various scenarios.

Example 1: Using null resource with local provisioner

In the following example, after deploying an Azure function app, imagine you want to deploy application code as a part of function app provisioning. 

In the code below, a null resource is created to execute code deployment via Azure CLI command on previously created function app. To make sure that it is executed every time, we used triggers with a timestamp function.

resource "azurerm_resource_group" "rg" {
  name     = "functionapp-resource-group"
  location = "East US"
}

resource "azurerm_windows_function_app" "example" {
  name                       = "example-function-app"
  resource_group_name        = azurerm_resource_group.example.name
  location                   = azurerm_resource_group.example.location
  app_service_plan_id        = azurerm_app_service_plan.example.id
  storage_account_name       = azurerm_storage_account.example.name
  storage_account_access_key = azurerm_storage_account.example.primary_access_key
}

resource "null_resource" "sourcecode" {
  provisioner "local-exec" {
    command = "az functionapp deployment source config-zip -g azurerm_resource_group.rg.name -n {azurerm_windows_function_app.monitor.name} -src ${path.module}/function.zip"
  }
  triggers = {
    always_run = timestamp()
  }
}

Example 2: Using null resource with remote provisioner

We can use a null resource with a remote provisioner to connect to a remote host for executing any custom actions typically over SSH or WinRM. This can be useful for performing tasks that require access to remote resources or environments during Terraform operations.

The code snippet below shows how to connect to a remote virtual machine following its creation using SSH and then create a directory under a specific path.

resource "null_resource" "example" {
  # Define triggers if needed
  triggers = {
  }

  # Define connection details for remote provisioner
  connection {
    type        = "ssh"
    user        = "username"
    private_key = file("~/.ssh/id_rsa")
    host        = "remote-host"
  }

  # Define remote-exec provisioner to execute commands on the remote host
  provisioner "remote-exec" {
    inline = [
      "echo 'Hello from remote host'",
      "mkdir -p /path/to/remote/directory"
    ]
  }
}

Let’s see what is happening here: 

  • The null_resource “example” is declared.
  • Connection details for SSH are specified within the connection block, including the username, private key, and hostname of the remote machine.
  • A remote-exec provisioner is defined within the null_resource block to execute inline commands on the remote host. In this case, it echoes a message and creates a directory.
  • Triggers, such as timestamp, can be defined if necessary to force updates based on changes in specific values.
  • This configuration will cause Terraform to establish an SSH connection to the remote host using the provided credentials and execute the specified commands remotely.

Example 3: Trigger a null resource every time

As mentioned at the beginning of the article, a null_resource executes only when there is a change in the plan. However, in some scenarios, you might need to execute it every time a Terraform script is executed. 

There are a couple of ways to achieve this:

  1. Use the timestamp() function.
  2. Introduce a trigger based on a resource attribute that changes frequently to ensure consistent execution of a null_resource without using a timestamp. This can be particularly useful if you have a resource in your configuration that undergoes frequent changes, ensuring that the null_resource is triggered accordingly. See the code below:
data "azurerm_storage_account" "example" {
  name                = "examplestorageaccount"
  resource_group_name = "example-resource-group"
}

resource "null_resource" "example" {
  # Define triggers based on a frequently changing attribute of an existing Azure resource
  triggers = {
    storage_account_properties = data.azurerm_storage_account.example.primary_access_key
  }

  # Define provisioner or other configuration as needed
  provisioner "local-exec" {
    command = "echo This command will execute every time the storage account's access key changes"
  }
}

Here, data block fetches information about an Azure Storage Account named “examplestorageaccount” from the Azure provider. The null_resource “example” has triggers defined based on the primary access key of the storage account obtained from the data source. Whenever the primary access key of the storage account changes, the null_resource will be triggered, and the associated provisioner will execute. 

This example shows how we can use a null_resource with a custom trigger mechanism to perform actions based on changes to specific attributes of an existing Azure resource.

Using terraform_data instead of null resource

In this section, we will talk about a new resource called terraform_data, which is introduced in Terraform version 1.4. 

A null_resource originates from a null provider, which is not inherently integrated into Terraform. When you run your code containing a null_resource, Terraform downloads the null provider, similar to other providers like AzureRM or AWS, as shown in the screenshot below.

terraform_data

terraform_data serves as an alternative to null_resource and accomplishes similar functionalities. Unlike null_resource, terraform_data is inherently a part of Terraform, meaning it is built-in and readily available without the need for additional provider downloads.

Let’s look at the example of terraform_data block.

variable "inputvariable" {
  type    = string
  default = "terraform"
}

resource "terraform_data" "source" {
  input = var.inputvariable
}

resource "terraform_data" "destination" {
  lifecycle {
    replace_triggered_by = [
      terraform_data.source
    ]
  }
}

In the code block above,

  • Variable block defines a variable called input variable with a default value of “terraform”
  • “Terraform_data” block 1 defines a resource called source, which takes input from the variable defined above
  • “Terraform_data” block 2 defines a resource called destination, which has a lifecycle block that uses replace_triggered_by block, which references terraform_data.source

When we run terraform init, it doesn’t have to download terraform_data, as a new provider as it’s already built-in to Terraform.

terraform null resource trigger every time

When we run terraform apply, it creates both the resources.

terraform null resource example

In the code snippet above, as terraform_data.destination refers to terraform_data.source, it would execute whenever there is a custom value provided for the input variable and replace the resource as it uses the “replace_triggered_by” keyword.

terraform null resource output

In the example above, instead of using a second terraform_data resource, we could use any remote resource, for example azurerm_storage_account, which can be recreated every time the input variable changes.

Key points

In this blog post, we explored the concept of a null_resource, examining its functionality and illustrating its usage through several examples. Additionally, we discussed the significance of triggers within the null_resource context. We introduced the terraform_data resource, which serves as an alternative to null_resource, and highlighted its intrinsic integration within Terraform.

We encourage you also to explore how Spacelift makes it easy to work with Terraform. If you need any help managing your Terraform infrastructure, building more complex workflows based on Terraform, and managing AWS credentials per run, instead of using a static pair on your local machine, Spacelift is a fantastic tool for this.

Note: New versions of Terraform will be placed under the BUSL license, but everything created before version 1.5.x stays open-source. OpenTofu is an open-source version of Terraform that will expand on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6. OpenTofu retained all the features and functionalities that had made Terraform popular among developers while also introducing improvements and enhancements. OpenTofu works with your existing Terraform state file, so you won’t have any issues when you are migrating to it.

Manage Terraform Better with Spacelift

Build more complex workflows based on Terraform using policy as code, programmatic configuration, context sharing, drift detection, resource visualization and many more.

Start free trial