Blog

Building Your Own Terraform Provider with Claude

Marta Radziszewska

Marta Radziszewska

April 28, 2026
9 minutes

Introduction

Terraform providers often feel like a black box. You declare resources, run terraform apply, and something somewhere makes the right API calls. In this post, I open that box — by building a provider myself, without really knowing Go, and with a lot of help from Claude.

This is a step-by-step walkthrough for getting started with building your own Terraform provider. Whether you are curious how providers work under the hood or you have a custom API you want to manage through Terraform, the guide will take you from zero to a working provider. No Go experience required.

A Terraform provider is essentially an abstraction layer over an API: if a service exposes CRUD operations, you can wrap it in a provider and manage it through Terraform like any other piece of infrastructure. I “vibe coded” this one by describing the API and letting Claude generate most of the structure, then iterated to understand how everything fits together.

Implementing a provider

At its core, a Terraform provider is a structured layer that translates declarative configuration into API calls. Once this structure is understood, the overall implementation becomes significantly more approachable.

Terraform providers are, in practice, implemented in Go. While Terraform’s plugin system is technically language-agnostic, HashiCorp only officially supports Go through its SDKs and frameworks.

I do not have much experience with Go myself, so I relied on Claude to generate the initial implementation and then worked from there to understand and refine it.

Prerequisites

To follow along, you will need:

  • Go installed
  • Terraform installed

What makes an API suitable?

A Terraform provider can be built for any API that supports basic CRUD operations:

  • Create an object
  • Retrieve that object
  • Update the object
  • Delete the object

This maps directly to Terraform’s resource lifecycle.

Generating the initial implementation

The API I was working with was not well documented, so rather than pointing Claude at a documentation page, I prepared a set of reference files myself:

  • A file listing all API endpoints, grouped by operation (create, read, update, delete), along with any required query parameters
  • A set of JSON files containing example request and response payloads, one per endpoint where a payload was required

If the API you are working with has good documentation, you can skip this step entirely and give Claude a link to it instead. In my case, constructing the files manually was worth the effort since it also forced me to understand the API before generating anything.

With those files in place, the prompt was straightforward:

Build a Terraform provider based on the example APIs found in path/to/your/files/

Based on this, it produced a provider with the following structure:

.
├── internal
│   ├── client
│   │   └── client.go
│   └── provider
│       ├── example_resource_1.go
│       ├── example_resource_2.go
│       └── provider.go
├── go.mod
├── go.sum
└── main.go

At first glance, this structure may seem complex, but most of it is standard boilerplate.

The go.mod and go.sum files manage dependencies and generally do not require manual changes.

main.go

This file serves as the entry point for the provider.

When Terraform launches the provider binary, main.go initializes the provider and sets up the communication layer between Terraform and the plugin.

It also defines the provider address used by Terraform to identify the provider. This address becomes relevant when publishing to the Terraform Registry. Until then, it functions as an internal identifier and does not need to correspond to a real or published provider.

client.go

The client.go file is responsible for defining how the provider communicates with the external API.

It typically includes:

  • Configuration such as the base URL and authentication details
  • An HTTP client
  • Helper methods for making API requests

This layer abstracts away the HTTP logic from the rest of the provider.

provider.go

The provider.go file connects user configuration to the API client.

It reads configuration values (such as API credentials), initializes the client, and makes it available to all resources managed by the provider.

This file effectively acts as the bridge between Terraform configuration and the underlying API client.

.go

Each resource file defines how Terraform manages a specific object exposed by the API.

A resource implementation consists of several key components:

The model struct is a blueprint of what Terraform stores in its state file — every attribute the user can configure maps to a field here.

type exampleServiceModel struct {
    ID   types.String `tfsdk:"id"`
    Name types.String `tfsdk:"name"`
    ...
}

The schema describing what the users need to specify in their .tf files to initialize the resource correctly. It declares every attribute — whether it's required, optional, or computed by the server — and documents what each one does.

func (r *exampleServiceResource) Schema(...) {
  // defines all attributes
}

The CRUD methods, four functions that implement the lifecycle of a resource:

  • Create — invoked when a resource is created
  • Read — refreshes the Terraform state with data from the API
  • Update — applies configuration changes
  • Delete — removes the resource

Each one reads from the plan/state, calls the API via the client, and writes the result back to state.

The helper methods are used to translate between Terraform and the API:

  • toAPIRequest converts Terraform state into an API request
  • applyAPIResponse maps the API response back into Terraform state

This separation keeps the CRUD logic focused and easier to maintain.

Testing the Provider

As I do not have a strong background in Go, and relied heavily on AI to generate both the implementation and tests, I focused primarily on validating the provider through manual, end-to-end testing.

The goal was simple: use the provider locally and verify that Terraform could successfully create, read, update, and delete resources via the API.

Terraform provides a built-in mechanism for local provider development through the dev_overrides configuration. When dev_overrides is configured, Terraform skips downloading the provider from the registry and instead uses a locally compiled binary. During terraform init, you will see a warning such as:

This is expected and indicates that Terraform is using your local provider. This approach made it possible to iteratively test the provider and validate its behavior without publishing it, which significantly speeds up development.

Testing process

First, compile the provider using Go: go build. This produces a binary that Terraform can execute.

Next, configure the Terraform CLI to point to your local build. This is done in the CLI configuration file (~/.terraformrc or terraform.rc):

provider_installation {
  dev_overrides {
    "registry.terraform-registry/example/example_provider" = "/path/to/your/provider/binary"
  }
  direct {}
}

This tells Terraform: when this provider is requested, use the local binary instead of downloading it.

The Terraform configuration itself does not need to change. You can reference the provider as if it were published:

terraform {
  required_providers {
    example = {
      source = "registry.terraform-registry/example/example_provider"
    }
  }
}

provider "example" {
  host  = "https://your-api-example.com"
  token = "your-token"
}

From Terraform’s perspective, nothing is different—it simply resolves the provider locally instead of from the registry.

Notes and gotchas

  • When testing locally, it is recommended to use a local backend to avoid interfering with any remote state.
  • If you place the compiled binary in a plugins directory, Terraform may continue to use it even after you intend to switch back to a registry version. In that case, remove the local binary to ensure Terraform downloads the correct version.
  • Expect some trial and error—especially when the implementation is generated—since debugging provider behavior can be less straightforward than typical application code.

Releasing

Once the provider is working locally, the next step is packaging and releasing it so it can be used more broadly. Terraform providers are typically released using GoReleaser, a tool that automates building binaries, packaging them, and publishing releases.

I used Claude to generate the GitHub Actions workflow for this. One thing worth being explicit about in the prompt is GPG signing — the Terraform Registry requires the SHA256SUMS file to be signed, and it is easy to end up with a workflow that looks complete but skips this step. The prompt I used was:

Create a GitHub Actions workflow that releases the provider, triggered on version tag pushes. The release should sign the SHA256SUMS file with a GPG key stored in GitHub secrets.

The release process is driven by a configuration file: .goreleaser.yml. This file contains configuration such as which distributions the files should be generated for, or anything you want to execute before the release (such as linting). You can find an example in this example configuration. Claude generated this file for me on its own.

Once this file is defined, releasing a new version can be as simple as running: goreleaser release.

This command:

  1. Builds the provider for multiple platforms
  2. Packages the binaries
  3. Generates checksums
  4. Publishes a release (e.g. to GitHub)

What gets published?

A Terraform provider release is not just a single binary. It consists of several components that Terraform uses to safely install and verify the provider. After running GoReleaser, your release artifacts (for example in a GitHub release) will typically look something like this:

terraform-provider-example_0.0.1_SHA256SUMS
terraform-provider-example_0.0.1_SHA256SUMS.sig

terraform-provider-example_0.0.1_darwin_amd64.zip
terraform-provider-example_0.0.1_darwin_arm64.zip
terraform-provider-example_0.0.1_linux_amd64.zip
terraform-provider-example_0.0.1_linux_arm64.zip
terraform-provider-example_0.0.1_windows_amd64.zip

Terraform downloads the correct archive based on the user’s operating system and architecture.

  • The ZIP files contain the actual provider binaries
  • The SHA256SUMS file contains checksums for all artifacts
  • The signature file (.sig) is used to verify the checksums

These files are what Terraform uses under the hood when installing a provider from the registry.

Publishing to the Terraform Registry

To publish a provider to the Terraform Registry, the release artifacts must follow a specific structure and naming convention.

In addition, the provider source address (defined earlier in main.go and Terraform configuration) must match the repository and namespace under which the provider is published.

Using scaffolding as a reference

If you are unsure about the correct structure or release setup, HashiCorp provides a scaffolding framework that includes working examples for both provider implementation and release configuration: terraform-provider-scaffolding-framework

This can be a useful reference when setting up your own provider or troubleshooting release issues.

Conclusion

Building a Terraform provider turned out to be much more approachable than expected. Once you understand the structure—provider, client, and resources—it really comes down to mapping Terraform’s lifecycle to API calls.

Using Claude to generate most of the implementation made it possible to get started quickly, even without prior Go experience. While this approach comes with trade-offs (especially around code quality and deeper understanding), it is a surprisingly effective way to explore how providers work in practice.

This was not a production-ready implementation, but it was enough to understand the moving parts and validate the concept end-to-end.

If you are working with an API that is not yet supported by Terraform, or you want to integrate internal systems into your infrastructure workflows, building a custom provider is a viable option—and a lot less intimidating than it initially seems.

Written by

Marta Radziszewska

Data Engineer

Contact

Let’s discuss how we can support your journey.