| name | terraform-provider-design |
| description | Advanced Terraform provider development patterns the official HashiCorp skills do not cover. Use when implementing Test-Driven Development (TDD) workflows (RED-GREEN-REFACTOR with parallel execution), writing drift detection tests with external API modification, using advanced state checks (statecheck.ExpectKnownValue, CompareValue, tfjsonpath, knownvalue, ExpectSensitiveValue), using plan checks (plancheck.ExpectEmptyPlan/ExpectNonEmptyPlan) for idempotency, designing API client architecture (auth, retry, cookie jars), or applying HashiCorp's five core design principles (single API, single object, schema alignment, import, version continuity). For scaffolding a new provider or basic CRUD/schema patterns, use new-terraform-provider and provider-resources instead. |
Terraform Provider Design — Advanced Patterns
Overview
This skill covers advanced Terraform provider patterns that complement the official HashiCorp skills. For scaffolding a new provider, see new-terraform-provider. For basic resource/data source CRUD and schema, see provider-resources. For Plugin Framework Actions, see provider-actions. This skill focuses on TDD workflows, drift detection, advanced state/plan checks, API client architecture, and design principles — gaps the official skills do not cover.
Core TDD Workflow
The RED-GREEN-REFACTOR Cycle
Provider development follows strict TDD phases executed in parallel where possible:
🔴 RED Phase: Write failing acceptance tests
- Define resource/data source behavior through tests
- Write Terraform configuration fixtures
- Verify tests fail for the right reasons
🟢 GREEN Phase: Write minimal CRUD code
- Implement simplest code to pass tests
- Use hardcoded values initially if needed
- Focus on making tests pass quickly
🔄 REFACTOR Phase: Improve implementation
- Add real API integration
- Enhance error handling
- Optimize code quality
- Keep all tests passing
Parallel Execution Pattern
Execute multiple TDD cycles concurrently:
Write("internal/provider/instance_resource_test.go")
Write("internal/provider/network_resource_test.go")
Write("internal/provider/user_data_source_test.go")
Bash("TF_ACC=1 go test -v -timeout 120m ./internal/provider/")
Write("internal/provider/instance_resource.go")
Write("internal/provider/network_resource.go")
Write("internal/provider/user_data_source.go")
Bash("TF_ACC=1 go test -v -timeout 120m ./internal/provider/")
Edit("internal/provider/instance_resource.go")
Edit("internal/provider/network_resource.go")
Edit("internal/provider/user_data_source.go")
Bash("TF_ACC=1 go test -v -timeout 120m ./internal/provider/")
Design Principles
Follow HashiCorp's core design principles (see references/hashicorp_best_practices.md for details):
- Single API Focus: One provider per API/service domain
- Single Object per Resource: One API object per Terraform resource
- Schema Alignment: Match underlying API unless it degrades UX
- Import Support: All resources must support
terraform import
- Version Continuity: Maintain backward compatibility
Testing Requirements
TestCase Structure - The Foundation
Every acceptance test is built around resource.TestCase, the Go struct that defines the complete test lifecycle.
📚 COMPLETE GUIDE: See references/testcase_structure.md for comprehensive TestCase documentation
Essential TestCase Fields
func TestAccResourceName(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_8_0),
},
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
CheckDestroy: testAccCheckResourceDestroy,
Steps: []resource.TestStep{
},
})
}
Key TestCase Fields:
- ✅ ProtoV6ProviderFactories (Required) - Provider factory map
- ✅ Steps (Required) - Array of test operations
- ✅ PreCheck (Recommended) - Environment validation
- ✅ CheckDestroy (Required for resources) - Cleanup verification
- ⚙️ TerraformVersionChecks (Optional) - Version constraints
- ⚙️ ErrorCheck (Optional) - Custom error handling
Acceptance Test Coverage
Every resource MUST test:
- ✅ Create and Read operations
- ✅ ImportState functionality
- ✅ Update and Read operations
- ✅ Delete operations with CheckDestroy
- ✅ Drift detection with plan checks
- ✅ Idempotency verification with ExpectEmptyPlan
📚 For complete TestCase structure details, see references/testcase_structure.md
📚 For comprehensive plan checks guidance, see references/plan_checks_guide.md
Testing Pattern Categories
HashiCorp recommends four core testing patterns (see Testing Patterns):
- Basic Attribute Verification - Configuration applies, attributes persist with correct values
- Update Tests - Resources correctly apply updates while maintaining state consistency
- Import Mode Testing - Verify import behavior with
ImportState: true
- Error and Plan Expectations - Validate expected failures and plan outcomes
State Checks
State checks validate resource attributes in Terraform state after terraform apply. They execute during Lifecycle (config) mode and provide comprehensive attribute verification.
Key Characteristics:
- Multiple state checks can run with aggregated error reporting
- Cleanup executes even when checks fail
- Primarily for validating computed attributes
ExpectKnownValue - Verify Attribute Type and Value:
import (
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
)
StateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(
"example_instance.test",
tfjsonpath.New("name"),
knownvalue.StringExact("test-instance"),
),
statecheck.ExpectKnownValue(
"example_instance.test",
tfjsonpath.New("enabled"),
knownvalue.Bool(true),
),
}
StateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(
"example_instance.test",
tfjsonpath.New("tags").AtMapKey("environment"),
knownvalue.StringExact("production"),
),
}
tfjsonpath.New("name")
tfjsonpath.New("configuration").AtMapKey("setting1")
tfjsonpath.New("items").AtSliceIndex(0)
tfjsonpath.New("network").AtMapKey("subnets").AtSliceIndex(0).AtMapKey("cidr")
CompareValue - Track Attribute Changes Across Steps:
compareValuesSame := statecheck.CompareValue(compare.ValuesSame())
{
Config: testAccInstanceResourceConfig("test"),
ConfigStateChecks: []statecheck.StateCheck{
compareValuesSame.AddStateValue(
"example_instance.test",
tfjsonpath.New("computed_id"),
),
},
}
{
Config: testAccInstanceResourceConfig("test-updated"),
ConfigStateChecks: []statecheck.StateCheck{
compareValuesSame.AddStateValue(
"example_instance.test",
tfjsonpath.New("computed_id"),
),
},
}
Known Value Types:
knownvalue.Bool(true)
knownvalue.StringExact("exact-match")
knownvalue.Int64Exact(42)
knownvalue.Float64Exact(3.14)
knownvalue.Null()
knownvalue.NotNull()
knownvalue.ListExact([]knownvalue.Check{
knownvalue.StringExact("item1"),
knownvalue.StringExact("item2"),
})
knownvalue.MapExact(map[string]knownvalue.Check{
"key1": knownvalue.StringExact("value1"),
"key2": knownvalue.Int64Exact(100),
})
knownvalue.SetExact([]knownvalue.Check{
knownvalue.StringExact("elem1"),
knownvalue.StringExact("elem2"),
})
Sensitive Value Verification (Terraform 1.4.6+):
StateChecks: []statecheck.StateCheck{
statecheck.ExpectSensitiveValue(
"example_instance.test",
tfjsonpath.New("api_key"),
),
}
State Check Best Practices:
- Validate computed attributes, not user-provided configuration
- Use
ExpectKnownValue for exact value matching
- Use
CompareValue to track consistency across test steps
- Use
ExpectSensitiveValue for sensitive attributes
- Leverage JSON paths for nested attribute access
Plan Checks
Plan checks are critical for validating Terraform plan behavior. They inspect plan files at specific phases to ensure expected operations.
📚 COMPREHENSIVE GUIDE: See references/plan_checks_guide.md for complete documentation
Plan checks validate Terraform plan outcomes during test execution. They ensure configuration changes produce expected planning results.
Key Characteristics:
- Multiple checks per phase with aggregated error reporting
- Cleanup executes even when checks fail
- Available in Lifecycle (config) and Refresh test modes
Built-in Plan Check Functions:
import "github.com/hashicorp/terraform-plugin-testing/plancheck"
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
}
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectNonEmptyPlan(),
},
}
Plan Check Example:
func TestAccInstanceResource_Idempotent(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccInstanceResourceConfig("test"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("example_instance.test", "name", "test"),
),
},
{
Config: testAccInstanceResourceConfig("test"),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
},
},
},
})
}
Plan Check Best Practices:
- Use
ExpectEmptyPlan() to verify idempotency after resource creation
- Use
ExpectNonEmptyPlan() to confirm updates actually trigger changes
- Combine with drift detection tests to verify Read operations
- Available check types: General, Resource, Output, and Custom plan checks
Comprehensive Testing Example
Combining state checks, plan checks, and traditional checks:
func TestAccInstanceResource_Complete(t *testing.T) {
compareID := statecheck.CompareValue(compare.ValuesSame())
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccInstanceResourceConfig("test"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("example_instance.test", "name", "test"),
resource.TestCheckResourceAttrSet("example_instance.test", "id"),
),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(
"example_instance.test",
tfjsonpath.New("name"),
knownvalue.StringExact("test"),
),
compareID.AddStateValue(
"example_instance.test",
tfjsonpath.New("id"),
),
},
},
{
Config: testAccInstanceResourceConfig("test"),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
},
},
{
Config: testAccInstanceResourceConfig("test-updated"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("example_instance.test", "name", "test-updated"),
),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(
"example_instance.test",
tfjsonpath.New("name"),
knownvalue.StringExact("test-updated"),
),
compareID.AddStateValue(
"example_instance.test",
tfjsonpath.New("id"),
),
},
},
},
})
}
Regression Testing Pattern
When fixing bugs, follow this workflow:
- Create regression test - Write failing test that reproduces the bug
- Commit test first - Commit shows problem independently verified
- Implement fix - Subsequent commits fix the issue
- Verify test passes - Confirms fix resolves the problem
This allows independent verification of problem reproduction before evaluating solutions
Test Helper Patterns
Create reusable test helpers for common operations:
Test Helpers File (internal/provider/test_helpers.go):
func createTestClient(t *testing.T) *APIClient {
endpoint := os.Getenv("API_ENDPOINT")
username := os.Getenv("API_USERNAME")
password := os.Getenv("API_PASSWORD")
if endpoint == "" || username == "" || password == "" {
t.Fatalf("API credentials not set")
}
client, err := NewAPIClient(context.Background(), endpoint, username, password)
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
return client
}
func getResourceIDByName(t *testing.T, service, method, name string) string {
client := createTestClient(t)
body, err := client.CallAPI(context.Background(), service, method, name)
if err != nil {
t.Fatalf("Failed to get resource: %v", err)
}
var resource map[string]interface{}
json.Unmarshal(body, &resource)
return resource["id"].(string)
}
func verifyResourceDeleted(ctx context.Context, client *APIClient,
service, method, id string, maxRetries int) (bool, error) {
for i := 0; i < maxRetries; i++ {
_, err := client.CallAPI(ctx, service, method, id)
if err != nil {
return true, nil
}
time.Sleep(time.Duration(1<<i) * time.Second)
}
return false, fmt.Errorf("resource still exists after %d retries", maxRetries)
}
func generateUniqueTestName(prefix string) string {
return fmt.Sprintf("%s-%d", prefix, time.Now().Unix())
}
CheckDestroy Pattern
Implement CheckDestroy to verify resource cleanup:
func testAccCheckResourceDestroy(s *terraform.State) error {
client := createTestClient(&testing.T{})
ctx := context.Background()
for _, rs := range s.RootModule().Resources {
if rs.Type != "example_resource" {
continue
}
deleted, err := verifyResourceDeleted(ctx, client,
"ServiceName", "getMethod", rs.Primary.ID, 4)
if !deleted {
if err != nil {
return fmt.Errorf("error checking deletion: %w", err)
}
return fmt.Errorf("resource %s still exists", rs.Primary.ID)
}
}
return nil
}
resource.Test(t, resource.TestCase{
CheckDestroy: testAccCheckResourceDestroy,
})
Drift Detection Test Pattern
Comprehensive drift detection using test helpers:
func TestAccResource_DriftDetection(t *testing.T) {
name := generateUniqueTestName("test-drift")
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccResourceConfig(name, "initial-value"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("example_resource.test", "attr", "initial-value"),
),
},
{
PreConfig: func() {
client := createTestClient(t)
ctx := context.Background()
id := getResourceIDByName(t, "Service", "getMethod", name)
body, _ := client.CallAPI(ctx, "Service", "getMethod", name)
var resourceData map[string]interface{}
json.Unmarshal(body, &resourceData)
resourceData["camelCaseField"] = "modified-value"
resourceData["id"] = id
client.CallAPI(ctx, "Service", "updateMethod", resourceData)
time.Sleep(2 * time.Second)
},
Config: testAccResourceConfig(name, "initial-value"),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectNonEmptyPlan(),
},
},
},
{
Config: testAccResourceConfig(name, "initial-value"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("example_resource.test", "attr", "initial-value"),
),
},
},
})
}
Important Drift Detection Notes:
- Field Name Mapping: APIs often use camelCase while Terraform uses snake_case (e.g.,
kernel_parameters → kernelParameters)
- Document Mappings: Create a mapping table in test_helpers.go for clarity
- Eventual Consistency: Add sleep after API modifications (typically 2s)
- UUID Lookup: Use test helper to get resource ID before modification
For comprehensive drift detection guidance, see references/drift_detection_guide.md
Running Tests
go test -v ./...
TF_ACC=1 go test -v -timeout 120m ./internal/provider/
TF_ACC=1 go test -v -parallel=4 -timeout 120m ./...
TF_ACC=1 go test -v -run "Drift" ./internal/provider/
TF_LOG=TRACE TF_ACC=1 go test -v ./internal/provider/
Quality Standards
- Pass Rate: 100% required
- Coverage: All CRUD operations
- Import: All resources importable
- Execution Time: <120m for full suite
- Parallel Tests: 4-8 concurrent recommended
Sensitive Data Handling
Recommended: Ephemeral Resources
For sensitive data like tokens or secrets:
Alternative: Sensitive Flag
Mark sensitive attributes:
"api_key": schema.StringAttribute{
Required: true,
Sensitive: true,
}
Note: Sensitive flag does NOT encrypt state files. Use remote backends with encryption at rest.
Documentation Generation
Generate provider documentation automatically:
go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs generate
Ensure examples/ directory contains:
provider/provider.tf - Provider configuration
resources/{resource_name}/resource.tf - Resource examples
data-sources/{data_source_name}/data-source.tf - Data source examples
Versioning
Follow Semantic Versioning (MAJOR.MINOR.PATCH):
MAJOR - Breaking changes:
- Removing/renaming resources or attributes
- Changing authentication patterns
- Incompatible type changes
MINOR - New features:
- Adding resources or attributes
- Deprecation warnings
- Compatible type changes
PATCH - Bug fixes only
Recommendation: Major versions no more than once per year
CI/CD and Tooling
Continuous Integration
Set up automated testing with GitHub Actions:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
with:
go-version: "1.21"
- run: go test -v -cover ./...
- run: TF_ACC=1 go test -v -timeout 120m ./internal/provider/
Code Quality and Release
See references/ci_cd_patterns.md for:
- golangci-lint configuration
- GoReleaser setup for provider releases
- Pre-commit hooks
- Documentation generation automation
Testing Best Practices
Test Environment Setup
Create isolated test environments:
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("EXAMPLE_API_KEY"); v == "" {
t.Fatal("EXAMPLE_API_KEY must be set for acceptance tests")
}
if v := os.Getenv("EXAMPLE_API_ENDPOINT"); v == "" {
t.Skip("EXAMPLE_API_ENDPOINT not set, skipping acceptance tests")
}
}
Test Safety
CRITICAL: Use separate accounts/namespaces for testing:
- Prevents accidental production infrastructure changes
- Allows safe parallel test execution
- Enables test cleanup without risk
ID Attribute Requirement
For terraform-plugin-testing v1.5.0+, always add root-level id attribute:
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Resource identifier",
},
Parallel Testing Configuration
TF_ACC=1 go test -v -parallel=4 -timeout 120m ./...
TF_ACC=1 go test -v -run TestAccExampleResource ./internal/provider/
Resources
Templates (assets/)
For provider scaffolding (main.go, provider.go), use the official new-terraform-provider skill.
resource_template.go - Resource implementation template with full CRUD
data_source_template.go - Data source implementation template
acceptance_test_template.go - Acceptance test template with all required steps
References (references/)
Testing Fundamentals:
testcase_structure.md - Complete TestCase structure, all fields, execution flow, and best practices
plan_checks_guide.md - Comprehensive plan checks documentation with all types and patterns
tdd_patterns.md - TDD workflows, test helpers, CheckDestroy, and test structures
drift_detection_guide.md - External modification testing with three-step pattern
Implementation Patterns:
api_client_patterns.md - API client architecture, authentication, retry logic, and error handling
hashicorp_best_practices.md - Official HashiCorp design principles and standards
ci_cd_patterns.md - CI/CD workflows, automation, tooling, and release management
Quick Reference
Provider Test Setup
var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){
"example": providerserver.NewProtocol6WithError(New("test")()),
}
func testAccPreCheck(t *testing.T) {
}
State Check Functions
resource.TestCheckResourceAttr("example_instance.test", "name", "expected")
resource.TestCheckResourceAttrSet("example_instance.test", "id")
resource.TestCheckResourceAttrPair("example_instance.test", "vpc_id", "example_vpc.test", "id")
resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(...),
resource.TestCheckResourceAttrSet(...),
)
Common Anti-Patterns to Avoid
❌ Skipping ImportState tests
❌ Skipping Drift tests
❌ Skipping CheckDestroy implementation
❌ Skipping idempotency tests with plan checks
❌ Using hardcoded test values (use generateUniqueTestName() instead)
❌ Incomplete CRUD testing
❌ Ignoring error cases
❌ Missing or outdated documentation
❌ Not testing state drift
❌ Tests dependent on external state
❌ Not verifying plan outcomes with ExpectEmptyPlan/ExpectNonEmptyPlan
❌ Using TestCheckResourceAttr for computed values (use StateChecks instead)
❌ Not tracking attribute consistency across test steps (use CompareValue)
❌ Missing sensitive attribute verification (use ExpectSensitiveValue)
❌ Not documenting field name mappings (snake_case vs camelCase)
❌ Missing test helper functions for repeated operations
❌ Not using exponential backoff for eventual consistency
❌ Duplicating client setup code across tests
Development Checklist