Chapter 13: Advanced Patterns
Topic 74

Custom Providers

ExtendingTooling

Terraform's reach ends where its providers do. There are hundreds of them on the Registry, but sometimes the thing you need to manage declaratively — an internal platform API, a niche SaaS, a homegrown service — has none. Writing a provider with the Terraform Plugin Framework brings that system into the same plan/apply model as an EC2 instance: a real resource type, with state, diffs, and lifecycle.

This is an overview, not a build tutorial. A provider is Go software you write, test, version, and maintain forever — so the first question is never "how do I build one" but "do I actually need one, or will something far lighter do the job?" Most of the time it will.

Two ways to reach an unsupported API
Full custom provider
A first-class resource with proper plan/apply, state, and lifecycle — but it is real Go software you write, test, version, and maintain forever.
Lighter options
restapi, the external data source, or terraform_data plus a script — bridge a simple API in minutes with no Go, at the cost of clean diffing and lifecycle.

When a Custom Provider Is Justified

The case for a real provider is a system you want to manage declaratively, with proper state and a precise plan/apply diff, that no existing provider covers. An internal account-provisioning service, a custom DNS or secrets platform, an in-house feature-flag system — anything your team treats as infrastructure and wants in the same workflow as the rest of the stack. The payoff is a first-class resource: plan shows exactly what will change, apply reconciles, and Terraform remembers what it created.

The bar is high because the alternative — a script, a bridge provider, a manual step — is usually cheaper. Build a provider when the system is central enough that first-class treatment pays back the maintenance, and when you have an owner who will keep it current with the target API.

The Plugin Framework

The Terraform Plugin Framework is the modern Go SDK for writing providers. It is what you reach for today; the older SDKv2 still exists and powers many shipped providers, but it is legacy for new work. The Framework gives you typed schemas, cleaner handling of null and unknown values, and a first-class plan-modification hook — the things that historically made SDKv2 providers produce confusing diffs.

A provider built on the Framework defines a schema for each resource and implements the four lifecycle operations: create, read, update, and delete. Terraform calls read to refresh, diffs the result against your config, and calls create, update, or delete to make reality match — the exact same loop the AWS provider runs, only against your API instead of AWS.

a resource's Create method (Plugin Framework, abbreviated)
func (r *widgetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
  // read the planned values out of the config
  var plan widgetModel
  resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)

  // call your platform's API to create the object
  created, err := r.client.CreateWidget(plan.Name.ValueString())
  if err != nil {
    resp.Diagnostics.AddError("create failed", err.Error())
    return
  }

  // write the real ID back into state so read/update/delete can find it
  plan.ID = types.StringValue(created.ID)
  resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
}

The shape is always this: pull the planned values, call the API, and write the returned identifiers into state. Implement read to fetch the current object so Terraform can detect drift, update to apply changes in place, and delete to remove it. Get the schema and these four methods right and your resource behaves like any other.

What Building One Involves

Beyond the four methods, the work is in the schema and the state mapping. Every attribute needs a type, a description, and the right flags — required, optional, computed, sensitive. You decide which changes can happen in place and which force replacement, and you map your API's representation onto Terraform's so a quiet field difference doesn't surface as a permanent diff. This is where most provider quality lives: a sloppy schema produces a provider that "wants to change something" on every plan.

You also own auth, acceptance tests that run real create/read/update/delete cycles, documentation, and a release pipeline that publishes versioned binaries — privately or to the Registry. It is a small software project, not a config file.

Lighter Alternatives

Before any of that, exhaust the options that need no Go at all. The community restapi provider drives a generic REST API as Terraform resources. The built-in external data source shells out to a script that returns JSON. The http provider reads an endpoint, and terraform_data plus a script can carry a side effect through the lifecycle. None of these give clean diffing or proper delete handling, but for a simple or one-off integration they are minutes of work against weeks for a real provider.

The rule: bridge a simple API with restapi or external first, and only graduate to a custom provider when you keep hitting the limits — perpetual diffs, missing lifecycle, a model that deserves to be first-class.

The Maintenance Reality

The cost of a provider is not building it — it is owning it. The target API changes, the Plugin Framework releases new versions, Terraform itself moves, and someone has to keep all of it working. An unmaintained internal provider rots into a liability that blocks every team depending on it. Treat the decision to write one as a decision to staff its upkeep indefinitely, the same way you would any internal library that other teams build on.

Custom provider vs lighter alternatives

Custom provider — a first-class resource with proper plan/apply, state, and lifecycle, at the cost of building and maintaining real Go software. Choose it when the system deserves first-class modeling and you have an owner to keep it current with the target API.

restapi / external / scripts — bridge a simple API in minutes with no Go, but with no clean diffing and weak lifecycle handling. Choose them for one-off or simple integrations, and only graduate to a provider when you keep hitting their limits.

Common Mistakes
  • Building a full custom provider for a one-off integration that the community restapi provider or an external data source would have handled in an afternoon.
  • Underestimating the maintenance burden — treating the provider as a one-time script when it is ongoing software that must track the target API, the Framework, and Terraform itself.
  • Starting a brand-new provider on the deprecated SDKv2 instead of the current Plugin Framework, inheriting its weaker null and unknown handling from day one.
  • Modeling resource schemas carelessly — wrong required/computed flags or unmapped API fields — so the provider produces a perpetual diff on every plan.
  • Shipping a provider with no acceptance tests, so an API change silently breaks create or delete and nobody finds out until a production apply fails.
Best Practices
  • Exhaust the lighter options — community providers, restapi, external, terraform_data — before committing to a custom provider.
  • Use the Plugin Framework for any new provider; reserve SDKv2 for maintaining the providers already built on it.
  • Model schemas carefully — correct required/optional/computed flags and full state mapping — so plans are clean and replacement is intentional.
  • Write acceptance tests that exercise real create/read/update/delete cycles, and run them in CI before every release.
  • Commit to the upkeep before you start: assign an owner, version the provider, and keep it current with the target API and the Framework.
Comparable tools Pulumi providers, frequently bridged from Terraform providers Crossplane providers in the Kubernetes resource model CloudFormation custom resources and registry types

Knowledge Check

When is writing a full custom provider justified over a lighter bridge?

  • When you need a first-class resource with proper plan/apply, state, and lifecycle for a system no provider covers, and you'll maintain it
  • Whenever a target API isn't published on the public Terraform Registry, regardless of how rarely you actually need to call it from a config
  • Only when the target system itself is written in Go, since providers are Go programs and must share the runtime
  • Any time you'd otherwise have to call a REST API from Terraform

Which SDK should a brand-new provider be built on?

  • The Plugin Framework — the modern SDK; SDKv2 is legacy and kept for existing providers
  • SDKv2, because it has the widest adoption, the most examples online, and the largest base of existing providers to copy
  • Either one — they are fully interchangeable and produce byte-identical compiled providers
  • The HCL parser library wired up directly, without using any provider SDK at all

What is the real ongoing cost of a custom provider?

  • It is software you own — versioned, tested, and kept current with the target API, the Framework, and Terraform indefinitely
  • A recurring per-resource fee that the Terraform Registry charges every month to host, index, and serve your published provider binary
  • Re-running terraform init on every developer machine and CI runner each time the provider version changes
  • Nothing — once written, a provider is static and needs no maintenance

Which lighter alternative bridges a simple REST API without writing any Go?

  • The community restapi provider or the external data source that shells out to a script
  • A second backend block pointed at the API's base URL so Terraform reads and writes through it
  • A moved block referencing the API endpoint so each request maps to a managed resource
  • Setting use_lockfile on the API resource so Terraform serializes the calls

You got correct