Clanker provides deep integration with Terraform, enabling you to manage multiple workspaces, execute Terraform commands, and query infrastructure state directly from the CLI. The integration supports both path-based and workspace-based configurations.
Configuration
Define Terraform workspaces in your ~/.clanker/config.yaml:
terraform :
default_workspace : "dev"
workspaces :
dev :
path : "/path/to/terraform/dev"
description : "Development environment"
staging :
path : "/path/to/terraform/staging"
description : "Staging environment"
prod :
path : "/path/to/terraform/production"
description : "Production environment"
Each workspace must specify a path to the directory containing Terraform configuration files (.tf files).
Client initialization
The Terraform client supports two initialization modes:
1. Path-based initialization
Directly specify a Terraform directory:
client , err := tfclient . NewClient ( "/path/to/terraform" )
// Creates a client with workspace="local" and path="/path/to/terraform"
The client recognizes path patterns and automatically expands:
func looksLikePath ( value string ) bool {
value = strings . TrimSpace ( value )
if value == "" {
return false
}
if strings . ContainsAny ( value , "/ \\ " ) {
return true
}
if strings . HasPrefix ( value , "~" ) || strings . HasPrefix ( value , "." ) {
return true
}
return false
}
2. Workspace-based initialization
Use configured workspace names:
client , err := tfclient . NewClient ( "dev" )
// Loads path from config: terraform.workspaces.dev.path
If no workspace is specified, uses terraform.default_workspace (defaults to “dev”).
Path expansion
The client automatically handles environment variables and home directory expansion:
func expandTerraformPath ( raw string ) ( string , bool ) {
raw = strings . TrimSpace ( raw )
if strings . HasPrefix ( raw , "~" ) {
if home , err := os . UserHomeDir (); err == nil {
raw = filepath . Join ( home , strings . TrimPrefix ( raw , "~" ))
}
}
path := filepath . Clean ( os . ExpandEnv ( raw ))
if info , err := os . Stat ( path ); err == nil && info . IsDir () {
return path , true
}
return "" , false
}
Supported patterns:
~/terraform/dev → /home/user/terraform/dev
$HOME/terraform/dev → /home/user/terraform/dev
./environments/dev → Current directory relative path
/absolute/path/to/terraform
Context-aware queries
The Terraform client provides intelligent context gathering based on query content:
func ( c * Client ) GetRelevantContext ( ctx context . Context , question string ) ( string , error ) {
var context strings . Builder
// Always get workspace info
workspaceInfo , err := c . getWorkspaceInfo ( ctx )
if err == nil {
context . WriteString ( "Terraform Workspace Info: \n " )
context . WriteString ( workspaceInfo )
context . WriteString ( " \n\n " )
}
// Always get state info
stateInfo , err := c . getStateInfo ( ctx )
if err == nil {
context . WriteString ( "Terraform State: \n " )
context . WriteString ( stateInfo )
context . WriteString ( " \n\n " )
}
// Get plan info if query mentions changes
questionLower := strings . ToLower ( question )
if strings . Contains ( questionLower , "plan" ) ||
strings . Contains ( questionLower , "change" ) ||
strings . Contains ( questionLower , "diff" ) {
planInfo , err := c . getPlanInfo ( ctx )
if err == nil {
context . WriteString ( "Terraform Plan: \n " )
context . WriteString ( planInfo )
}
}
// Get outputs if query mentions outputs or resources
if strings . Contains ( questionLower , "output" ) ||
strings . Contains ( questionLower , "infrastructure" ) ||
strings . Contains ( questionLower , "resource" ) {
outputInfo , err := c . getOutputInfo ( ctx )
if err == nil && outputInfo != "" {
context . WriteString ( "Terraform Outputs: \n " )
context . WriteString ( outputInfo )
}
}
return context . String (), nil
}
Retrieve current workspace and path:
func ( c * Client ) getWorkspaceInfo ( ctx context . Context ) ( string , error ) {
cmd := exec . CommandContext ( ctx , "terraform" , "workspace" , "show" )
cmd . Dir = c . path
output , err := cmd . Output ()
if err != nil {
return "" , err
}
return fmt . Sprintf ( "Current workspace: %s \n Configured path: %s " ,
strings . TrimSpace ( string ( output )), c . path ), nil
}
Example output:
Current workspace: default
Configured path: /home/user/terraform/dev
List resources and group by type:
cmd := exec . CommandContext ( ctx , "terraform" , "state" , "list" )
cmd . Dir = c . path
output , err := cmd . Output ()
lines := strings . Split ( strings . TrimSpace ( string ( output )), " \n " )
// Group resources by type
resourceTypes := make ( map [ string ] int )
for _ , line := range lines {
if line == "" {
continue
}
parts := strings . Split ( line , "." )
if len ( parts ) > 0 {
resourceTypes [ parts [ 0 ]] ++
}
}
Example output:
Total resources: 23
Resource types:
aws_instance: 3
aws_s3_bucket: 5
aws_iam_role: 8
aws_security_group: 7
Execute terraform plan and extract summary:
func ( c * Client ) getPlanInfo ( ctx context . Context ) ( string , error ) {
cmd := exec . CommandContext ( ctx , "terraform" , "plan" , "-no-color" , "-compact-warnings" )
cmd . Dir = c . path
output , err := cmd . Output ()
if err != nil {
return "" , err
}
// Return summary of plan
lines := strings . Split ( string ( output ), " \n " )
var summary strings . Builder
for _ , line := range lines {
if strings . Contains ( line , "Plan:" ) || strings . Contains ( line , "No changes" ) {
summary . WriteString ( line )
summary . WriteString ( " \n " )
}
}
return summary . String (), nil
}
Example output:
Plan: 2 to add, 1 to change, 0 to destroy.
Retrieve Terraform outputs in JSON format:
func ( c * Client ) GetTerraformOutputs ( ctx context . Context ) ( map [ string ] interface {}, error ) {
cmd := exec . CommandContext ( ctx , "terraform" , "output" , "-json" )
cmd . Dir = c . path
output , err := cmd . Output ()
if err != nil {
return nil , err
}
var outputs map [ string ] interface {}
if err := json . Unmarshal ( output , & outputs ); err != nil {
return nil , fmt . Errorf ( "failed to parse terraform outputs: %w " , err )
}
// Extract just the values from terraform output format
result := make ( map [ string ] interface {})
for key , value := range outputs {
if valueMap , ok := value .( map [ string ] interface {}); ok {
if val , exists := valueMap [ "value" ]; exists {
result [ key ] = val
}
}
}
return result , nil
}
Init
Initialize Terraform working directory:
func ( c * Client ) RunInit ( ctx context . Context ) ( string , error ) {
return c . runCommand ( ctx , "init" , "-input=false" )
}
Command line:
clanker ask --terraform dev "terraform init"
Plan
Generate and show execution plan:
func ( c * Client ) RunPlan ( ctx context . Context ) ( string , error ) {
initOutput , err := c . RunInit ( ctx )
if err != nil {
return "" , err
}
planOutput , err := c . runCommand ( ctx , "plan" , "-no-color" , "-compact-warnings" )
if err != nil {
return "" , err
}
return mergeOutputs ( initOutput , planOutput ), nil
}
Command line:
clanker ask --terraform staging "terraform plan"
Plan operations automatically run terraform init first to ensure dependencies are up to date.
Apply
Apply Terraform changes (requires confirmation):
func ( c * Client ) RunApply ( ctx context . Context ) ( string , error ) {
initOutput , err := c . RunInit ( ctx )
if err != nil {
return "" , err
}
applyOutput , err := c . runCommand ( ctx , "apply" , "-auto-approve" , "-no-color" )
if err != nil {
return "" , err
}
return mergeOutputs ( initOutput , applyOutput ), nil
}
Command line:
clanker ask --terraform prod "confirm apply terraform"
Apply operations require explicit confirmation with “confirm apply” in the query. This prevents accidental infrastructure changes.
List workspaces
View all configured Terraform workspaces:
Example output:
Available Terraform Workspaces (default: dev):
dev (default)
Path: /home/user/terraform/dev
Description: Development environment
staging
Path: /home/user/terraform/staging
Description: Staging environment
prod
Path: /home/user/terraform/production
Description: Production environment
Usage: clanker ask --terraform <workspace-name> "your infrastructure question"
Natural language queries
Use the --terraform flag with natural language queries:
clanker ask --terraform dev "How many EC2 instances are managed?"
clanker ask --terraform staging "What will change if I apply now?"
clanker ask --terraform prod "Show me all S3 buckets and their configurations"
The AI receives Terraform state, outputs, and workspace context automatically.
Workspace selection
Specify workspace via flag:
clanker ask --workspace staging --terraform "what resources exist?"
Or configure default:
terraform :
default_workspace : "staging"
Then omit the flag:
clanker ask --terraform "what resources exist?"
Command execution
The client executes Terraform commands in the configured workspace directory:
func ( c * Client ) runCommand ( ctx context . Context , args ... string ) ( string , error ) {
cmd := exec . CommandContext ( ctx , "terraform" , args ... )
cmd . Dir = c . path // Set working directory to workspace path
output , err := cmd . CombinedOutput ()
if err != nil {
return "" , fmt . Errorf ( "terraform %s failed: %w \n Output: %s " ,
strings . Join ( args , " " ), err , strings . TrimSpace ( string ( output )))
}
return strings . TrimSpace ( string ( output )), nil
}
Error handling
The client provides detailed error messages including Terraform output:
if err != nil {
return fmt . Errorf ( "terraform workspace ' %s ' not found in configuration" , workspace )
}
Common errors:
Workspace not found : Check terraform.workspaces configuration
Path does not exist : Verify workspace path in config
Terraform not installed : Ensure terraform binary is in PATH
Backend initialization failed : Run terraform init manually
Best practices
Use descriptive workspace names
Name workspaces after environments (dev, staging, prod) or teams (platform, data, ml) for clarity.
Set a sensible default workspace
Configure terraform.default_workspace to the environment you query most frequently, typically “dev” or “staging”.
Always run init before operations
The client automatically runs terraform init before plan/apply operations. Ensure your backend configuration is correct.
Use path expansion features
Leverage ~ and environment variables in paths for portability across different systems: path : "~/projects/terraform/dev"
path : "$TERRAFORM_ROOT/staging"
Natural language queries leverage cached state information. For real-time data, explicitly request a plan or refresh.
Integration with AI context
When using ask with --terraform, Clanker enriches AI queries with workspace context:
if includeTerraform {
tfClient , err := tfclient . NewClient ( workspace )
if err != nil {
return fmt . Errorf ( "failed to create Terraform client: %w " , err )
}
terraformContext , err = tfClient . GetRelevantContext ( ctx , question )
// Terraform context passed to AI for intelligent analysis
}
This enables complex queries:
clanker ask --terraform staging "Compare resource counts between staging and prod"
clanker ask --terraform dev "What security groups allow unrestricted access?"
Combined queries
Combine Terraform with AWS context for comprehensive infrastructure analysis:
clanker ask --aws --terraform dev "Compare live AWS resources with Terraform state"
The AI receives both Terraform state and live AWS resource data for drift detection and validation.