The null_resource in Terraform is a placeholder resource that lets you run provisioners and arbitrary actions without creating any real infrastructure. It follows the standard Terraform resource lifecycle, but stops there, taking no further action beyond initialization.
It’s commonly used to execute scripts, trigger re-runs based on external changes, and bridge gaps between managed and unmanaged operations.
In this article, we’ll cover how null_resource works, how triggers control its execution, practical examples with local and remote provisioners, and when to use terraform_data as the modern alternative.
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 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:
To test this, add the word “specific” to the above command.
See the results after we run terraform apply –auto-approve:
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 every time 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.
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 a null resource with a 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 a 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 a null resource with a 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 runs only when the plan changes. However, in some scenarios, you might need to run it every time a Terraform script runs.
There are a couple of ways to achieve this:
- Use the
timestamp()function. - 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.
When to use terraform_data instead of null resource?
In this section, we will talk about a new resource called terraform_data, which was introduced in Terraform version 1.4.
While null_resource is not formally deprecated, official docs recommend using terraform_data on Terraform 1.4 and later, so for any new code, terraform_data is the preferred choice.
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, just as it does for other providers like AzureRM or AWS, as shown in the screenshot below.
terraform_data serves as an alternative to null_resource and accomplishes similar functionalities. Unlike null_resource, terraform_data is built into Terraform, meaning it is available by default without requiring 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.
When we run terraform apply, it creates both the resources.
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.
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.
How to manage Terraform resources with Spacelift
Terraform is really powerful, but to achieve an end-to-end secure GitOps approach, you need a platform built for infrastructure orchestration. Spacelift goes beyond running Terraform workflows, giving you a governed, two-path deployment model and unlocking features such as:
- Policy as code (based on Open Policy Agent) — Control how many approvals you need for runs, what kind of resources you can create, and what parameters those resources can have. You can also govern behavior when a pull request is open or merged.
- Multi-IaC orchestration — Combine Terraform with Kubernetes, Ansible, and other infrastructure as code tools such as OpenTofu, Pulumi, and CloudFormation. Create dependencies between them and share outputs across stacks.
- Governed developer self-service — Use Blueprints and Templates to build Golden Paths for your teams. Complete a simple form to provision infrastructure based on Terraform and other supported tools — with guardrails enforced throughout.
- AI-powered visibility – Use Spacelift Intelligence to surface actionable insights across your stacks, runs, and resources, helping you identify issues faster and make better infrastructure decisions.
- Integrations with third-party tools — Connect your existing tools and build policies around them. For example, see how to integrate security tools into your workflows using Custom Inputs.
Spacelift also supports private workers, so you can execute infrastructure workflows inside your own security perimeter. See the documentation for details on configuring worker pools.
You can try it for free by creating a trial account or booking a demo with one of our engineers.
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.
Note: New versions of Terraform are 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 expands on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6.
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.
Frequently asked questions
What is the difference between null resource and provisioner?
A null resource is a Terraform resource used to run provisioners without managing real infrastructure. A provisioner is a block that executes scripts or commands during resource lifecycle events. Null resources are often used with triggers to rerun provisioners when input values change.
What is a tainted resource in Terraform?
A tainted resource in Terraform is marked for destruction and recreation on the next terraform apply, regardless of configuration changes. It’s used when a resource is faulty or misbehaving but still technically exists. Tainting ensures the resource is replaced without modifying the rest of the infrastructure.
Should I use null_resource or terraform_data?
Use terraform_data for any new configuration on Terraform 1.4+. It’s built-in and officially recommended replacement. Keep null_resource only if you’re on an older version or maintaining existing code.
Why does my null_resource run on every apply?
A null_resource re-runs on every apply when its triggers map includes a value that changes each time, such as timestamp(), or when no triggers are defined at all. Without stable triggers, Terraform has no stored state to compare, so it treats the resource as new. Define triggers using values that only change when you actually want re-execution, like a file hash or a specific resource attribute.
Is Terraform null_resource deprecated?
Not formally, but official docs recommend terraform_data instead for Terraform 1.4 and later. It’s safe in existing code, but consider it legacy for anything new.
