The null_resource in Terraform is a bit of a Swiss Army knife, and its most surprising capability is that it doesn’t actually do anything on its own, but it can be instructed to trigger other things, like running scripts or provisioning infrastructure, at specific points in your Terraform execution.

Let’s see it in action. Imagine you need to run a local script after a resource is created, or maybe trigger a remote command on a newly provisioned server. This is where null_resource shines.

resource "null_resource" "run_local_script" {
  triggers = {
    # This ensures the resource is re-created (and thus the provisioner runs)
    # whenever the content of this file changes.
    script_content_hash = filemd5("my_local_script.sh")
  }

  provisioner "local-exec" {
    command = "bash my_local_script.sh ${self.triggers.script_content_hash}"
    interpreter = ["bash", "-c"]
  }
}

resource "aws_instance" "example" {
  ami           = "ami-0c55b159cbfafe1f0" # Example AMI for us-east-1
  instance_type = "t2.micro"

  # Associate the null_resource with a dependency
  depends_on = [null_resource.run_local_script]
}

In this example, null_resource.run_local_script is configured to execute a local shell script. The triggers block is crucial: it tells Terraform to re-evaluate and potentially re-run the provisioner whenever the script_content_hash changes. This is typically achieved by hashing the content of the script file itself, ensuring that the script runs only when its content has been modified. The local-exec provisioner then executes the specified command.

The aws_instance.example resource has depends_on = [null_resource.run_local_script]. This doesn’t mean the instance waits for the script to finish. Instead, it means Terraform will plan to run null_resource.run_local_script before it attempts to create aws_instance.example. This ordering is key to using null_resource for setup tasks.

The mental model for null_resource is this: it’s a placeholder. It exists solely to host provisioners. Terraform doesn’t track any actual infrastructure state for a null_resource. Its lifecycle is entirely determined by its triggers and its position in the dependency graph. When Terraform evaluates a null_resource, it checks its triggers. If any trigger value has changed since the last apply, Terraform considers the null_resource to be "changed" and will execute its provisioners. If the null_resource itself is part of a depends_on list for another resource, Terraform will ensure it’s processed (and its provisioners run if triggered) before the dependent resource is acted upon.

You can also use null_resource to trigger remote provisioners, such as running commands on a newly created EC2 instance.

resource "null_resource" "run_remote_script_on_ec2" {
  triggers = {
    # Re-run if the instance IP changes or the remote script content changes
    instance_ip = aws_instance.example.public_ip
    script_hash = filemd5("setup_ec2.sh")
  }

  connection {
    type        = "ssh"
    user        = "ec2-user"
    private_key = file("~/.ssh/my-aws-key.pem")
    host        = aws_instance.example.public_ip
  }

  provisioner "remote-exec" {
    inline = [
      "sudo yum update -y",
      "sudo yum install -y git",
      "git clone <your_repo_url>",
      "bash setup_ec2.sh"
    ]
  }

  depends_on = [aws_instance.example]
}

In this scenario, the null_resource relies on the aws_instance.example being created first (due to depends_on). The connection block specifies how to connect to the remote host (SSH, user, key, host IP). The remote-exec provisioner then runs a series of commands, including executing a script (setup_ec2.sh) on the instance. The triggers here are designed to re-run the provisioner if the instance’s IP address changes (which would happen if the instance was replaced) or if the content of setup_ec2.sh itself is modified.

The most common pitfall with null_resource is forgetting to include triggers. Without them, the provisioner will only run during the initial terraform apply. If you want the script to re-run when certain conditions change (like an updated configuration file or a new version of your application code), you must use triggers to signal that change to Terraform. The triggers map can contain any arbitrary key-value pairs, and Terraform will re-evaluate the null_resource if any value in this map differs from the last applied state.

The next logical step after managing arbitrary script execution is often to integrate with more advanced configuration management tools, like Ansible or Chef, which null_resource can also invoke.

Want structured learning?

Take the full Terraform course →