Infra chaos crushing your controls?

Meet Spacelift at AWS re:Invent

Terraform local-exec Provisioner Explained [Examples]

terraform

While Terraform’s primary strength lies in describing what infrastructure should exist, there are moments when executing imperative logic during the apply phase becomes necessary to complete an operational workflow. In these cases, running local commands can help integrate deployments with scripts, notifications, or configuration generation steps that sit outside Terraform’s direct scope. 

In this article, we’ll explore how to use the Terraform local-exec provisioner effectively, following current best practices to ensure reliability and maintainability.

What is Terraform local-exec?

local-exec is a Terraform provisioner that runs a command on the machine where Terraform is executed, not on the target infrastructure. It is commonly used to trigger local scripts or CLI tools after a resource is created or destroyed. 

Unlike remote-exec, which runs commands on provisioned resources (e.g., EC2 instances), local-exec executes locally and has no access to the remote infrastructure’s runtime unless explicitly configured to interact via APIs or CLI tools.

While useful for tasks like generating files, sending notifications, or calling external systems, local-exec is not recommended for critical infrastructure logic, as it breaks Terraform’s declarative model and can introduce non-idempotent behavior.

When and how to use Terraform local-exec?

The local-exec provisioner is a block inside a resource (or sometimes a null_resource) that defines a command to run on the local machine where Terraform is executed, rather than remotely on the resource itself.

A typical local-exec block looks like this:

provisioner "local-exec" {
  command     = "echo The server IP is ${self.public_ip}"
  working_dir = "./scripts"
  environment = {
    ENVIRONMENT = "production"
    OWNER       = "devops-team"
  }
  interpreter = ["/bin/bash", "-c"]
}

Here’s what each argument does:

  • command – This is the main argument and specifies the shell command to be executed locally. It supports interpolation, allowing you to reference Terraform resource attributes (like self.public_ip) or variables.
  • working_dir – Specifies the directory from which the command is executed. This is useful when your script or tool expects to run from a specific location (for example, a folder containing configuration files).
  • environment – Defines environment variables to be available to the local command during execution. This keeps sensitive data out of the command line and makes scripts cleaner.
  • interpreter – Lets you control the shell used to run the command. By default, Terraform uses /bin/sh on Unix systems and cmd.exe on Windows. You can override this to use bash, PowerShell, or any other shell.
  • when – Determines when the provisioner runs — either on create (default) or destroy. The destroy mode is useful for cleanup tasks.
  • on_failure – Specifies what Terraform should do if the command fails. Options include:
    • continue: Ignore the error and continue applying.
    • fail (default): Stop the apply operation.

local-exec gives you granular control over how and when local commands execute. However, it’s best used sparingly and in a predictable, idempotent manner. The common use cases include:

  1. Triggering a CI/CD step after resource creation – You can run a local command to notify a CI/CD system (like GitHub Actions, Jenkins, or CircleCI) that infrastructure deployment has completed. For example, after provisioning AWS resources, you might call an API endpoint or trigger a webhook to start an application deployment pipeline.
  2. Generating local configuration files – It’s common to use local-exec to write files that depend on Terraform outputs, such as .env files, Kubernetes kubeconfig, or Ansible inventory lists. These files allow developers and tools to interact with newly created infrastructure.
  3. Running validation or security checks – You can execute local scripts that validate the deployed environment, for example, running custom compliance or security checks using tools like terraform-compliance, tfsec, or OPA. This allows you to integrate static or runtime validation steps directly into the Terraform workflow.
  4. Sending notifications or webhooks — After a successful apply, you can send notifications to Slack, Microsoft Teams, or email or trigger external systems via REST APIs. For example, posting a message that a new environment was created with its URL and owner.
  5. Cleanup tasks on destroy – When Terraform destroys infrastructure, you might want to remove local files, deregister DNS records, or delete entries from a monitoring system. Using when = destroy, you can run local cleanup logic safely tied to Terraform’s lifecycle.

Example 1: Running a post-provision notification script

Imagine you are provisioning an AWS EC2 instance, and once it’s successfully created, you want to trigger a local script that sends a Slack notification or logs deployment details into an internal system. Using the local-exec provisioner here can help automate such integration tasks without modifying the infrastructure resources themselves.

resource "aws_instance" "app_server" {
  ami           = "ami-08c40ec9ead489470"
  instance_type = "t3.micro"
  tags = {
    Name = "app-server"
  }

  provisioner "local-exec" {
    command = "bash ./scripts/notify_deploy.sh ${self.public_ip}"
  }
}

In this configuration, Terraform creates an EC2 instance and then immediately runs a local shell script, notify_deploy.sh, passing the instance’s public IP address as an argument. The script might handle tasks such as sending notifications, updating monitoring dashboards, or writing audit logs. 

It is important to note that the command executes on the local machine running Terraform, not on the EC2 instance itself. 

A key best practice here is to make the local-exec idempotent, meaning that re-running Terraform should not break or duplicate results. For example, the notification script could check whether a notification for that instance already exists before sending a new one.

Example 2: Generating a configuration file from Terraform outputs

Another common use case for local-exec is generating local files or configuration artifacts that depend on Terraform outputs, such as producing a .env file or a kubeconfig for Kubernetes access. This can help bridge Terraform-managed infrastructure and the developer’s local environment without manual steps.

resource "terraform_data" "generate_env" {

  triggers_replace = {
    lb_dns = aws_lb.app.dns_name
    db_endpoint = aws_db_instance.app_db.endpoint
}

  provisioner "local-exec" {
    command = <<EOT
echo "APP_URL=${aws_lb.app.dns_name}" > ./envfile
echo "DB_HOST=${aws_db_instance.app_db.endpoint}" >> ./envfile
echo "DB_USER=${aws_db_instance.app_db.username}" >> ./envfile
EOT
  }
}

Here, the null_resource acts as a dummy resource that executes a local-exec block to write environment variables into a local file whenever the specified triggers change. The triggers ensure Terraform reruns this provisioner only when dependent values change, keeping it aligned with the declarative model. 

This design is better than manually running shell commands because it integrates seamlessly into Terraform’s lifecycle and ensures reproducibility. Using null_resource combined with triggers for purely local operations, rather than attaching local-exec directly to cloud resources, helps maintain clarity and avoid unintended dependencies.

Terraform local-exec limitations

Terraform local-exec it comes with a number of architectural, operational, and reliability constraints that make it unsuitable for many tasks that might seem convenient at first glance.

  • Runs only on the local machine, making it environment-dependent and inconsistent across CI/CD or team setups.
  • Not idempotent – Side effects aren’t tracked in Terraform state, so reruns can repeat or break actions.
  • No drift detection or state awareness – Terraform can’t know what the local command actually changed.
  • Limited error handling – Only supports fail or continue, with no retries or structured output parsing.
  • Weak dependency management – May run out of order unless explicitly controlled with depends_on.
  • Security and portability risks – can expose secrets in logs and behave differently across OS environments.
  • Unsuitable for long-running or interactive tasks, as Terraform waits indefinitely for completion.

Key points

Using local-exec effectively means treating it as a bridge between Terraform’s declarative infrastructure management and the imperative world of scripts and external systems. It’s best suited for post-provision hooks, automation integrations, and controlled local side effects. 

Always ensure that your local-exec commands are idempotent, environment-aware, and trigger-driven, keeping Terraform runs predictable and reproducible.

Terraform is really powerful, but to achieve an end-to-end secure GitOps approach, you need to use a product that can run your Terraform workflows. Spacelift takes managing Terraform to the next level by giving you access to a powerful CI/CD workflow and unlocking features such as:

  • Policies (based on Open Policy Agent)
  • Multi-IaC workflows
  • Self-service infrastructure
  • Integrations with any third-party tools

If you want to learn more about Spacelift, create a free account today or book a demo with one of our engineers.

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.

Terraform management made easy

Spacelift effectively manages Terraform state, more complex workflows, supports policy as code, programmatic configuration, context sharing, drift detection, resource visualization and includes many more features.

Learn more

Frequently asked questions

  • What is the difference between remote-exec and local-exec in Terraform?

    local-exec is often used for triggering local scripts, sending notifications, or chaining deployment steps from the local system. In contrast, remote-exec connects to the target resource via SSH or WinRM to configure it directly, commonly used for bootstrapping virtual machines after creation. Both are provisioners, but remote-exec depends on network access to the instance, while local-exec has no such dependency.

  • What is the difference between null resource and local-exec in Terraform?

    The null_resource acts as a trigger-based resource that can execute provisioners like local-exec or remote-exec. It is often used when no existing Terraform resource fits a specific task, such as running scripts or orchestrating external systems. local-exec is the actual provisioner block that runs shell commands on the local system and can be used within any resource, including null_resource.

  • Can I use Terraform locally?

    Terraform can be used entirely locally without needing to connect to any remote backend or cloud service.

    When running Terraform locally, both the state file and configuration files reside on your local machine. This setup is ideal for testing, learning, or managing small environments. By default, Terraform uses the local backend, storing the terraform.tfstate file in the working directory unless configured otherwise. You can run all Terraform commands like init, plan, and apply from the command line without any remote dependencies.

    However, for team use or production deployments, remote backends like S3 or Azure Blob are recommended to enable collaboration, locking, and versioning.

Terraform Commands Cheat Sheet

Grab our ultimate cheat sheet PDF
for all the Terraform commands
and concepts you need.

Share your data and download the cheat sheet