How to Build and Publish Your Own Terraform Provider
Creating your own Terraform provider is like opening a gateway into the Terraform ecosystem — you’re not just using infrastructure as code, you’re extending it. With a provider, you can define custom resources and data sources that interact with APIs, platforms, or even your own tools. Here’s a complete guide on how to build one from scratch and publish it to the Terraform Registry.
Understanding What a Provider Is
A Terraform provider is a Go-based plugin that tells Terraform how to manage external systems. Think of it as a translator — it takes your Terraform configuration and converts it into API calls to create, read, update, and delete resources.
For example:
resource "mycloud_instance" {
name = "server-1"
}
Behind the scenes, that simple block might call your cloud provider’s API to spin up a real virtual machine.
Step 1: Setting Up Your Environment
You’ll need the following tools:
- Go 1.22+
- Terraform CLI
- GitHub account (for publishing)
- Go modules enabled
On Linux or macOS:
sudo apt install golang terraform git
Then initialize your Go module:
mkdir terraform-provider-myprovider
cd terraform-provider-myprovider
go mod init github.com/myorg/terraform-provider-myprovider
Your project structure will look like this:
.
├── main.go
├── provider.go
├── resource_example.go
├── go.mod
└── go.sum
Step 2: Writing the Provider Code
Here’s the heart of your provider: the code that defines what Terraform can do with it.
main.go
package main
import (
"context"
"flag"
"log"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/myorg/terraform-provider-myprovider/myprovider"
)
var version = "0.1.0"
func main() {
flag.Parse()
err := providerserver.Serve(context.Background(), myprovider.New, providerserver.ServeOpts{
Address: "registry.terraform.io/myorg/myprovider",
})
if err != nil {
log.Fatal(err.Error())
}
}
provider.go
package myprovider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/types"
)
type MyProvider struct{}
func (p *MyProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "myprovider"
}
func (p *MyProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = map[string]provider.Schema{
"api_key": {Type: types.StringType, Optional: true},
}
}
func (p *MyProvider) Resources(_ context.Context) []func() provider.Resource {
return []func() provider.Resource{
NewExampleResource,
}
}
func (p *MyProvider) DataSources(_ context.Context) []func() provider.DataSource {
return nil
}
func New() provider.Provider {
return &MyProvider{}
}
Step 3: Adding a Custom Resource
Let’s add a simple resource to demonstrate functionality.
resource_example.go
package myprovider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
)
type ExampleResource struct{}
func NewExampleResource() resource.Resource {
return &ExampleResource{}
}
type ExampleResourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
}
func (r *ExampleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_example"
}
func (r *ExampleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = map[string]resource.Schema{
"name": {Type: types.StringType, Required: true},
}
}
func (r *ExampleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data ExampleResourceModel
req.Plan.Get(ctx, &data)
data.ID = types.StringValue("example-id")
resp.State.Set(ctx, &data)
}
This simple resource creates an “example” object with an ID and a name — just enough to show Terraform that it works.
Step 4: Build and Test Locally
Compile the provider:
go build -o terraform-provider-myprovider
Create a local Terraform configuration:
terraform {
required_providers {
myprovider = {
source = "myorg/myprovider"
version = "0.1.0"
}
}
}
provider "myprovider" {
api_key = "12345"
}
resource "myprovider_example" "test" {
name = "hello"
}
To make Terraform recognize your local provider:
mkdir -p ~/.terraform.d/plugins/registry.terraform.io/myorg/myprovider/0.1.0/linux_amd64
mv terraform-provider-myprovider ~/.terraform.d/plugins/registry.terraform.io/myorg/myprovider/0.1.0/linux_amd64/
Now run:
terraform init
terraform apply
You should see Terraform successfully create your example resource.
Step 5: Publishing to the Terraform Registry
Push your code to GitHub Repository name must follow:
terraform-provider-myproviderCreate a semantic version tag:
bashgit tag v0.1.0 git push origin v0.1.0Go to the Terraform Registry
- Log in with GitHub
- Choose “Publish Provider”
- It will automatically detect your repo and tagged release
Step 6: Automate Releases with GitHub Actions
HashiCorp expects signed binaries for multiple platforms (Linux, macOS, Windows). Use the official scaffolding framework as your base. It includes GitHub Actions that build, test, and publish your provider automatically.
Step 7: Use Your Provider in Terraform
Once published, anyone can use your provider just like official ones:
terraform {
required_providers {
myprovider = {
source = "myorg/myprovider"
version = "~> 0.1"
}
}
}
Run terraform init, and Terraform will download it directly from the Registry.
The Bigger Picture
Writing your own Terraform provider is not just about custom infrastructure automation — it’s about interoperability. You’re effectively extending Terraform’s language so it can talk to anything with an API. That means your internal services, your IoT devices, or your own cloud tools can now live inside Terraform’s declarative universe.
Building a provider is a beautiful way to blur the line between software and infrastructure. You’re not just deploying systems — you’re teaching Terraform to understand new worlds.