Documentation Index
Fetch the complete documentation index at: https://mintlify.com/hashicorp/terraform/llms.txt
Use this file to discover all available pages before exploring further.
Provider Development Overview
Terraform providers are standalone plugin binaries that communicate with Terraform Core using the gRPC protocol. This architecture enables providers to be developed independently in any language that supports gRPC.
Plugin Protocol
Terraform uses HashiCorp’s go-plugin library for the plugin system, implementing communication over gRPC.
Protocol Versions
Terraform supports multiple protocol versions for backward compatibility (internal/plugin/serve.go:12):
const (
ProviderPluginName = "provider"
ProvisionerPluginName = "provisioner"
DefaultProtocolVersion = 4 // Legacy compatibility
)
Current Protocol: Version 5 (recommended for new providers)
Handshake Configuration
The plugin handshake ensures Terraform Core and providers are compatible (internal/plugin/serve.go:26):
var Handshake = plugin.HandshakeConfig{
ProtocolVersion: DefaultProtocolVersion,
MagicCookieKey: "TF_PLUGIN_MAGIC_COOKIE",
MagicCookieValue: "d602bf8f470bc67ca7faa0386276bbdd4330efaf76d1a219cb4d6991ca9872b2",
}
Purpose:
- Verify provider compatibility
- Prevent accidental execution of non-provider binaries
- Negotiate protocol version
Implementing a Provider
Provider Interface
Implement the providers.Interface (internal/providers/provider.go:17) to create a provider:
type Interface interface {
// Schema methods
GetProviderSchema() GetProviderSchemaResponse
GetResourceIdentitySchemas() GetResourceIdentitySchemasResponse
// Configuration methods
ValidateProviderConfig(ValidateProviderConfigRequest) ValidateProviderConfigResponse
ConfigureProvider(ConfigureProviderRequest) ConfigureProviderResponse
// Resource methods
ValidateResourceConfig(ValidateResourceConfigRequest) ValidateResourceConfigResponse
ReadResource(ReadResourceRequest) ReadResourceResponse
PlanResourceChange(PlanResourceChangeRequest) PlanResourceChangeResponse
ApplyResourceChange(ApplyResourceChangeRequest) ApplyResourceChangeResponse
ImportResourceState(ImportResourceStateRequest) ImportResourceStateResponse
UpgradeResourceState(UpgradeResourceStateRequest) UpgradeResourceStateResponse
// Data source methods
ValidateDataResourceConfig(ValidateDataResourceConfigRequest) ValidateDataResourceConfigResponse
ReadDataSource(ReadDataSourceRequest) ReadDataSourceResponse
// Lifecycle methods
Stop() error
Close() error
}
Serving the Provider
Use the plugin.Serve function to expose your provider (internal/plugin/serve.go:52):
func main() {
plugin.Serve(&plugin.ServeOpts{
GRPCProviderFunc: func() proto.ProviderServer {
return &MyProvider{}
},
})
}
Provider Factory
For built-in or testing providers, use the Factory pattern (internal/providers/factory.go:6):
type Factory func() (Interface, error)
// Create a fixed factory for testing
func FactoryFixed(p Interface) Factory {
return func() (Interface, error) {
return p, nil
}
}
Schema Definition
Provider Schema
Define the schema for your provider configuration:
func (p *MyProvider) GetProviderSchema() providers.GetProviderSchemaResponse {
return providers.GetProviderSchemaResponse{
Provider: providers.Schema{
Version: 1,
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"api_token": {
Type: cty.String,
Required: true,
Sensitive: true,
},
"endpoint": {
Type: cty.String,
Optional: true,
},
},
},
},
ResourceTypes: map[string]providers.Schema{
"mycloud_instance": instanceSchema,
"mycloud_database": databaseSchema,
},
DataSources: map[string]providers.Schema{
"mycloud_image": imageDataSourceSchema,
},
}
}
Resource Schema
Define schemas for each resource type:
var instanceSchema = providers.Schema{
Version: 1,
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"name": {
Type: cty.String,
Required: true,
},
"size": {
Type: cty.String,
Required: true,
},
"tags": {
Type: cty.Map(cty.String),
Optional: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"network": {
Nesting: configschema.NestingList,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"subnet_id": {
Type: cty.String,
Required: true,
},
},
},
},
},
},
}
Resource Lifecycle Implementation
Read Operation
Refresh the current state of a resource (internal/providers/provider.go:455):
func (p *MyProvider) ReadResource(req providers.ReadResourceRequest) providers.ReadResourceResponse {
// Extract resource ID from prior state
id := req.PriorState.GetAttr("id").AsString()
// Fetch current state from API
current, err := p.client.GetInstance(id)
if err != nil {
if isNotFound(err) {
// Resource no longer exists
return providers.ReadResourceResponse{
NewState: cty.NullVal(req.PriorState.Type()),
}
}
return providers.ReadResourceResponse{
Diagnostics: diagnosticsFromError(err),
}
}
// Convert API response to state value
newState := instanceToState(current)
return providers.ReadResourceResponse{
NewState: newState,
}
}
Plan Operation
Compute planned changes for a resource (internal/providers/provider.go:536):
func (p *MyProvider) PlanResourceChange(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
var requiresReplace []cty.Path
// Check if resource needs replacement
priorName := req.PriorState.GetAttr("name")
proposedName := req.ProposedNewState.GetAttr("name")
if !priorName.IsNull() && !priorName.RawEquals(proposedName) {
// Name change requires replacement
requiresReplace = append(requiresReplace, cty.GetAttrPath("name"))
}
// Compute planned state (may include unknown values)
plannedState := req.ProposedNewState
return providers.PlanResourceChangeResponse{
PlannedState: plannedState,
RequiresReplace: requiresReplace,
}
}
Apply Operation
Execute planned changes and return final state (internal/providers/provider.go:604):
func (p *MyProvider) ApplyResourceChange(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
// Handle deletion
if req.PlannedState.IsNull() {
id := req.PriorState.GetAttr("id").AsString()
err := p.client.DeleteInstance(id)
if err != nil && !isNotFound(err) {
return providers.ApplyResourceChangeResponse{
Diagnostics: diagnosticsFromError(err),
}
}
return providers.ApplyResourceChangeResponse{
NewState: cty.NullVal(req.PriorState.Type()),
}
}
// Handle creation
if req.PriorState.IsNull() {
instance, err := p.client.CreateInstance({
Name: req.PlannedState.GetAttr("name").AsString(),
Size: req.PlannedState.GetAttr("size").AsString(),
})
if err != nil {
return providers.ApplyResourceChangeResponse{
Diagnostics: diagnosticsFromError(err),
}
}
return providers.ApplyResourceChangeResponse{
NewState: instanceToState(instance),
}
}
// Handle update
id := req.PriorState.GetAttr("id").AsString()
instance, err := p.client.UpdateInstance(id, {
Size: req.PlannedState.GetAttr("size").AsString(),
})
if err != nil {
return providers.ApplyResourceChangeResponse{
Diagnostics: diagnosticsFromError(err),
}
}
return providers.ApplyResourceChangeResponse{
NewState: instanceToState(instance),
}
}
Import Operation
Import existing infrastructure (internal/providers/provider.go:658):
func (p *MyProvider) ImportResourceState(req providers.ImportResourceStateRequest) providers.ImportResourceStateResponse {
// Fetch resource by ID
instance, err := p.client.GetInstance(req.ID)
if err != nil {
return providers.ImportResourceStateResponse{
Diagnostics: diagnosticsFromError(err),
}
}
return providers.ImportResourceStateResponse{
ImportedResources: []providers.ImportedResource{
{
TypeName: req.TypeName,
State: instanceToState(instance),
},
},
}
}
gRPC Communication
The GRPCProvider handles translation between Terraform and the provider (internal/plugin/grpc_provider.go:49):
Client-Side Translation
type GRPCProvider struct {
PluginClient *plugin.Client
TestServer *grpc.Server
Addr addrs.Provider
client proto.ProviderClient
ctx context.Context
schema providers.GetProviderSchemaResponse
}
Schema Caching
Providers can declare GetProviderSchemaOptional to enable schema caching (internal/plugin/grpc_provider.go:86):
if !p.Addr.IsZero() {
if resp, ok := providers.SchemaCache.Get(p.Addr); ok {
if resp.ServerCapabilities.GetProviderSchemaOptional {
return resp // Use cached schema
}
}
}
Message Size Limits
Large schemas require increased message size limits (internal/plugin/grpc_provider.go:115):
const maxRecvSize = 64 << 20 // 64MB
protoResp, err := p.client.GetSchema(
p.ctx,
new(proto.GetProviderSchema_Request),
grpc.MaxRecvMsgSizeCallOption{MaxRecvMsgSize: maxRecvSize},
)
Error Handling and Diagnostics
Diagnostic Types
Providers return diagnostics for errors and warnings:
type Diagnostics []Diagnostic
type Diagnostic struct {
Severity Severity // Error or Warning
Summary string // Short description
Detail string // Detailed explanation
Subject *SourceRange // Location in configuration
}
Creating Diagnostics
func diagnosticsFromError(err error) tfdiags.Diagnostics {
return tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"API Error",
fmt.Sprintf("Failed to communicate with API: %s", err),
),
}
}
State Upgrade
Handle schema version changes with state upgrade (internal/providers/provider.go:392):
func (p *MyProvider) UpgradeResourceState(req providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse {
// Unmarshal old state
var oldState map[string]interface{}
json.Unmarshal(req.RawStateJSON, &oldState)
// Migrate from version 0 to version 1
if req.Version == 0 {
// Add new required field with default value
oldState["region"] = "us-east-1"
}
// Convert to current schema
newState := mapToState(oldState)
return providers.UpgradeResourceStateResponse{
UpgradedState: newState,
}
}
Testing Providers
Unit Testing
Test provider logic in isolation:
func TestReadResource(t *testing.T) {
provider := &MyProvider{
client: &mockClient{
instances: map[string]*Instance{
"i-12345": {ID: "i-12345", Name: "test"},
},
},
}
req := providers.ReadResourceRequest{
TypeName: "mycloud_instance",
PriorState: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-12345"),
}),
}
resp := provider.ReadResource(req)
if resp.Diagnostics.HasErrors() {
t.Fatalf("unexpected errors: %s", resp.Diagnostics)
}
if resp.NewState.GetAttr("name").AsString() != "test" {
t.Error("expected name to be 'test'")
}
}
Integration Testing
Use Terraform’s test framework for end-to-end testing with actual Terraform operations.
Best Practices
While you can implement the provider interface directly, use the official Terraform Plugin SDK for production providers. It provides:
- Schema helpers
- CRUD operation scaffolding
- Testing utilities
- Automatic protocol handling
Implement Graceful Degradation
Handle API errors gracefully:
if isRateLimitError(err) {
// Return warning instead of error
return ReadResourceResponse{
NewState: req.PriorState, // Keep existing state
Diagnostics: tfdiags.Diagnostics{
tfdiags.Warning(
"Rate Limited",
"API rate limit reached. Using cached state.",
),
},
}
}
Validate Early
Implement ValidateResourceConfig to catch errors before apply:
func (p *MyProvider) ValidateResourceConfig(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse {
size := req.Config.GetAttr("size").AsString()
if !isValidSize(size) {
return providers.ValidateResourceConfigResponse{
Diagnostics: tfdiags.Diagnostics{
tfdiags.AttributeValue(
tfdiags.Error,
"Invalid size",
fmt.Sprintf("Size %q is not valid. Must be one of: small, medium, large.", size),
cty.GetAttrPath("size"),
),
},
}
}
return providers.ValidateResourceConfigResponse{}
}
Store provider-specific data in the Private field:
type privateMetadata struct {
InternalID string
ETag string
}
func (p *MyProvider) ReadResource(req providers.ReadResourceRequest) providers.ReadResourceResponse {
var meta privateMetadata
json.Unmarshal(req.Private, &meta)
// Use ETag for conditional requests
instance, err := p.client.GetInstanceIfModified(id, meta.ETag)
newMeta, _ := json.Marshal(privateMetadata{
InternalID: instance.InternalID,
ETag: instance.ETag,
})
return providers.ReadResourceResponse{
NewState: instanceToState(instance),
Private: newMeta,
}
}
Handle Partial Updates
Return partial state on errors during apply:
func (p *MyProvider) ApplyResourceChange(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
id := req.PriorState.GetAttr("id").AsString()
// Update instance size
if err := p.client.UpdateSize(id, newSize); err != nil {
// Return current state even on error
current, _ := p.client.GetInstance(id)
return providers.ApplyResourceChangeResponse{
NewState: instanceToState(current),
Diagnostics: diagnosticsFromError(err),
}
}
// Continue with other updates...
}
Next Steps