At some point while working with Terraform, you’ll run into a situation where you need to do something that isn’t tied to a specific resource—run a script, call an API, or glue together steps that Terraform doesn’t natively support. That’s where the Terraform null resource quietly becomes one of the most useful (and misunderstood) tools in your toolkit.
What is a Terraform Null Resource?
A null resource is exactly what it sounds like: a resource that doesn’t manage infrastructure. Instead, it acts as a placeholder that allows you to attach provisioners or define execution logic within your Terraform workflow.
Think of it as a bridge between Terraform’s declarative model and imperative actions.
A minimal example
1resource "null_resource" "example" {
2 provisioner "local-exec" {
3 command = "echo Hello, Terraform"
4 }
5}This doesn’t create any infrastructure, but Terraform will execute the command during the apply phase.
Why Use Null Resources?
Terraform is designed to describe infrastructure, not run scripts. Still, real-world workflows often require:
- Running initialization scripts
- Triggering CI/CD steps
- Calling external services
- Performing one-off configuration tasks
A Terraform null resource lets you integrate these steps without leaving your Terraform codebase.
Triggers: The Real Power Behind Null Resources
By default, a null resource runs only once. That’s rarely useful. The magic comes from triggers, which tell Terraform when to re-run the resource.
Example with triggers
1resource "null_resource" "build" {
2 triggers = {
3 version = "${var.app_version}"
4 }
5
6 provisioner "local-exec" {
7 command = "./build.sh ${var.app_version}"
8 }
9}Whenever app_version changes, Terraform will destroy and recreate this null resource—rerunning the script.
In practice, triggers turn a null resource into a controlled execution unit inside Terraform.
Common Use Cases
1. Running Local Scripts
Need to compile assets, generate configs, or run a custom CLI?
1resource "null_resource" "compile_assets" {
2 triggers = {
3 timestamp = timestamp()
4 }
5
6 provisioner "local-exec" {
7 command = "npm run build"
8 }
9}Using timestamp() forces execution on every apply. Useful, but easy to abuse.
2. Remote Commands via SSH
You can execute commands on a remote machine using remote-exec:
1resource "null_resource" "configure_server" {
2 provisioner "remote-exec" {
3 inline = [
4 "sudo apt update",
5 "sudo apt install -y nginx"
6 ]
7
8 connection {
9 type = "ssh"
10 user = "ubuntu"
11 private_key = file("~/.ssh/id_rsa")
12 host = aws_instance.web.public_ip
13 }
14 }
15}This is often used for bootstrapping when cloud-init or user_data isn't enough.
3. Dependency Orchestration
Sometimes you just need to enforce ordering:
1resource "null_resource" "wait_for_db" {
2 depends_on = [aws_db_instance.main]
3
4 provisioner "local-exec" {
5 command = "echo Database is ready"
6 }
7}This can help coordinate steps across unrelated resources.
4. API Calls or Webhooks
Trigger external systems:
1resource "null_resource" "notify" {
2 provisioner "local-exec" {
3 command = "curl -X POST https://example.com/deploy"
4 }
5}Where Things Get Tricky
Null resources are powerful, but they come with trade-offs.
They break Terraform’s declarative model
Terraform is built around state and desired outcomes. Null resources introduce imperative steps that Terraform can’t fully track or validate.
Provisioners are a last resort
Even HashiCorp recommends using provisioners sparingly. If there’s a native Terraform resource or cloud-native solution, prefer that.
Idempotency becomes your responsibility
Terraform won’t ensure your scripts are safe to run multiple times. You must handle:
- Duplicate executions
- Partial failures
- Side effects
A Better Pattern: Use Data + Resources First
Before reaching for a null resource, ask:
- Can this be handled by an existing provider?
- Can I use
user_dataor cloud-init? - Can a CI/CD pipeline handle this instead?
If the answer is yes, skip null resources.
Combining Null Resource with Files
A practical pattern is using file hashes as triggers.
1resource "null_resource" "deploy_config" {
2 triggers = {
3 config_hash = filemd5("config.yaml")
4 }
5
6 provisioner "local-exec" {
7 command = "./deploy-config.sh"
8 }
9}Now the script only runs when the file changes.
When You Should Use Terraform Null Resource
- You need lightweight orchestration inside Terraform
- No provider exists for your task
- You need controlled script execution tied to state
When You Should Avoid It
- For complex workflows better handled by CI/CD
- When idempotency is critical and hard to guarantee
- If a native Terraform resource exists
Final Thought
The Terraform null resource isn’t flashy, but it’s incredibly useful when used with restraint. Treat it as a glue layer—not a foundation. If you lean on it too heavily, your infrastructure code starts to behave less like Terraform and more like a shell script with state.
Used carefully, though, it fills the exact gaps Terraform intentionally leaves open.