When your infrastructure includes proprietary services or internal APIs that existing Terraform providers do not cover, you can build a custom provider using the Terraform Plugin Framework in Go. This guide covers the architecture, implementation, and testing of custom providers.
Provider Architecture
# Standard provider structure
terraform-provider-myservice/
├── internal/
│ └── provider/
│ ├── provider.go # Provider configuration
│ ├── resource_server.go # Server resource
│ ├── data_server.go # Server data source
│ └── models.go # Data models
├── main.go # Entry point
├── go.mod
└── examples/
└── main.tf
Provider Implementation
// internal/provider/provider.go
package provider
import (
"context"
"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"
)
type MyServiceProvider struct{ version string }
func (p *MyServiceProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"api_url": schema.StringAttribute{Required: true},
"api_token": schema.StringAttribute{Required: true, Sensitive: true},
},
}
}
func (p *MyServiceProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config MyServiceProviderModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
client := NewAPIClient(config.ApiUrl.ValueString(), config.ApiToken.ValueString())
resp.DataSourceData = client
resp.ResourceData = client
}
func (p *MyServiceProvider) Resources(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{NewServerResource}
}
func (p *MyServiceProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{NewServerDataSource}
}
Resource CRUD Implementation
// internal/provider/resource_server.go
func (r *ServerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan ServerModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() { return }
server, err := r.client.CreateServer(plan.Name.ValueString(), plan.Size.ValueString())
if err != nil {
resp.Diagnostics.AddError("Create Error", err.Error())
return
}
plan.ID = types.StringValue(server.ID)
plan.IP = types.StringValue(server.IP)
resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
}
func (r *ServerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state ServerModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
server, err := r.client.GetServer(state.ID.ValueString())
if err != nil {
resp.State.RemoveResource(ctx)
return
}
state.Name = types.StringValue(server.Name)
state.IP = types.StringValue(server.IP)
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}
Testing
// Use acceptance tests
func TestAccServerResource(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: `resource "myservice_server" "test" { name = "test-server" size = "small" }`,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("myservice_server.test", "name", "test-server"),
),
},
},
})
}
# Run tests
TF_ACC=1 go test ./internal/provider/ -v
Building and Installing
# Build
go build -o terraform-provider-myservice
# Install locally
mkdir -p ~/.terraform.d/plugins/registry.terraform.io/myorg/myservice/1.0.0/linux_amd64
cp terraform-provider-myservice ~/.terraform.d/plugins/registry.terraform.io/myorg/myservice/1.0.0/linux_amd64/
# Use in Terraform
terraform {
required_providers {
myservice = {
source = "myorg/myservice"
version = "1.0.0"
}
}
}
Summary
Custom Terraform providers extend infrastructure as code to any API or service. The Plugin Framework provides a structured approach to implementing resources and data sources with proper state management, validation, and error handling. For organizations with internal platforms or proprietary infrastructure, a custom provider brings those resources under Terraform management alongside public cloud resources.