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
| Feature | Default | Description |
|---|---|---|
json | Yes | Typed JSON output parsing via serde / serde_json |
config | No | TerraformConfig 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.jsonfiles. - error
- exec
- prelude
- Convenience re-exports for common usage.
- streaming
- Streaming JSON output from
terraform planandterraform apply. - types
Structs§
- Terraform
- Terraform client configuration.
- Terraform
Builder - Builder for constructing a
Terraformclient.