Docs / Cloud & DevOps / Custom Terraform Providers

Custom Terraform Providers

By Admin · Mar 15, 2026 · Updated Apr 25, 2026 · 286 views · 3 min read

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.

Was this article helpful?