Terraform Provider#
SousChef includes a Terraform provider that enables infrastructure-as-code management of migrations to Ansible, Habitat to Docker conversions, and InSpec profile transformations.
Overview#
The Terraform provider allows you to:
- Manage migrations declaratively - Define cookbook conversions in Terraform configuration
- Track migration state - Terraform tracks which cookbooks have been migrated
- Assess before migrating - Query migration complexity and cost estimates using data sources
- Automate migration pipelines - Integrate with CI/CD and GitOps workflows
- Batch process cookbooks - Convert multiple recipes in one operation
- Containerise Habitat plans - Transform Chef Habitat plans to Dockerfiles
- Convert testing frameworks - Migrate InSpec profiles to modern test tools
Installation#
Prerequisites#
- Terraform >= 1.0
- SousChef CLI installed (>= 2.4.0) and available in PATH
- Go >= 1.21 (for building from source)
Building from Source#
Local Installation#
# Create plugins directory
mkdir -p ~/.terraform.d/plugins/registry.terraform.io/kpeacocke/souschef/0.1.0/linux_amd64
# Copy binary
cp terraform-provider-souschef ~/.terraform.d/plugins/registry.terraform.io/kpeacocke/souschef/0.1.0/linux_amd64/
Configuration#
Configure the provider in your Terraform configuration:
terraform {
required_providers {
souschef = {
source = "kpeacocke/souschef"
version = "~> 0.1"
}
}
}
provider "souschef" {
# Optional: specify custom souschef CLI path
souschef_path = "/usr/local/bin/souschef"
}
If souschef_path is not specified, the provider will use souschef from your PATH.
Resources#
souschef_migration#
Manages a Chef cookbook to Ansible playbook migration.
Example:
resource "souschef_migration" "web_server" {
cookbook_path = "/path/to/chef/cookbooks/web_server"
output_path = "/path/to/ansible/playbooks"
recipe_name = "default"
}
Arguments:
cookbook_path(Required, string) - Path to the Chef cookbook directoryoutput_path(Required, string) - Directory where Ansible playbook will be writtenrecipe_name(Optional, string) - Name of the recipe to convert. Defaults to "default"
Attributes:
id(string) - Unique identifier for the migration (format:cookbook-recipe)cookbook_name(string) - Name of the cookbookplaybook_content(string) - Generated Ansible playbook YAML content
Resource Behaviour:
- Create: Converts the specified Chef recipe to an Ansible playbook
- Read: Verifies the playbook still exists and reads current content
- Update: Re-runs the conversion if cookbook_path or recipe_name changes
- Delete: Removes the generated Ansible playbook file
souschef_batch_migration#
Manages batch migration of multiple Chef recipes from a single cookbook to Ansible playbooks.
Example:
resource "souschef_batch_migration" "web_server" {
cookbook_path = "/path/to/chef/cookbooks/web_server"
output_path = "/path/to/ansible/playbooks"
recipe_names = ["default", "setup", "deploy", "configure"]
}
output "playbook_count" {
value = souschef_batch_migration.web_server.playbook_count
}
output "all_playbooks" {
value = souschef_batch_migration.web_server.playbooks
}
Arguments:
cookbook_path(Required, string) - Path to the Chef cookbook directoryoutput_path(Required, string) - Directory where Ansible playbooks will be writtenrecipe_names(Required, list of strings) - List of recipe names to convert
Attributes:
id(string) - Unique identifier for the batch migrationcookbook_name(string) - Name of the cookbookplaybook_count(number) - Number of playbooks generatedplaybooks(map of strings) - Map of recipe names to playbook content
Resource Behaviour:
- Create: Converts all specified recipes to Ansible playbooks in one operation
- Read: Verifies all playbooks exist and reads current content
- Update: Re-runs conversion if cookbook_path or recipe_names change
- Delete: Removes all generated Ansible playbook files
souschef_habitat_migration#
Manages conversion of Chef Habitat plans to Dockerfiles for containerised deployments.
Example:
resource "souschef_habitat_migration" "nginx" {
plan_path = "/path/to/habitat/nginx/plan.sh"
output_path = "/path/to/docker"
base_image = "ubuntu:22.04" # Optional, defaults to ubuntu:latest
}
output "dockerfile" {
value = souschef_habitat_migration.nginx.dockerfile_content
}
Arguments:
plan_path(Required, string) - Path to the Habitat plan.sh fileoutput_path(Required, string) - Directory where Dockerfile will be writtenbase_image(Optional, string) - Base Docker image to use (default: ubuntu:latest)
Attributes:
id(string) - Unique identifier for the migrationpackage_name(string) - Name of the Habitat packagedockerfile_content(string) - Generated Dockerfile content
Resource Behaviour:
- Create: Converts Habitat plan to Dockerfile
- Read: Verifies Dockerfile exists and reads current content
- Update: Re-runs conversion if plan_path or base_image changes
- Delete: Removes the generated Dockerfile
souschef_inspec_migration#
Manages conversion of Chef InSpec profiles to various testing frameworks.
Example:
resource "souschef_inspec_migration" "linux_baseline" {
profile_path = "/path/to/inspec/profiles/linux"
output_path = "/path/to/tests"
output_format = "testinfra" # Options: testinfra, serverspec, goss, ansible
}
output "test_content" {
value = souschef_inspec_migration.linux_baseline.test_content
}
Arguments:
profile_path(Required, string) - Path to the InSpec profile directoryoutput_path(Required, string) - Directory where converted tests will be writtenoutput_format(Required, string) - Output test framework:testinfra,serverspec,goss, oransible
Attributes:
id(string) - Unique identifier for the migrationprofile_name(string) - Name of the InSpec profiletest_content(string) - Generated test content
Output Formats:
| Format | Extension | Description |
|---|---|---|
testinfra |
.py |
Python-based infrastructure testing |
serverspec |
.rb |
Ruby-based server testing |
goss |
.yaml |
YAML-based quick validation |
ansible |
.yml |
Ansible assert/test tasks |
Resource Behaviour:
- Create: Converts InSpec profile to target test framework
- Read: Verifies test file exists and reads current content
- Update: Re-runs conversion if profile_path or output_format changes
- Delete: Removes the generated test file
Data Sources#
souschef_assessment#
Fetches migration assessment for a Chef cookbook.
Example:
data "souschef_assessment" "web_server" {
cookbook_path = "/path/to/chef/cookbooks/web_server"
}
output "complexity" {
value = data.souschef_assessment.web_server.complexity
}
output "estimated_hours" {
value = data.souschef_assessment.web_server.estimated_hours
}
Arguments:
cookbook_path(Required, string) - Path to the Chef cookbook directory
Attributes:
id(string) - Unique identifier (cookbook path)complexity(string) - Migration complexity level: "Low", "Medium", or "High"recipe_count(number) - Number of recipes in the cookbookresource_count(number) - Total Chef resources across all recipesestimated_hours(number) - Estimated migration effort in hoursrecommendations(string) - Migration recommendations and best practices
souschef_cost_estimate#
Fetches detailed cost estimation for migration projects, suitable for Terraform Cloud cost analysis features.
Example:
data "souschef_cost_estimate" "web_server" {
cookbook_path = "/path/to/chef/cookbooks/web_server"
developer_hourly_rate = 175.0 # Optional, default: 150 USD
infrastructure_cost = 1000.0 # Optional, default: 500 USD
}
output "total_cost" {
value = data.souschef_cost_estimate.web_server.total_project_cost_usd
}
output "labour_cost" {
value = data.souschef_cost_estimate.web_server.estimated_cost_usd
}
output "breakdown" {
value = {
hours = data.souschef_cost_estimate.web_server.estimated_hours
labour = data.souschef_cost_estimate.web_server.estimated_cost_usd
infra = 1000.0
total = data.souschef_cost_estimate.web_server.total_project_cost_usd
complexity = data.souschef_cost_estimate.web_server.complexity
}
}
Arguments:
cookbook_path(Required, string) - Path to the Chef cookbook directorydeveloper_hourly_rate(Optional, number) - Developer hourly rate in USD (default: 150)infrastructure_cost(Optional, number) - Additional infrastructure/tooling cost in USD (default: 500)
Attributes:
id(string) - Unique identifier (cookbook path)complexity(string) - Migration complexity levelrecipe_count(number) - Number of recipesresource_count(number) - Total resourcesestimated_hours(number) - Estimated migration hoursestimated_cost_usd(number) - Labour cost in USD (hours × hourly_rate)total_project_cost_usd(number) - Total cost including infrastructurerecommendations(string) - Cost-aware recommendations
Cost Calculation:
- Labour Cost =
estimated_hours×developer_hourly_rate - Total Cost =
labour_cost+infrastructure_cost
Usage Examples#
Basic Migration#
Convert a single cookbook's default recipe:
resource "souschef_migration" "database" {
cookbook_path = "/chef/cookbooks/postgresql"
output_path = "/ansible/playbooks"
}
Multiple Recipes#
Migrate multiple recipes from the same cookbook:
locals {
web_cookbook = "/chef/cookbooks/web_server"
recipes = ["default", "setup", "deploy", "configure"]
}
resource "souschef_migration" "web_recipes" {
for_each = toset(local.recipes)
cookbook_path = local.web_cookbook
output_path = "/ansible/playbooks"
recipe_name = each.value
}
Conditional Migration Based on Assessment#
Only migrate cookbooks with Low or Medium complexity:
data "souschef_assessment" "app_server" {
cookbook_path = "/chef/cookbooks/app_server"
}
resource "souschef_migration" "app_server" {
count = contains(["Low", "Medium"], data.souschef_assessment.app_server.complexity) ? 1 : 0
cookbook_path = data.souschef_assessment.app_server.cookbook_path
output_path = "/ansible/playbooks"
}
output "migration_status" {
value = length(souschef_migration.app_server) > 0 ? "Migrated" : "Skipped (complexity: ${data.souschef_assessment.app_server.complexity})"
}
Migration Pipeline with Multiple Cookbooks#
Manage a complete migration project:
locals {
cookbooks = {
database = "/chef/cookbooks/postgresql"
web_server = "/chef/cookbooks/nginx"
app_server = "/chef/cookbooks/rails"
monitoring = "/chef/cookbooks/prometheus"
}
output_base = "/ansible/playbooks"
}
# Assess all cookbooks
data "souschef_assessment" "cookbooks" {
for_each = local.cookbooks
cookbook_path = each.value
}
# Migrate all cookbooks
resource "souschef_migration" "cookbooks" {
for_each = local.cookbooks
cookbook_path = each.value
output_path = local.output_base
recipe_name = "default"
}
# Generate migration report
output "migration_report" {
value = {
for name, assessment in data.souschef_assessment.cookbooks : name => {
complexity = assessment.complexity
recipes = assessment.recipe_count
resources = assessment.resource_count
estimated_hours = assessment.estimated_hours
playbook_path = "${local.output_base}/${name}.yml"
status = "Completed"
}
}
}
Batch Migration with Cost Analysis#
Migrate multiple recipes only if cost is acceptable:
# Get cost estimate before proceeding
data "souschef_cost_estimate" "web_server" {
cookbook_path = "/chef/cookbooks/web_server"
developer_hourly_rate = 200.0
infrastructure_cost = 750.0
}
# Only proceed if total cost is under budget
resource "souschef_batch_migration" "web_server" {
count = data.souschef_cost_estimate.web_server.total_project_cost_usd < 5000 ? 1 : 0
cookbook_path = "/chef/cookbooks/web_server"
output_path = "/ansible/playbooks"
recipe_names = ["default", "setup", "deploy", "configure"]
}
output "migration_decision" {
value = {
total_cost_usd = data.souschef_cost_estimate.web_server.total_project_cost_usd
labour_cost_usd = data.souschef_cost_estimate.web_server.estimated_cost_usd
hours = data.souschef_cost_estimate.web_server.estimated_hours
complexity = data.souschef_cost_estimate.web_server.complexity
approved = length(souschef_batch_migration.web_server) > 0
reason = length(souschef_batch_migration.web_server) > 0 ? "Within budget" : "Cost exceeds $5,000 limit"
}
}
Habitat to Docker Conversion#
Convert Habitat plans to containerised applications:
# Convert Habitat plan to Dockerfile
resource "souschef_habitat_migration" "nginx" {
plan_path = "/habitat/plans/nginx/plan.sh"
output_path = "/docker/nginx"
base_image = "ubuntu:22.04"
}
resource "souschef_habitat_migration" "postgresql" {
plan_path = "/habitat/plans/postgresql/plan.sh"
output_path = "/docker/postgresql"
base_image = "postgres:15-alpine"
}
output "dockerfiles" {
value = {
nginx = souschef_habitat_migration.nginx.dockerfile_content
postgresql = souschef_habitat_migration.postgresql.dockerfile_content
}
}
InSpec Profile Transformation#
Convert InSpec profiles to multiple test frameworks:
locals {
inspec_profile = "/inspec/profiles/cis-ubuntu-22.04"
}
# Convert to TestInfra for Python-based testing
resource "souschef_inspec_migration" "testinfra" {
profile_path = local.inspec_profile
output_path = "/tests/testinfra"
output_format = "testinfra"
}
# Convert to Ansible asserts for integration testing
resource "souschef_inspec_migration" "ansible" {
profile_path = local.inspec_profile
output_path = "/tests/ansible"
output_format = "ansible"
}
# Convert to Goss for quick validation
resource "souschef_inspec_migration" "goss" {
profile_path = local.inspec_profile
output_path = "/tests/goss"
output_format = "goss"
}
output "test_frameworks" {
value = {
testinfra = souschef_inspec_migration.testinfra.test_content
ansible = souschef_inspec_migration.ansible.test_content
goss = souschef_inspec_migration.goss.test_content
}
}
Complete Multi-Environment Migration#
Manage migrations across multiple environments with cost controls:
locals {
environments = {
dev = "/chef/cookbooks/dev"
test = "/chef/cookbooks/test"
prod = "/chef/cookbooks/prod"
}
hourly_rate = 180.0
infra_cost = 800.0
}
# Get cost estimates for all environments
data "souschef_cost_estimate" "environments" {
for_each = local.environments
cookbook_path = each.value
developer_hourly_rate = local.hourly_rate
infrastructure_cost = local.infra_cost
}
# Migrate environments that aren't High complexity and under $10k
resource "souschef_batch_migration" "environments" {
for_each = {
for env, path in local.environments :
env => path
if data.souschef_cost_estimate.environments[env].complexity != "High" &&
data.souschef_cost_estimate.environments[env].total_project_cost_usd < 10000
}
cookbook_path = each.value
output_path = "/ansible/playbooks/${each.key}"
recipe_names = ["default", "config", "deploy"]
}
output "environment_analysis" {
value = {
for env, cost_data in data.souschef_cost_estimate.environments :
env => {
total_cost_usd = cost_data.total_project_cost_usd
labour_hours = cost_data.estimated_hours
complexity = cost_data.complexity
migrated = contains(keys(souschef_batch_migration.environments), env)
playbook_count = try(souschef_batch_migration.environments[env].playbook_count, 0)
}
}
}
Integration with Version Control#
Use Terraform to manage migration state while keeping playbooks in Git:
resource "souschef_migration" "web_server" {
cookbook_path = var.cookbook_path
output_path = "${path.module}/generated-playbooks"
recipe_name = var.recipe_name
}
# Write playbook to version control
resource "local_file" "playbook" {
filename = "${path.module}/playbooks/${var.recipe_name}.yml"
content = souschef_migration.web_server.playbook_content
}
Best Practices#
1. Use Cost Estimates for Budget Planning#
Always estimate costs before committing to large migrations:
data "souschef_cost_estimate" "cookbook" {
cookbook_path = var.cookbook_path
developer_hourly_rate = var.hourly_rate
infrastructure_cost = var.infra_cost
}
# Make data-driven decisions
locals {
should_migrate = (
data.souschef_cost_estimate.cookbook.total_project_cost_usd < var.budget &&
data.souschef_cost_estimate.cookbook.complexity != "High"
)
}
2. Use Batch Migrations for Efficiency#
Convert multiple recipes in one operation instead of individually:
# [YES] Efficient - single batch operation
resource "souschef_batch_migration" "recipes" {
cookbook_path = "/chef/cookbooks/app"
output_path = "/ansible/playbooks"
recipe_names = ["default", "setup", "deploy", "config"]
}
# [NO] Inefficient - multiple separate operations
resource "souschef_migration" "default" {
cookbook_path = "/chef/cookbooks/app"
output_path = "/ansible/playbooks"
recipe_name = "default"
}
# ... repeated for each recipe
3. Organise Output Directories Logically#
Structure output paths for clarity:
resource "souschef_batch_migration" "services" {
for_each = var.services
cookbook_path = "/chef/cookbooks/${each.key}"
output_path = "/ansible/playbooks/${each.key}"
recipe_names = each.value.recipes
}
4. Version Your Generated Artefacts#
Commit generated playbooks and Dockerfiles to version control:
resource "souschef_habitat_migration" "app" {
plan_path = var.plan_path
output_path = "${path.module}/generated/docker"
}
resource "local_file" "dockerfile" {
filename = "${path.module}/docker/Dockerfile"
content = souschef_habitat_migration.app.dockerfile_content
lifecycle {
create_before_destroy = true
}
}
5. Use Data Sources for Conditional Logic#
Make intelligent decisions based on assessment data:
data "souschef_assessment" "cookbook" {
cookbook_path = var.cookbook_path
}
# Only proceed with migration if complexity is acceptable
resource "souschef_migration" "cookbook" {
count = contains(["Low", "Medium"], data.souschef_assessment.cookbook.complexity) ? 1 : 0
# ...
}
6. Validate Before Applying#
Always review Terraform plans before execution:
terraform plan -out=migration.tfplan
# Review changes carefully
terraform show migration.tfplan
# Apply only after verification
terraform apply migration.tfplan
7. Test Converted Artefacts#
Validate generated playbooks and Dockerfiles:
# Generate test playbook
resource "souschef_migration" "test" {
cookbook_path = var.cookbook_path
output_path = "/tmp/test-playbooks"
recipe_name = "default"
}
# Run validation
resource "null_resource" "validate_playbook" {
provisioner "local-exec" {
command = "ansible-playbook --syntax-check /tmp/test-playbooks/default.yml"
}
depends_on = [souschef_migration.test]
}
## CI/CD Integration
### GitHub Actions
```yaml
name: Chef to Ansible Migration
on:
pull_request:
paths:
- 'terraform/**'
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install SousChef
run: pip install mcp-souschef
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Terraform Init
run: terraform init
working-directory: terraform
- name: Terraform Plan
run: terraform plan
working-directory: terraform
GitLab CI#
stages:
- assess
- migrate
assess:
stage: assess
script:
- pip install mcp-souschef
- terraform init
- terraform plan
migrate:
stage: migrate
script:
- terraform apply -auto-approve
only:
- main
Limitations#
- The provider requires the SousChef CLI to be installed (>= 2.4.0)
- Migrations run locally on the Terraform executor
- Large cookbooks may take significant time to convert
- The provider does not currently support remote execution
- Habitat conversions require valid plan.sh files
- InSpec migrations support four output formats (testinfra, serverspec, goss, ansible)
- Cost estimates are approximations based on cookbook complexity
Troubleshooting#
Provider Not Found#
If Terraform can't find the provider:
# Verify installation path
ls -la ~/.terraform.d/plugins/registry.terraform.io/kpeacocke/souschef/
# Run terraform init with debug logging
TF_LOG=DEBUG terraform init
SousChef CLI Not Found#
Ensure the CLI is in your PATH or explicitly configured:
# Check CLI availability
which souschef
souschef --version
# Or specify explicit path in provider config
provider "souschef" {
souschef_path = "/usr/local/bin/souschef"
}
Permission Errors#
Check read permissions on cookbook paths and write permissions on output paths:
# Verify cookbook access
ls -la /path/to/cookbooks
# Verify output directory permissions
ls -la /path/to/output
mkdir -p /path/to/output # Create if needed
Conversion Failures#
If migrations fail, check the SousChef CLI directly:
# Test recipe conversion
souschef convert-recipe --cookbook-path /path/to/cookbook \
--output-path /tmp/test --recipe-name default
# Test Habitat conversion
souschef convert-habitat --plan-path /path/to/plan.sh \
--output-path /tmp/test
# Test InSpec conversion
souschef convert-inspec --profile-path /path/to/profile \
--output-path /tmp/test --format testinfra
Cost Estimate Accuracy#
Cost estimates are approximations. For precise estimates:
# Get detailed assessment
data "souschef_assessment" "detailed" {
cookbook_path = var.cookbook_path
}
# Calculate custom estimates
locals {
custom_hours = (
data.souschef_assessment.detailed.estimated_hours *
var.complexity_multiplier
)
custom_cost = local.custom_hours * var.hourly_rate
}
Performance Issues#
For large cookbooks or batch migrations:
# Process in smaller batches
resource "souschef_batch_migration" "batch_1" {
cookbook_path = var.cookbook_path
output_path = var.output_path
recipe_names = slice(var.all_recipes, 0, 5)
}
resource "souschef_batch_migration" "batch_2" {
cookbook_path = var.cookbook_path
output_path = var.output_path
recipe_names = slice(var.all_recipes, 5, 10)
}
Next Steps#
- Review the Migration Guide for best practices
- See CLI Usage for direct SousChef CLI commands
- Check API Reference for programmatic usage