Terraform
Creating Custom Terraform Providers
Learn how to develop your own Terraform provider to extend Terraform's capabilities.
January 17, 2024
DevHub Team
4 min read
Creating Custom Terraform Providers
Learn how to develop your own Terraform provider to extend Terraform's functionality and integrate with custom services.
Understanding Terraform Providers
Terraform providers are plugins that enable Terraform to manage resources in various platforms and services. While HashiCorp and the community maintain many providers, you might need to create a custom provider for:
- Internal services
- Proprietary platforms
- Unsupported APIs
- Specialized requirements
Prerequisites
Before creating a custom provider, you need:
# Install Go brew install go # Install Terraform brew install terraform # Set up Go workspace mkdir -p $GOPATH/src/github.com/yourusername/terraform-provider-example cd $GOPATH/src/github.com/yourusername/terraform-provider-example
Provider Structure
A typical provider project structure:
terraform-provider-example/ ├── main.go ├── provider/ │ ├── provider.go │ ├── resource_example.go │ └── data_source_example.go ├── examples/ │ └── main.tf └── go.mod
Basic Provider Implementation
1. Provider Definition
// provider/provider.go package provider import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func Provider() *schema.Provider { return &schema.Provider{ ResourcesMap: map[string]*schema.Resource{ "example_resource": resourceExample(), }, DataSourcesMap: map[string]*schema.Resource{ "example_data": dataSourceExample(), }, Schema: map[string]*schema.Schema{ "api_token": { Type: schema.TypeString, Required: true, Sensitive: true, }, }, } }
2. Resource Implementation
// provider/resource_example.go package provider import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceExample() *schema.Resource { return &schema.Resource{ Create: resourceExampleCreate, Read: resourceExampleRead, Update: resourceExampleUpdate, Delete: resourceExampleDelete, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "description": { Type: schema.TypeString, Optional: true, }, }, } } func resourceExampleCreate(d *schema.ResourceData, m interface{}) error { // Implementation for creating a resource name := d.Get("name").(string) // API calls to create resource d.SetId("generated-id") return nil }
3. Data Source Implementation
// provider/data_source_example.go package provider import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceExample() *schema.Resource { return &schema.Resource{ Read: dataSourceExampleRead, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "value": { Type: schema.TypeString, Computed: true, }, }, } }
Main Entry Point
// main.go package main import ( "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" "github.com/yourusername/terraform-provider-example/provider" ) func main() { plugin.Serve(&plugin.ServeOpts{ ProviderFunc: provider.Provider, }) }
Testing Your Provider
1. Unit Tests
// provider/resource_example_test.go package provider import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccExampleResource_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccExampleResourceConfig_basic, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr( "example_resource.test", "name", "test"), ), }, }, }) }
2. Acceptance Tests
# Create test configuration provider "example" { api_token = "test-token" } resource "example_resource" "test" { name = "test-resource" description = "Test resource description" }
Building and Installing
# Build the provider go build -o terraform-provider-example # Install locally mkdir -p ~/.terraform.d/plugins/registry.terraform.io/yourusername/example/1.0.0/darwin_amd64 cp terraform-provider-example ~/.terraform.d/plugins/registry.terraform.io/yourusername/example/1.0.0/darwin_amd64/
Using Your Custom Provider
terraform { required_providers { example = { source = "yourusername/example" version = "1.0.0" } } } provider "example" { api_token = var.api_token } resource "example_resource" "my_resource" { name = "custom-resource" description = "Created with custom provider" }
Best Practices
- Error Handling
if err != nil { return fmt.Errorf("error creating resource: %s", err) }
- Resource Validation
Schema: map[string]*schema.Schema{ "port": { Type: schema.TypeInt, Required: true, ValidateFunc: validation.IntBetween(1, 65535), }, }
- Documentation
// Add descriptions to schema fields Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, Description: "The name of the resource", }, }
Advanced Features
1. Custom Validation
func validateName(v interface{}, k string) (ws []string, es []error) { value := v.(string) if len(value) > 50 { es = append(es, fmt.Errorf("name cannot be longer than 50 characters")) } return }
2. State Migration
func resourceExampleV0() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ "old_field": { Type: schema.TypeString, Required: true, }, }, } } func resourceExampleStateUpgradeV0(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { // Migration logic return rawState, nil }
Debugging Tips
- Enable Logging
export TF_LOG=DEBUG export TF_LOG_PATH=terraform.log
- Use Debugger
import "log" log.Printf("[DEBUG] Resource data: %#v", d)
Publishing Your Provider
- Create a GitHub repository
- Tag releases with semantic versioning
- Document usage and examples
- Submit to Terraform Registry (optional)
Conclusion
Creating a custom provider allows you to:
- Extend Terraform's capabilities
- Integrate with internal services
- Maintain consistency in infrastructure
- Automate custom workflows
Remember to:
- Follow Go best practices
- Handle errors gracefully
- Write comprehensive tests
- Document your provider
- Maintain backward compatibility
IaC
DevOps
Go
Provider Development