Skip to main content

Crate terraform_wrapper

Crate terraform_wrapper 

Source
Expand description

§terraform-wrapper

A type-safe Terraform CLI wrapper for Rust.

This crate provides an idiomatic Rust interface to the Terraform command-line tool. All commands use a builder pattern and async execution via Tokio.

§Quick Start

use terraform_wrapper::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let tf = Terraform::builder()
        .working_dir("./infra")
        .build()?;

    // Initialize, apply, read outputs, destroy
    InitCommand::new().execute(&tf).await?;

    ApplyCommand::new()
        .auto_approve()
        .var("region", "us-west-2")
        .execute(&tf)
        .await?;

    let result = OutputCommand::new()
        .name("endpoint")
        .raw()
        .execute(&tf)
        .await?;

    if let OutputResult::Raw(value) = result {
        println!("Endpoint: {value}");
    }

    DestroyCommand::new().auto_approve().execute(&tf).await?;

    Ok(())
}

§Core Concepts

§The TerraformCommand Trait

All commands implement TerraformCommand, which provides the execute() method. You must import this trait to call .execute():

use terraform_wrapper::TerraformCommand; // Required for .execute()

§Builder Pattern

Commands are configured using method chaining:

ApplyCommand::new()
    .auto_approve()
    .var("region", "us-west-2")
    .var_file("prod.tfvars")
    .target("module.vpc")
    .parallelism(10)
    .execute(&tf)
    .await?;

§The Terraform Client

The Terraform struct holds shared configuration (binary path, working directory, environment variables) passed to every command:

let tf = Terraform::builder()
    .working_dir("./infra")
    .env("AWS_REGION", "us-west-2")
    .env_var("instance_type", "t3.medium")  // Sets TF_VAR_instance_type
    .timeout_secs(300)
    .build()?;

Programmatic defaults: -no-color and -input=false are enabled by default. Override with .color(true) and .input(true).

§Error Handling

All commands return Result<T, terraform_wrapper::Error>. The error type implements std::error::Error, so it works with anyhow and other error libraries via ?:

match InitCommand::new().execute(&tf).await {
    Ok(output) => println!("Initialized: {}", output.stdout),
    Err(Error::NotFound) => eprintln!("Terraform binary not found"),
    Err(Error::CommandFailed { stderr, .. }) => eprintln!("Failed: {stderr}"),
    Err(Error::Timeout { timeout_seconds }) => eprintln!("Timed out after {timeout_seconds}s"),
    Err(e) => eprintln!("Error: {e}"),
}

§Command Categories

§Lifecycle

use terraform_wrapper::commands::{
    InitCommand,     // terraform init
    PlanCommand,     // terraform plan
    ApplyCommand,    // terraform apply
    DestroyCommand,  // terraform destroy
};

§Inspection

use terraform_wrapper::commands::{
    ValidateCommand,  // terraform validate
    ShowCommand,      // terraform show (state or plan)
    OutputCommand,    // terraform output
    FmtCommand,       // terraform fmt
    GraphCommand,     // terraform graph (DOT format)
    ModulesCommand,   // terraform modules
    ProvidersCommand, // terraform providers (lock, mirror, schema)
    TestCommand,      // terraform test
    VersionCommand,   // terraform version
};

§State and Workspace Management

use terraform_wrapper::commands::{
    StateCommand,       // terraform state (list, show, mv, rm, pull, push, replace-provider)
    WorkspaceCommand,   // terraform workspace (list, show, new, select, delete)
    ImportCommand,      // terraform import
    ForceUnlockCommand, // terraform force-unlock
    GetCommand,         // terraform get (download modules)
    RefreshCommand,     // terraform refresh (deprecated)
    RawCommand,         // any subcommand not covered above
};

§JSON Output Types

With the json feature (enabled by default), commands return typed structs instead of raw strings:

// Version info
let info = tf.version().await?;
println!("Terraform {} on {}", info.terraform_version, info.platform);

// Validate with diagnostics
let result = ValidateCommand::new().execute(&tf).await?;
if !result.valid {
    for diag in &result.diagnostics {
        eprintln!("[{}] {}: {}", diag.severity, diag.summary, diag.detail);
    }
}

// Show state with typed resources
let result = ShowCommand::new().execute(&tf).await?;
if let ShowResult::State(state) = result {
    for resource in &state.values.root_module.resources {
        println!("{} ({})", resource.address, resource.resource_type);
    }
}

// Show plan with resource changes
let result = ShowCommand::new().plan_file("tfplan").execute(&tf).await?;
if let ShowResult::Plan(plan) = result {
    for change in &plan.resource_changes {
        println!("{}: {:?}", change.address, change.change.actions);
    }
}

// Output values
let result = OutputCommand::new().json().execute(&tf).await?;
if let OutputResult::Json(outputs) = result {
    for (name, val) in &outputs {
        println!("{name} = {}", val.value);
    }
}

§Streaming Output

Long-running commands like apply and plan with -json produce streaming NDJSON (one JSON object per line) instead of a single blob. Use streaming::stream_terraform to process events as they arrive – useful for progress reporting, logging, or UI updates:

use terraform_wrapper::streaming::{stream_terraform, JsonLogLine};

let result = stream_terraform(
    &tf,
    ApplyCommand::new().auto_approve().json(),
    &[0],
    |line: JsonLogLine| {
        match line.log_type.as_str() {
            "apply_start" => println!("Creating: {}", line.message),
            "apply_progress" => println!("  {}", line.message),
            "apply_complete" => println!("Done: {}", line.message),
            "apply_errored" => eprintln!("Error: {}", line.message),
            "change_summary" => println!("Summary: {}", line.message),
            _ => {}
        }
    },
).await?;

Common event types: version, planned_change, change_summary, apply_start, apply_progress, apply_complete, apply_errored, outputs.

§Config Builder

With the config feature, define Terraform configurations entirely in Rust. No .tf files needed – generates .tf.json that Terraform processes natively.

Available builder methods: required_provider, backend, provider, variable, data, resource, local, module, output.

use terraform_wrapper::config::TerraformConfig;
use serde_json::json;

let config = TerraformConfig::new()
    .required_provider("aws", "hashicorp/aws", "~> 5.0")
    .backend("s3", json!({
        "bucket": "my-tf-state",
        "key": "terraform.tfstate",
        "region": "us-west-2"
    }))
    .provider("aws", json!({ "region": "us-west-2" }))
    .variable("instance_type", json!({
        "type": "string", "default": "t3.micro"
    }))
    .data("aws_ami", "latest", json!({
        "most_recent": true,
        "owners": ["amazon"]
    }))
    .resource("aws_instance", "web", json!({
        "ami": "${data.aws_ami.latest.id}",
        "instance_type": "${var.instance_type}"
    }))
    .local("common_tags", json!({
        "Environment": "production",
        "ManagedBy": "terraform-wrapper"
    }))
    .module("vpc", json!({
        "source": "terraform-aws-modules/vpc/aws",
        "version": "~> 5.0",
        "cidr": "10.0.0.0/16"
    }))
    .output("instance_id", json!({
        "value": "${aws_instance.web.id}"
    }));

let dir = config.write_to_tempdir()?;
// Terraform::builder().working_dir(dir.path()).build()?;

Enable with:

terraform-wrapper = { version = "0.3", features = ["config"] }

§Feature Flags

FeatureDefaultDescription
jsonYesTyped JSON output parsing via serde / serde_json
configNoTerraformConfig builder for .tf.json generation

Disable defaults for raw command output only:

terraform-wrapper = { version = "0.3", default-features = false }

§OpenTofu Compatibility

OpenTofu works out of the box by pointing the client at the tofu binary:

let tf = Terraform::builder()
    .binary("tofu")
    .working_dir("./infra")
    .build()?;

§Imports

The prelude module re-exports everything you need:

use terraform_wrapper::prelude::*;

Or import selectively from commands:

use terraform_wrapper::{Terraform, TerraformCommand};
use terraform_wrapper::commands::{InitCommand, ApplyCommand, OutputCommand, OutputResult};

Re-exports§

pub use command::TerraformCommand;
pub use error::Error;
pub use error::Result;
pub use exec::CommandOutput;

Modules§

command
commands
config
Terraform configuration builder for generating .tf.json files.
error
exec
prelude
Convenience re-exports for common usage.
streaming
Streaming JSON output from terraform plan and terraform apply.
types

Structs§

Terraform
Terraform client configuration.
TerraformBuilder
Builder for constructing a Terraform client.