Go for DevOps: Writing Terraform Providers
go devops
Terraform providers let you manage any API as infrastructure. When the provider you need doesn’t exist, you can write your own in Go. Here’s how.
Why Write a Provider
- Internal APIs without existing providers
- Custom integrations
- Self-service platforms
- Extending existing providers
Provider Architecture
Terraform Core ←→ Provider (Go binary) ←→ API
Provider responsibilities:
├── Resource CRUD operations
├── Data source reads
├── Schema definition
└── State management
Setup
Dependencies
# Initialize module
go mod init terraform-provider-myservice
# Add dependencies
go get github.com/hashicorp/terraform-plugin-framework
Project Structure
terraform-provider-myservice/
├── main.go
├── internal/
│ └── provider/
│ ├── provider.go
│ ├── resource_item.go
│ └── data_source_item.go
├── go.mod
└── go.sum
The Provider
// 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"
"github.com/hashicorp/terraform-plugin-framework/types"
)
type MyServiceProvider struct {
version string
}
type MyServiceProviderModel struct {
Endpoint types.String `tfsdk:"endpoint"`
ApiKey types.String `tfsdk:"api_key"`
}
func (p *MyServiceProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "myservice"
resp.Version = p.version
}
func (p *MyServiceProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"endpoint": schema.StringAttribute{
Required: true,
Description: "The API endpoint",
},
"api_key": schema.StringAttribute{
Required: true,
Sensitive: true,
Description: "API key for authentication",
},
},
}
}
func (p *MyServiceProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config MyServiceProviderModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
// Create API client
client := NewAPIClient(config.Endpoint.ValueString(), config.ApiKey.ValueString())
resp.DataSourceData = client
resp.ResourceData = client
}
func (p *MyServiceProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewItemResource,
}
}
func (p *MyServiceProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewItemDataSource,
}
}
func New(version string) func() provider.Provider {
return func() provider.Provider {
return &MyServiceProvider{version: version}
}
}
A Resource
// internal/provider/resource_item.go
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
type ItemResource struct {
client *APIClient
}
type ItemResourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Description types.String `tfsdk:"description"`
}
func NewItemResource() resource.Resource {
return &ItemResource{}
}
func (r *ItemResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_item"
}
func (r *ItemResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
},
"name": schema.StringAttribute{
Required: true,
},
"description": schema.StringAttribute{
Optional: true,
},
},
}
}
func (r *ItemResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
r.client = req.ProviderData.(*APIClient)
}
func (r *ItemResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan ItemResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
// Call API
item, err := r.client.CreateItem(plan.Name.ValueString(), plan.Description.ValueString())
if err != nil {
resp.Diagnostics.AddError("Create failed", err.Error())
return
}
// Set state
plan.ID = types.StringValue(item.ID)
resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
}
func (r *ItemResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state ItemResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
item, err := r.client.GetItem(state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Read failed", err.Error())
return
}
state.Name = types.StringValue(item.Name)
state.Description = types.StringValue(item.Description)
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}
func (r *ItemResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan ItemResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
_, err := r.client.UpdateItem(plan.ID.ValueString(), plan.Name.ValueString(), plan.Description.ValueString())
if err != nil {
resp.Diagnostics.AddError("Update failed", err.Error())
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
}
func (r *ItemResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state ItemResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
err := r.client.DeleteItem(state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Delete failed", err.Error())
return
}
}
Main Entry Point
// main.go
package main
import (
"context"
"flag"
"log"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"terraform-provider-myservice/internal/provider"
)
var version = "dev"
func main() {
var debug bool
flag.BoolVar(&debug, "debug", false, "set to true to enable debug mode")
flag.Parse()
opts := providerserver.ServeOpts{
Address: "registry.terraform.io/myorg/myservice",
Debug: debug,
}
err := providerserver.Serve(context.Background(), provider.New(version), opts)
if err != nil {
log.Fatal(err)
}
}
Building and Installing
# Build
go build -o terraform-provider-myservice
# Install locally (macOS)
mkdir -p ~/.terraform.d/plugins/registry.terraform.io/myorg/myservice/1.0.0/darwin_amd64
mv terraform-provider-myservice ~/.terraform.d/plugins/registry.terraform.io/myorg/myservice/1.0.0/darwin_amd64/
Using the Provider
# main.tf
terraform {
required_providers {
myservice = {
source = "myorg/myservice"
version = "~> 1.0"
}
}
}
provider "myservice" {
endpoint = "https://api.myservice.com"
api_key = var.myservice_api_key
}
resource "myservice_item" "example" {
name = "my-item"
description = "An example item"
}
Testing
// internal/provider/resource_item_test.go
package provider
import (
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)
func TestAccItemResource(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: `
provider "myservice" {
endpoint = "https://test.local"
api_key = "test"
}
resource "myservice_item" "test" {
name = "test-item"
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("myservice_item.test", "name", "test-item"),
resource.TestCheckResourceAttrSet("myservice_item.test", "id"),
),
},
},
})
}
Publishing
# Build for multiple platforms
goreleaser release --rm-dist
# Register with Terraform Registry
# 1. Create GitHub release with binaries
# 2. Sign with GPG key
# 3. Add to registry via GitHub OAuth
Best Practices
- Idempotent operations: Create should be safe to retry
- Handle drift: Read should update state from reality
- Sensitive data: Mark sensitive attributes
- Timeouts: Use context for cancellation
- Logging: Use tflog for debugging
Final Thoughts
Writing Terraform providers is straightforward with the plugin framework. If you have an internal service or custom API, a provider brings it into your IaC workflow.
The Go tooling ecosystem makes building, testing, and distributing providers reliable.
Everything as code, including your custom APIs.