Comprehensive documentation and best practices for building Terraform providers with terraform-plugin-framework (v1.17.0). Covers providers, resources, schemas, types, validators, testing, and common pitfalls.
Overall
score
97%
Learn how to implement the provider interface, configure provider-level settings, and set up the provider server.
The provider.Provider interface is the core of your Terraform provider. It defines metadata, schema, configuration, and registers resources and data sources.
type Provider interface {
// Metadata returns provider type name and version
Metadata(context.Context, MetadataRequest, *MetadataResponse)
// Schema defines provider configuration schema
Schema(context.Context, SchemaRequest, *SchemaResponse)
// Configure initializes provider (create API clients, etc.)
Configure(context.Context, ConfigureRequest, *ConfigureResponse)
// Resources returns all managed resources
Resources(context.Context) []func() resource.Resource
// DataSources returns all data sources
DataSources(context.Context) []func() datasource.DataSource
}// Optional: Register provider functions (Terraform functions)
Functions(context.Context) []func() function.Function
// Optional: Register ephemeral resources (session-scoped)
EphemeralResources(context.Context) []func() ephemeral.EphemeralResource
// Optional: Register provider actions
Actions(context.Context) []func() action.Actionpackage provider
import (
"context"
"os"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// Ensure provider implements provider.Provider
var _ provider.Provider = &ExampleProvider{}
type ExampleProvider struct {
version string
}
// New creates a provider factory function
func New(version string) func() provider.Provider {
return func() provider.Provider {
return &ExampleProvider{
version: version,
}
}
}Returns the provider type name used as prefix for resources and data sources.
func (p *ExampleProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "example"
resp.Version = p.version
}Usage in HCL:
terraform {
required_providers {
example = {
source = "registry.terraform.io/example/example"
}
}
}
provider "example" {
# Configuration here
}
# Resources use TypeName as prefix
resource "example_pet" "my_pet" {
# example + "_" + resource name
}Defines provider-level configuration attributes.
func (p *ExampleProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Example provider for managing resources",
Attributes: map[string]schema.Attribute{
"endpoint": schema.StringAttribute{
Description: "API endpoint URL",
Optional: true,
},
"api_key": schema.StringAttribute{
Description: "API authentication key",
Optional: true,
Sensitive: true, // Masks in logs
},
"timeout": schema.Int64Attribute{
Description: "API request timeout in seconds",
Optional: true,
},
},
}
}Common Patterns:
Optional: true for provider config (allows environment variables)Sensitive: true for secrets (API keys, tokens)Description for Terraform documentation generationInitializes the provider with configuration data. Create API clients, validate credentials, set up connections.
type ExampleProviderModel struct {
Endpoint types.String `tfsdk:"endpoint"`
APIKey types.String `tfsdk:"api_key"`
Timeout types.Int64 `tfsdk:"timeout"`
}func (p *ExampleProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config ExampleProviderModel
// Read configuration
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
// Handle unknown values (during plan phase)
if config.Endpoint.IsUnknown() {
resp.Diagnostics.AddWarning(
"Unable to create client",
"Cannot use unknown value for endpoint during plan",
)
return
}
// Apply defaults from environment if not set in config
endpoint := config.Endpoint.ValueString()
if config.Endpoint.IsNull() {
endpoint = os.Getenv("EXAMPLE_ENDPOINT")
}
apiKey := config.APIKey.ValueString()
if config.APIKey.IsNull() {
apiKey = os.Getenv("EXAMPLE_API_KEY")
}
// Validate required configuration
if endpoint == "" {
resp.Diagnostics.AddError(
"Missing API Endpoint",
"The provider requires an endpoint. Set the endpoint in provider configuration or EXAMPLE_ENDPOINT environment variable.",
)
}
if apiKey == "" {
resp.Diagnostics.AddError(
"Missing API Key",
"The provider requires an API key. Set the api_key in provider configuration or EXAMPLE_API_KEY environment variable.",
)
}
if resp.Diagnostics.HasError() {
return
}
// Create API client
timeout := 30
if !config.Timeout.IsNull() {
timeout = int(config.Timeout.ValueInt64())
}
client, err := NewClient(endpoint, apiKey, timeout)
if err != nil {
resp.Diagnostics.AddError(
"Unable to create API client",
"An error occurred creating the API client: "+err.Error(),
)
return
}
// Make client available to resources and data sources
resp.DataSourceData = client
resp.ResourceData = client
}Key Points:
resp.Diagnostics.HasError() and return earlyIsUnknown() during plan phase (return early with warning)resp.ResourceData and resp.DataSourceDataReturns factory functions for all managed resources.
func (p *ExampleProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewPetResource,
NewUserResource,
NewGroupResource,
}
}
// Resource factory function
func NewPetResource() resource.Resource {
return &PetResource{}
}Returns factory functions for all data sources.
func (p *ExampleProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewPetDataSource,
NewPetsDataSource, // Plural for listing
}
}
func NewPetDataSource() datasource.DataSource {
return &PetDataSource{}
}Resources and data sources can access provider-configured data via the Configure method.
type PetResource struct {
client *Client
}
func (r *PetResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Always check ProviderData is set
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *Client, got: %T", req.ProviderData),
)
return
}
r.client = client
}Pattern:
Configure method sets resp.ResourceDataConfigure method with that datapackage main
import (
"context"
"flag"
"log"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/example/terraform-provider-example/internal/provider"
)
var (
version string = "dev"
)
func main() {
var debug bool
flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers")
flag.Parse()
opts := providerserver.ServeOpts{
// Provider address in Terraform registry format
Address: "registry.terraform.io/example/example",
Debug: debug,
}
err := providerserver.Serve(context.Background(), provider.New(version), opts)
if err != nil {
log.Fatal(err.Error())
}
}For debugging with Delve or other debuggers:
# Start provider in debug mode
go run main.go -debug
# Output will show TF_REATTACH_PROVIDERS value
# Set environment variable in another terminal:
export TF_REATTACH_PROVIDERS='{"registry.terraform.io/example/example":{"Protocol":"grpc","ProtocolVersion":6,"Pid":12345,"Test":true,"Addr":{"Network":"unix","String":"/tmp/plugin123"}}}'
# Run Terraform commands
terraform planThe framework uses protocol version 6 (latest):
// Automatically handled by providerserver.Serve
// No explicit protocol version configuration neededProtocol v6 features:
Support multiple configurations of the same provider:
provider "example" {
alias = "west"
endpoint = "https://api.west.example.com"
}
provider "example" {
alias = "east"
endpoint = "https://api.east.example.com"
}
resource "example_pet" "west_pet" {
provider = example.west
name = "Fluffy"
}
resource "example_pet" "east_pet" {
provider = example.east
name = "Spot"
}Each provider instance calls Configure independently with its own config.
Store common configuration in provider, use in resources:
type ProviderData struct {
Client *APIClient
UserAgent string
RetryMax int
}
// In provider Configure:
data := &ProviderData{
Client: client,
UserAgent: fmt.Sprintf("terraform-provider-example/%s", p.version),
RetryMax: 3,
}
resp.ResourceData = data
// In resource Configure:
data, ok := req.ProviderData.(*ProviderData)
r.data = datafunc TestProvider(t *testing.T) {
provider := New("test")()
// Test Metadata
metadataResp := &provider.MetadataResponse{}
provider.Metadata(context.Background(), provider.MetadataRequest{}, metadataResp)
require.Equal(t, "example", metadataResp.TypeName)
require.Equal(t, "test", metadataResp.Version)
}See Testing for comprehensive testing patterns.
Continue to Resources to learn about implementing CRUD operations and state management.