Skip to content
Guilherme Nogueira
Go back

Building my first Terraform provider, from scratch to the registry

3 min read

I use Terraform providers every day. At some point I realized I had no real idea how they work under the hood. So I built one.

There were already Slack providers out there. That was not the point. The point was to go through the whole lifecycle myself, from an empty Go module to a signed release that anyone can pull with terraform init. Slack was just a friendly API to practice on.

From code to terraform init: write, test, release, registry, use

Table of contents

Open Table of contents

What the provider does

It manages Slack as code. The main resource is slack_channel, which creates and updates public or private channels, manages members, and sets topic and purpose. On top of that there are data sources to read existing channels, users and user groups.

Configuration is a bot token, passed by environment variable or in the provider block.

terraform {
  required_providers {
    slack = {
      source  = "gfnogueira/slack"
      version = "0.1.5"
    }
  }
}

provider "slack" {
  token = var.slack_token
}

resource "slack_channel" "incidents" {
  name       = "incidents"
  is_private = false
  topic      = "Where production goes to confess"
}
Get the provider

On the Terraform Registry: gfnogueira/slack

That is the whole point of a provider. Something that lives outside Terraform becomes a resource you can plan, apply and diff like any other.

What a provider actually is

Built in Go, a provider is a typed contract between Terraform and an API. Two pieces carry most of the weight.

The first is the schema. It describes what a resource looks like: its fields, their types, which are required, which are computed. The schema is the source of truth. Get it right and the plan and apply experience follows. Get it wrong and every apply fights you.

The second is CRUD. For each resource you implement create, read, update and delete. Each one maps Terraform’s desired state to API calls and back.

The function that surprised me was Read. It is where state drift lives. Read has to fetch the current reality from the API and map it back into Terraform state, field by field. If your Read is sloppy, Terraform either sees phantom changes on every plan or misses real ones. Most of the careful work in a provider is there, not in Create.

Schema first

Before writing any CRUD, get the schema right. Almost every weird plan diff I hit traced back to a field typed or marked wrong in the schema.

Shipping it to the registry

Writing the code was half the job. The other half was publishing.

The Terraform Registry does not host binaries you upload. It watches your GitHub releases. So the flow is: tag a release, let CI build the binaries for each platform, sign them with a GPG key, and publish the release. The registry picks it up and the provider becomes installable by source = "gfnogueira/slack".

The signing part is the gate. The registry will not serve a provider whose releases are not signed by a key you registered. Once that pipeline exists, every new version is just another tagged release.

Was it worth it

For a tool I will probably never run in production, absolutely. The next time a provider behaves strangely, I am not guessing anymore. I know there is a schema and a Read function on the other side, and I know which one to suspect.

That is the real return on building one from scratch. Not the provider. The understanding.


Share this post:

Previous Post
A Terraform provider for AWS WorkMail, because ClickOps was not an option
Next Post
It Is Always DNS. This Time It Was Hiding in the Defaults.