Provider Architecture & GRPC Protocol
Provider Architecture Overview
Understanding how Terraform providers work under the hood is essential for building reliable, performant providers. This chapter explores the plugin architecture, communication protocols, and lifecycle management.
By the end of this chapter, you’ll understand how Terraform Core and providers communicate, what happens during initialization, and why certain design decisions were made.
The Plugin System
Terraform uses a plugin architecture where providers run as separate processes that communicate with Terraform Core via Remote Procedure Calls (RPC).
Why Plugins?
Benefits:
- ✅ Isolation — Provider crashes don’t crash Terraform
- ✅ Language flexibility — Providers can be written in any language (though Go is standard)
- ✅ Independent development — Providers evolve separately from Terraform Core
- ✅ Security — Process boundaries limit blast radius
- ✅ Scalability — Multiple providers run concurrently
Architecture Diagram
┌─────────────────────────────────────────┐
│ Terraform CLI / Core │
│ (Terraform Language Interpreter) │
└────────────┬────────────────────────────┘
│ GRPC over
│ stdin/stdout
│ (Protocol v6)
┌────────────▼────────────────────────────┐
│ terraform-plugin-go │
│ (GRPC Protocol Implementation) │
└────────────┬────────────────────────────┘
│
┌────────────▼────────────────────────────┐
│ terraform-plugin-framework │
│ (High-level Provider SDK) │
└────────────┬────────────────────────────┘
│
┌────────────▼────────────────────────────┐
│ Your Provider Implementation │
│ (Resources, Data Sources, Logic) │
└────────────┬────────────────────────────┘
│
┌────────────▼────────────────────────────┐
│ External API / Service │
│ (AWS, Azure, Custom Platform) │
└─────────────────────────────────────────┘
GRPC Protocol
Terraform uses GRPC (Google Remote Procedure Call) for provider communication.
What is GRPC?
GRPC is a high-performance RPC framework that uses:
- HTTP/2 for transport
- Protocol Buffers for serialization
- Strongly-typed service definitions
- Bi-directional streaming support
Why GRPC?
Advantages over JSON-RPC (used in older versions):
| Feature | GRPC | JSON-RPC (Legacy) |
|---|---|---|
| Performance | Faster (binary) | Slower (text) |
| Type Safety | Strong | Weak |
| Streaming | Yes | No |
| Versioning | Built-in | Manual |
| Code Generation | Automatic | Manual |
Protocol Version
Terraform supports multiple protocol versions:
| Version | Status | Notes |
|---|---|---|
| Protocol 5 | Legacy | SDK v2 |
| Protocol 6 | Current | Plugin Framework (recommended) |
Check protocol version:
// In provider implementation
func (p *MyProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "mycloud"
resp.Version = p.version
}
Plugin Handshake
When Terraform starts a provider, they perform a handshake to establish compatibility.
Handshake Process
1. Terraform starts provider binary
└─> terraform-provider-aws
2. Provider writes magic cookie to stdout
└─> "PLUGIN_PROTOCOL_VERSIONS=6"
3. Terraform reads cookie and validates
└─> Checks protocol version compatibility
4. If compatible, establish GRPC connection
└─> Provider serves GRPC on assigned port
5. Terraform sends GetProviderSchema request
└─> Provider responds with schema
6. Ready for operations
└─> Create, Read, Update, Delete resources
Magic Cookie
The handshake includes a “magic cookie” for security:
var handshake = plugin.HandshakeConfig{
ProtocolVersion: 6,
MagicCookieKey: "TF_PLUGIN_MAGIC_COOKIE",
MagicCookieValue: "d602bf8f470bc67ca7faa0386276bbdd4330efaf76d1a219cb4d6991ca9872b2",
}
Purpose:
- Prevents accidental execution
- Ensures only Terraform can invoke the provider
- Version negotiation
Communication Flow
Let’s trace a complete operation from Terraform to provider.
Example: Creating a Resource
1. User runs: terraform apply
2. Terraform Core:
├─> Starts provider binary
├─> Performs handshake
├─> Calls GetProviderSchema()
└─> Receives resource schemas
3. Terraform Core:
├─> Parses configuration
├─> Generates execution plan
└─> Calls ValidateResourceConfig()
4. Provider validates:
├─> Checks required attributes
├─> Validates constraints
└─> Returns diagnostics
5. Terraform Core:
└─> Calls ApplyResourceChange() with CREATE action
6. Provider:
├─> Extracts configuration
├─> Calls external API
├─> Creates resource
├─> Returns new state
└─> Includes computed attributes
7. Terraform Core:
├─> Receives new state
├─> Updates state file
└─> Continues to next resource
GRPC Service Definition
Simplified Protocol Buffer definition:
service Provider {
rpc GetProviderSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response);
rpc ValidateProviderConfig(ValidateProviderConfig.Request) returns (ValidateProviderConfig.Response);
rpc ConfigureProvider(ConfigureProvider.Request) returns (ConfigureProvider.Response);
rpc ReadResource(ReadResource.Request) returns (ReadResource.Response);
rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response);
rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response);
rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response);
rpc StopProvider(StopProvider.Request) returns (StopProvider.Response);
}
Provider Lifecycle
A provider goes through distinct lifecycle phases.
Phase 1: Discovery
terraform init
What happens:
- Terraform reads
required_providersblock - Resolves provider source and version
- Downloads provider binary
- Installs in plugin cache (
~/.terraform.d/plugins/)
Phase 2: Initialization
terraform plan
What happens:
- Terraform starts provider binary
- Handshake and GRPC setup
GetProviderSchema()called- Provider returns schemas for all resources/data sources
Code:
func (p *MyProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"endpoint": schema.StringAttribute{
Optional: true,
},
},
}
}
Phase 3: Configuration
What happens:
- Terraform calls
ConfigureProvider() - Passes provider configuration
- Provider initializes (e.g., creates API client)
- Stores client in provider instance
Code:
func (p *MyProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config MyProviderModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
// Initialize API client
client := NewAPIClient(config.Endpoint.ValueString())
// Store for use by resources
resp.ResourceData = client
}
Phase 4: Resource Operations
What happens:
- Terraform calls resource-specific RPCs
- Provider performs CRUD operations
- Returns updated state
- Terraform writes to state file
Phase 5: Shutdown
What happens:
- All operations complete
- Terraform calls
StopProvider() - Provider performs cleanup
- Process terminates
Binary Distribution Model
Providers are distributed as compiled binaries, not source code.
Binary Naming Convention
terraform-provider-{NAME}_v{VERSION}_{OS}_{ARCH}
Examples:
terraform-provider-aws_v5.0.0_linux_amd64
terraform-provider-aws_v5.0.0_darwin_arm64
terraform-provider-aws_v5.0.0_windows_amd64.exe
Installation Locations
Registry providers:
~/.terraform.d/plugins/
└── registry.terraform.io/
└── hashicorp/
└── aws/
└── 5.0.0/
└── linux_amd64/
└── terraform-provider-aws_v5.0.0
Local providers (development):
~/.terraform.d/plugins/
└── registry.terraform.io/
└── YOUR_USERNAME/
└── mycloud/
└── 0.1.0/
└── darwin_arm64/
└── terraform-provider-mycloud
Platform Support
Standard targets for multi-platform builds:
# .goreleaser.yml
builds:
- goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
Results in 6 binaries:
- linux_amd64
- linux_arm64
- darwin_amd64 (Intel Mac)
- darwin_arm64 (M1/M2 Mac)
- windows_amd64
- windows_arm64
Process Management
Concurrent Providers
Terraform can run multiple provider instances:
provider "aws" {
alias = "us-east-1"
region = "us-east-1"
}
provider "aws" {
alias = "us-west-2"
region = "us-west-2"
}
Result: Two separate provider processes, one per configuration.
Resource Parallelism
Terraform runs operations in parallel by default:
terraform apply -parallelism=10
Default: 10 concurrent operations
Each operation: Separate GRPC call to provider
Provider must: Handle concurrent requests safely
State Locking
Providers don’t handle state locking directly—Terraform Core does.
But providers should:
- Be stateless between calls
- Handle concurrent API requests
- Implement proper synchronization for shared resources
Error Handling
Providers communicate errors through diagnostics.
Diagnostic Types
Error:
resp.Diagnostics.AddError(
"Failed to Create Resource",
fmt.Sprintf("API returned 500: %s", err),
)
Warning:
resp.Diagnostics.AddWarning(
"Deprecated Attribute",
"The 'legacy_mode' attribute is deprecated. Use 'mode' instead.",
)
Attribute-Specific:
resp.Diagnostics.AddAttributeError(
path.Root("endpoint"),
"Invalid Endpoint",
"Endpoint must start with https://",
)
User-Facing Errors
Terraform displays diagnostics to users:
╷
│ Error: Failed to Create Resource
│
│ with mycloud_environment.example,
│ on main.tf line 10, in resource "mycloud_environment" "example":
│ 10: resource "mycloud_environment" "example" {
│
│ API returned 500: Internal Server Error
╵
Performance Considerations
Provider Startup Cost
Each provider invocation has overhead:
Startup time breakdown:
- Binary execution: 10-50ms
- GRPC handshake: 5-10ms
- Schema transfer: 10-50ms (depends on schema size)
Total: 25-110ms per provider instance
Mitigation:
- Terraform reuses provider instances within a single run
- Minimize provider configurations (aliases)
Schema Size
Large schemas slow initialization:
// ❌ Bad: Massive schema
Attributes: map[string]schema.Attribute{
"field1": schema.StringAttribute{...},
"field2": schema.StringAttribute{...},
// ... 10,000+ attributes
}
// ✅ Good: Nested schemas
Attributes: map[string]schema.Attribute{
"config": schema.SingleNestedAttribute{
Attributes: map[string]schema.Attribute{
"field1": schema.StringAttribute{...},
// Grouped logically
},
},
}
API Rate Limiting
Providers should:
- Implement retries with exponential backoff
- Respect API rate limits
- Use bulk operations when available
- Cache results when appropriate
Security Model
Process Isolation
Benefits:
- Provider bugs don’t crash Terraform
- Compromised provider has limited system access
- Resource limits can be applied per process
Limitations:
- Provider still runs with user permissions
- Can access environment variables
- Can make arbitrary network calls
Credential Handling
Best practices:
// ✅ Good: Sensitive attribute
"api_key": schema.StringAttribute{
Optional: true,
Sensitive: true, // Prevents logging
},
// ❌ Bad: Plain text in logs
log.Printf("API Key: %s", apiKey) // DON'T DO THIS
// ✅ Good: Redacted logging
tflog.Debug(ctx, "Authenticating with API", map[string]interface{}{
"endpoint": endpoint,
// api_key automatically redacted if marked sensitive
})
State File Security
Providers should:
- Mark sensitive attributes as
Sensitive: true - Never log sensitive data
- Assume state files may be compromised
Debugging Provider Communication
Enable GRPC Logging
TF_LOG=DEBUG terraform apply
Shows:
2024-01-15T10:30:00.123-0700 [DEBUG] provider: starting plugin: path=.terraform/providers/...
2024-01-15T10:30:00.234-0700 [DEBUG] provider: plugin started: address=/var/tmp/plugin123
2024-01-15T10:30:00.345-0700 [DEBUG] provider: GRPC handshake complete
Provider Debug Mode
Run provider with debugger support:
./terraform-provider-mycloud -debug
Output:
Provider started. To attach Terraform:
TF_REATTACH_PROVIDERS='{"registry.terraform.io/YOUR_USERNAME/mycloud":{"Protocol":"grpc","ProtocolVersion":6,"Pid":12345,"Test":true,"Addr":{"Network":"unix","String":"/tmp/plugin123"}}}'
Use in Terraform:
export TF_REATTACH_PROVIDERS='...'
terraform apply
Key Takeaways
- ✅ Providers run as separate processes using GRPC
- ✅ Protocol version 6 is current (Plugin Framework)
- ✅ Handshake establishes compatibility
- ✅ Providers are distributed as compiled binaries
- ✅ Multiple provider instances can run concurrently
- ✅ Diagnostics communicate errors to users
- ✅ Process isolation provides security boundaries
- ✅ Debugging tools reveal GRPC communication
What’s Next?
Now that you understand how providers work, let’s discuss when you should build a custom provider versus using alternatives.
Next: When to Build a Custom Provider →
Previous: ← Plugin Framework vs SDK
Last updated: June 30, 2026