Subchapter 12.1
references/design-principles.mdMarkdown5 KBView on GitHub
Distilled from HashiCorp’s Provider Design Principles and the conventions of large production providers (notably terraform-provider-aws). Apply these writing code — most painful provider mistakes are modeling mistakes.
A provider abstracts one platform’s API/SDK into Terraform’s lifecycle. Keep out of a provider:
Heuristic: if the API offers create/read/(update)/delete for a thing, that thing is a resource. Prefer many small resources over one large one — practitioners compose small blocks far more easily than they fight a mega-resource with intertwined attribute behaviors.
One resource, one API. A resource should call a single service’s API. Cross-service resources look convenient but:
If two services must cooperate, model each side as its own resource and let configuration connect them.
| API concept | Model as |
|---|---|
| Attached policy / rule document | Separate resource referencing the parent, not a blob attribute on the parent |
| One-to-many attachment (e.g. member of group) | Separate “attachment/membership” resource |
| Start/stop/enable/disable running state | An attribute in the resource — a separate “power state” resource fights the parent’s lifecycle |
| Long-running task / job / operation the API exposes | Separate resource representing the task; Create starts it, Read polls it |
| Invitation / handshake / approval flows | Resource on the accepting side: Create = accept, Read = status, Delete = reject/leave |
| Versioned artifact (function version, template version) | Usually a separate <thing>_version resource so versions can pin and iterate independently |
When a single API object has two defensible modelings (one resource with nested attributes vs. parent + child resources), prefer the one whose update semantics match the API: if children can be added/removed independently server-side, separate resources avoid the classic “whole-list replacement” diff problem — but never ship both patterns for the same underlying collection without conflict warnings in both.
Data sources are read-only views. They must not create, modify, or delete
anything, ever — a data source with side effects breaks terraform plan‘s
promise of being safe to run.
Singular data sources (examplecloud_widget) fetch exactly one object:
Returning an arbitrary element instead of erroring hides real environmental problems and produces non-deterministic plans.
Plural data sources (examplecloud_widgets) fetch a collection:
Ship both for most resources. Users need the singular form for point lookups (“give me this widget by name”) and the plural form for enumeration (“give me every widget matching these filters”) — different configurations need different shapes of the same data, and adding the missing one later is a common feature request. Skip one only when the API genuinely cannot support it.
<provider>_<service?>_<noun>, all lowercase snake_case,
noun derived from the API’s own CRUD operation names (CreateWidget →
_widget) so users can map docs ↔ API.NewWidgetResource, correct initialisms in
MixedCaps (VPCEndpoint, not VpcEndpoint).provider-actions skill, if
available).