CLI tool
hashicorp/terraform-exec avatar
hashicorp/terraform-exec

terraform-exec and driving the Terraform CLI from Go

Terraform CLI commands via Go.

788 stars132 forksGoMPL-2.0

At a glance

What is it?
terraform-exec is a Go module for running Terraform CLI commands with structured return values instead of parsing terminal output. It is still pre v1.0, so minor releases can break your build.
Who is it for?
terraform-exec suits Go automation that must invoke Terraform as a tool, especially when you want parsed output rather than scraped log lines. It does not fit plugin authors, who should use the Terraform Plugin Framework, and it needs a real Terraform binary on disk.
Can I use it commercially?
Yes, with conditions. MPL-2.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why not just run the terraform binary yourself

The problem is narrow. You are writing a Go program that has to drive Terraform, and you would rather call functions than assemble command lines, parse human readable output and hope the format never changes. terraform-exec exists for exactly that case: it constructs and runs Terraform CLI commands, and it returns structured values instead of strings you have to scrape.

The structured types come from a separate module, terraform-json, so the shape of a returned plan or state follows a shared definition rather than a parser written inside this library. That division matters when you compare it with the usual alternative of running the binary yourself with os/exec: the difference in approach is not that you avoid the subprocess, because you do not. The difference is that command output arrives as data.

There is a second design decision in the README that matters more than the parsing. The README recommends importing this library rather than importing `github.com/hashicorp/terraform/command`, because that package is not intended for use outside Terraform Core. The canonical Go interface for building plugins is the Terraform Plugin Framework over gRPC, and terraform-exec serves the other Terraform interface, the CLI, for Go programs that need it.

NewTerraform takes a working directory and an executable path

The entry point is the `Terraform` struct, which the README says must be initialised with `NewTerraform(workingDir, execPath)`. Both arguments matter. The working directory tells the library which Terraform configuration to operate on, and the executable path tells it which binary to run. There is no lookup of a binary on PATH in this description, and no download step, so the caller is responsible for supplying a Terraform binary.

Above that struct, each top level Terraform command has its own function. The README states that these return either an error or a value and an error, where the value is a terraform-json type. In other words, the API is one method per command rather than a general purpose runner that takes an argument list, which means the return type of a call tells you what it produces and how that value is built.

The package lives under the module root in a `tfexec` directory, alongside `internal/`, `scripts/`, `go.mod` and `go.sum`. The module declares `go 1.26.0` with a toolchain directive of `go1.26.8`, and it requires terraform-json v0.29.0 along with go-version and hc-install.

A first program that installs Terraform, runs init and reads state

The README's example shows the whole sequence in one main function. It uses hc-install to fetch a pinned Terraform version, wraps it in the `Terraform` struct, runs Init with the upgrade option, and then calls Show, whose result carries a FormatVersion that the example prints as "0.1".

go
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/hashicorp/go-version"
	"github.com/hashicorp/hc-install/product"
	"github.com/hashicorp/hc-install/releases"
	"github.com/hashicorp/terraform-exec/tfexec"
)

func main() {
	installer := &releases.ExactVersion{
		Product: product.Terraform,
		Version: version.Must(version.NewVersion("1.0.6")),
	}

	execPath, err := installer.Install(context.Background())
	if err != nil {
		log.Fatalf("error installing Terraform: %s", err)
	}

	workingDir := "/path/to/working/dir"
	tf, err := tfexec.NewTerraform(workingDir, execPath)
	if err != nil {
		log.Fatalf("error running NewTerraform: %s", err)
	}

	err = tf.Init(context.Background(), tfexec.Upgrade(true))
	if err != nil {
		log.Fatalf("error running Init: %s", err)
	}

	state, err := tf.Show(context.Background())
	if err != nil {
		log.Fatalf("error running Show: %s", err)
	}

	fmt.Println(state.FormatVersion) // "0.1"
}

Two details carry over into any real program. The working directory in the example is a placeholder path that you replace with the directory holding your configuration, and the version string is pinned to an exact release, so a build does not silently pick up a newer Terraform. Every command takes a context, which is what makes cancellation and timeouts possible in a long running automation job.

The compatibility promises are narrower than the module looks

terraform-exec advertises that it follows semantic versioning, and the README warns in the same breath that the module is not yet at v1.0.0 and that breaking changes may occur in minor releases. Both statements are true at once, and the second one is the one that affects your dependency file. Pre 1.0, semantic versioning does not protect you from a signature change arriving in a minor bump.

The Terraform side has its own rule. The README says the project generally follows Terraform's own compatibility promises and recommends Terraform v1.x alongside the library. For older versions, compatibility is maintained on a best effort basis with latest minor versions from 0.12 and later, which the README is careful to qualify: it does not imply coverage of all features or CLI surface, only that things should not break in unexpected ways.

On the Go side, the library uses the Go release support policy, and the two latest major Go releases are supported. The practical consequence for an upgrade is that you are tracking three moving parts at once: the library version, the Terraform binary version and the Go toolchain. The repository keeps a CHANGELOG.md, and the README points contributors at CONTRIBUTING.md for details about the test suite.

Running the end-to-end tests against a real Terraform binary

The test suite is worth knowing about because it shows what the library considers a realistic workflow. The README states that it contains end-to-end tests which run realistic workflows against a real Terraform binary using `tfexec.Terraform{}`. No fake command runner, no recorded fixtures.

To point those tests at a Terraform binary you already have, set the environment variable named in the README to its path and run the test command from the repository root. The timeout in the command is generous, which makes sense for tests that download providers and create real state.

sh
go test -timeout=20m ./tfexec/internal/e2etest

The same path is useful when you are working on the library itself or on code that depends on it, since it lets you run the suite against the exact binary you intend to ship rather than whatever version is downloaded during the test.

Where terraform-exec is the wrong choice

The clearest boundary is plugin development. The README names the Terraform Plugin Framework as the canonical Go interface for plugins using the gRPC protocol, and says terraform-exec targets Terraform's other interface, the CLI. If you are writing a provider or a provisioner, this library is the wrong side of the boundary, and using `github.com/hashicorp/terraform/command` instead is discouraged by the README as well.

Second, there is a binary dependency. The library runs a Terraform executable that you provide, and the example goes to the trouble of installing a specific version first. In a container image or a CI runner, that means managing an extra artifact, and any environment where downloading Terraform is not allowed cannot use the approach at all.

Third, the pre 1.0 status makes upgrades a decision rather than a routine. A minor release can carry breaking changes, so a dependency bump needs a changelog read. That cost is small for a tool you own and unpleasant for a library that many teams depend on, which is why the recommendation to pin the version in the example is worth following.

terraform-exec against raw exec and the Terraform Plugin Framework

The realistic alternatives are two, and they differ in kind rather than in degree.

Against raw os/exec, terraform-exec gives you typed returns, one method per command and error handling that follows the Go convention of returning an error rather than writing to a buffer. What you give up is control over the exact command line, and you inherit a dependency on the library's version of Terraform's behaviour, including the best effort guarantee for 0.12 and later.

Against the Terraform Plugin Framework, the difference is architectural. The Plugin Framework is the interface for code that Terraform loads and calls, over gRPC, as a provider or a provisioner. terraform-exec is the interface for code that calls Terraform, over the CLI, as a supervisor. A CI runner that applies a configuration and then reads the resulting state is in the second category. A tool that adds a new resource type is in the first, and no amount of shelling out changes that.

Editorial conclusion

terraform-exec suits Go automation that must invoke Terraform as a tool, especially when you want parsed output rather than scraped log lines. It does not fit plugin authors, who should use the Terraform Plugin Framework, and it needs a real Terraform binary on disk. Before depending on it, pin a version and check the CHANGELOG.md in the repository, because the module is still below v1.0.0 and breaking changes can arrive in a minor release.

Frequently asked questions

How do I create a terraform-exec Terraform instance?

The README states that the Terraform struct must be initialised with NewTerraform(workingDir, execPath), passing the working directory and the path of the Terraform binary to run.

Is terraform-exec safe to depend on before v1.0?

The README notes that the module is not yet at v1.0.0 and that breaking changes may occur in minor releases, even though the project says it follows semantic versioning. Pin a version and read the CHANGELOG.md before upgrading.

Should I import terraform/command instead of terraform-exec?

No. The README says importing this library is preferable, because github.com/hashicorp/terraform/command is not intended for use outside Terraform Core. Plugin authors should use the Terraform Plugin Framework instead.

How do I run the terraform-exec end-to-end tests?

Set the environment variable TFEXEC_E2ETEST_TERRAFORM_PATH to the path of a Terraform binary and run go test -timeout=20m ./tfexec/internal/e2etest from the repository root.

Which Terraform versions does terraform-exec support?

The README recommends Terraform v1.x and says compatibility with latest minor versions from 0.12 and later is maintained on a best effort basis, which does not imply coverage of all features or CLI surface.

Official sources

  1. hashicorp/terraform-exec on GitHub
  2. License: MPL-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/hashicorp-terraform-exec.svg)](https://hysenlabs.com/projects/hashicorp-terraform-exec)

Was this useful?