generate-terraform-provider
Generate a Terraform provider from an OpenAPI specification using the Speakeasy CLI. This skill covers the full lifecycle: annotating your spec with entity metadata, mapping CRUD operations, generating the provider, configuring workflows, and publishing to the Terraform Registry.
Content Guides
| Topic | Guide |
|---|
| Advanced Customization | content/customization.md |
The customization guide covers entity mapping placement, multi-operation resources, async polling, property customization, plan modification, validation, and state upgraders.
When to Use
- Generating a new Terraform provider from an OpenAPI spec
- Annotating an OpenAPI spec with and
x-speakeasy-entity-operation
- Mapping API operations to Terraform CRUD methods
- Understanding Terraform type inference from OpenAPI schemas
- Configuring for Terraform provider generation
- Publishing a provider to the Terraform Registry
- User says: "terraform provider", "generate terraform", "create terraform provider", "CRUD mapping", "x-speakeasy-entity", "terraform resource", "terraform registry"
Inputs
| Input | Required | Description |
|---|
| OpenAPI spec | Yes | OpenAPI 3.0 or 3.1 specification (local file, URL, or registry source) |
| Provider name | Yes | PascalCase name for the provider (e.g., ) |
| Package name | Yes | Lowercase package identifier (e.g., ) |
| Entity annotations | Yes | on schemas, x-speakeasy-entity-operation
on operations |
Outputs
| Output | Location |
|---|
| Workflow config | |
| Generation config | |
| Generated Go provider | Output directory (default: current dir) |
| Terraform examples | directory |
Prerequisites
- Speakeasy CLI installed and authenticated
- OpenAPI 3.0 or 3.1 specification with entity annotations
- Go installed (Terraform providers are written in Go)
- Authentication: Set env var or run
bash
export SPEAKEASY_API_KEY="<your-api-key>"
Run
to authenticate interactively, or set the
environment variable.
Command
First-time generation (quickstart)
bash
speakeasy quickstart --skip-interactive --output console \
-s <spec-path> \
-t terraform \
-n <ProviderName> \
-p <package-name>
Regenerate after changes
bash
speakeasy run --output console
Regenerate a specific target
bash
speakeasy run -t <target-name> --output console
Entity Annotations
Before generating, annotate your OpenAPI spec with two extensions:
1. Mark schemas as entities
Add
to component schemas that should become Terraform resources:
yaml
components:
schemas:
Pet:
x-speakeasy-entity: Pet
type: object
properties:
id:
type: string
readOnly: true
name:
type: string
price:
type: number
required:
- name
- price
2. Map operations to CRUD methods
Add
x-speakeasy-entity-operation
to each API operation:
yaml
paths:
/pets:
post:
x-speakeasy-entity-operation: Pet#create
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
/pets/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
get:
x-speakeasy-entity-operation: Pet#read
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
put:
x-speakeasy-entity-operation: Pet#update
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
delete:
x-speakeasy-entity-operation: Pet#delete
responses:
"204":
description: Deleted
CRUD Mapping Summary
| HTTP Method | Path | Annotation | Purpose |
|---|
| | | Create a new resource |
| | | Read a single resource |
| | | Update a resource |
| | | Delete a resource |
Data sources (list): For list endpoints (
), use a separate plural entity name with
(e.g.,
). Do NOT use
-- it is not a valid operation type.
Terraform Type Inference
Speakeasy infers Terraform schema types from the OpenAPI spec automatically:
| Rule | Condition | Terraform Attribute |
|---|
| Required | Property is in CREATE request body | |
| Optional | Property is not in CREATE request body | |
| Computed | Property appears in response but not in CREATE request | |
| ForceNew | Property exists in CREATE request but not in UPDATE request | (forces resource recreation) |
| Enum validation | Property defined as enum | added for runtime checks |
Every parameter needed for READ, UPDATE, or DELETE must either appear in the CREATE response or be required in the CREATE request.
Example
Full workflow: Petstore provider
bash
# 1. Ensure your spec has entity annotations (see above)
# 2. Generate the provider
speakeasy quickstart --skip-interactive --output console \
-s ./openapi.yaml \
-t terraform \
-n Petstore \
-p petstore
# 3. Build and test
cd terraform-provider-petstore
go build ./...
go test ./...
# 4. After spec changes, regenerate
speakeasy run --output console
This produces a Terraform resource usable as:
hcl
resource "petstore_pet" "my_pet" {
name = "Buddy"
price = 1500
}
Workflow Configuration
Local spec
yaml
# .speakeasy/workflow.yaml
workflowVersion: 1.0.0
speakeasyVersion: latest
sources:
my-api:
inputs:
- location: ./openapi.yaml
targets:
my-provider:
target: terraform
source: my-api
Remote spec with overlays
For providers built against third-party APIs, fetch the spec remotely and apply local overlays:
yaml
# .speakeasy/workflow.yaml
workflowVersion: 1.0.0
speakeasyVersion: latest
sources:
vendor-api:
inputs:
- location: https://api.vendor.com/openapi.yaml
overlays:
- location: terraform_overlay.yaml
output: openapi.yaml
targets:
vendor-provider:
target: terraform
source: vendor-api
Use
speakeasy overlay compare
to track upstream API changes:
bash
speakeasy overlay compare \
--before https://api.vendor.com/openapi.yaml \
--after terraform_overlay.yaml \
--out overlay-diff.yaml
Repository and Naming Conventions
Repository naming
Name the repository
, where
is the provider type name. The provider type name should be lowercase alphanumeric (
), though hyphens and underscores are permitted.
Entity naming
Use PascalCase for entity names so they translate correctly to Terraform's underscore naming:
| Entity Name | Terraform Resource |
|---|
| |
| konnect_gateway_control_plane
|
| konnect_mesh_control_plane
|
For list data sources, use the plural PascalCase form (e.g.,
).
Resource Importing
Generated providers support importing existing resources into Terraform state.
Simple keys
For resources with a single ID field:
bash
terraform import petstore_pet.my_pet my_pet_id
Composite keys
For resources with multiple ID fields, pass a JSON-encoded object:
bash
terraform import my_test_resource.my_example \
'{ "primary_key_one": "9cedad30-...", "primary_key_two": "e20c40a0-..." }'
Or use an import block:
hcl
import {
id = jsonencode({
primary_key_one: "9cedad30-..."
primary_key_two: "e20c40a0-..."
})
to = my_test_resource.my_example
}
Then generate configuration:
bash
terraform plan -generate-config-out=generated.tf
Publishing to the Terraform Registry
Prerequisites
- Public repository named
terraform-provider-{name}
(lowercase)
- GPG signing key for release signing
- GoReleaser configuration
- Registration at registry.terraform.io
Step 1: Generate GPG Key
bash
gpg --full-generate-key # Choose RSA, 4096 bits
gpg --armor --export-secret-keys YOUR_KEY_ID > private.key
gpg --armor --export YOUR_KEY_ID > public.key
Step 2: Configure Repository Secrets
Add to GitHub repository secrets:
- - Private key content
- - Key passphrase
Step 3: Add Release Workflow
yaml
# .github/workflows/release.yaml
name: Release
on:
push:
tags: ['v*']
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v4
with:
go-version-file: 'go.mod'
- uses: crazy-max/ghaction-import-gpg@v5
id: import_gpg
with:
gpg_private_key: ${{ secrets.terraform_gpg_secret_key }}
passphrase: ${{ secrets.terraform_gpg_passphrase }}
- uses: goreleaser/goreleaser-action@v6
with:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
Step 4: Register with Terraform Registry
- Go to registry.terraform.io
- Sign in with GitHub (org admin required)
- Publish → Provider → Select your repository
After registration, releases auto-publish when tags are pushed.
Beta Provider Pattern
For large APIs, maintain separate stable and beta providers:
- Stable:
terraform-provider-{name}
with semver ()
- Beta:
terraform-provider-{name}-beta
with versioning
Users can install both simultaneously. When beta features mature, graduate them to the stable provider. To set up a beta provider, create a separate
terraform-provider-{name}-beta
repository with its own
using
versioning, and publish it alongside the stable provider.
Testing the Provider
Add Test Dependency
yaml
terraform:
additionalDependencies:
github.com/hashicorp/terraform-plugin-testing: v1.13.3
Acceptance Test Structure
go
// internal/provider/resource_test.go
func TestAccPet_Lifecycle(t *testing.T) {
t.Parallel()
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProviders(),
Steps: []resource.TestStep{
{
Config: testAccPetConfig("Buddy", 1500),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("petstore_pet.test", "name", "Buddy"),
),
},
{
ResourceName: "petstore_pet.test",
ImportState: true,
ImportStateVerify: true,
},
},
})
}
Running Tests
bash
# Unit tests
go test -v ./...
# Acceptance tests (REQUIRES TF_ACC=1)
TF_ACC=1 go test -v ./internal/provider/... -timeout 30m
Note: Without
, tests silently skip with PASS status.
What NOT to Do
- Do NOT use as an operation type -- only , , , are valid
- Do NOT modify generated Go code directly -- changes are overwritten on regeneration. Use overlays or hooks instead
- Do NOT omit the CREATE response body -- Terraform needs the response to populate computed fields (e.g., )
- Do NOT skip on schemas -- without it, Speakeasy cannot identify Terraform resources
- Do NOT use camelCase or snake_case for entity names -- use PascalCase so Terraform underscore naming works
- Do NOT generate Terraform providers in monorepo mode -- HashiCorp requires a dedicated repository
Troubleshooting
| Problem | Cause | Solution |
|---|
invalid entity operation type: list
| Used instead of | Change to ; list endpoints use a plural entity name |
| Resource missing fields after import | READ operation does not return all attributes | Ensure the GET endpoint returns the complete resource schema |
| on unexpected field | Field exists in CREATE but not UPDATE request | Add the field to the UPDATE request body if it should be mutable |
| Provider fails to compile | Missing Go dependencies | Run in the provider directory |
| Computed field not populated | Field absent from CREATE response | Ensure the CREATE response returns the full resource including computed fields |
| Entity not appearing as resource | Missing annotation | Add x-speakeasy-entity: EntityName
to the component schema |
| Auth not working | Missing API key | Set env var or run |
Related Skills
- - Initial project setup
- - Add entity annotations via overlay
diagnose-generation-failure
- Troubleshoot generation errors