Skip to main content
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
}

Workspace information

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\nConfigured path: %s", 
        strings.TrimSpace(string(output)), c.path), nil
}
Example output:
Current workspace: default
Configured path: /home/user/terraform/dev

State information

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

Plan information

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.

Output information

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
}

Terraform operations

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:
clanker terraform list
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\nOutput: %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

Name workspaces after environments (dev, staging, prod) or teams (platform, data, ml) for clarity.
Configure terraform.default_workspace to the environment you query most frequently, typically “dev” or “staging”.
The client automatically runs terraform init before plan/apply operations. Ensure your backend configuration is correct.
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.

Build docs developers (and LLMs) love