generate-terraform-provider
π―Skillfrom speakeasy-api/agent-skills
Generates a fully-annotated Terraform provider from an OpenAPI specification, handling entity mapping, CRUD operations, and Terraform Registry publishing.
Part of
speakeasy-api/agent-skills(25 items)
Installation
npx skills add speakeasy-api/skillsSkill Details
>-
Overview
# 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](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
x-speakeasy-entityandx-speakeasy-entity-operation - Mapping API operations to Terraform CRUD methods
- Understanding Terraform type inference from OpenAPI schemas
- Configuring
workflow.yamlfor 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., Petstore) |
| Package name | Yes | Lowercase package identifier (e.g., petstore) |
| Entity annotations | Yes | x-speakeasy-entity on schemas, x-speakeasy-entity-operation on operations |
Outputs
| Output | Location |
|--------|----------|
| Workflow config | .speakeasy/workflow.yaml |
| Generation config | gen.yaml |
| Generated Go provider | Output directory (default: current dir) |
| Terraform examples | 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
SPEAKEASY_API_KEYenv var or runspeakeasy auth login
```bash
export SPEAKEASY_API_KEY="
```
Run speakeasy auth login to authenticate interactively, or set the SPEAKEASY_API_KEY environment variable.
Command
First-time generation (quickstart)
```bash
speakeasy quickstart --skip-interactive --output console \
-s
-t terraform \
-n
-p
```
Regenerate after changes
```bash
speakeasy run --output console
```
Regenerate a specific target
```bash
speakeasy run -t
```
Entity Annotations
Before generating, annotate your OpenAPI spec with two extensions:
1. Mark schemas as entities
Add x-speakeasy-entity 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 |
|-------------|------|------------|---------|
| POST | /resource | Entity#create | Create a new resource |
| GET | /resource/{id} | Entity#read | Read a single resource |
| PUT | /resource/{id} | Entity#update | Update a resource |
| DELETE | /resource/{id} | Entity#delete | Delete a resource |
Data sources (list): For list endpoints (GET /resources), use a separate plural entity name with #read (e.g., Pets#read). Do NOT use #list -- 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 required in CREATE request body | Required: true |
| Optional | Property is not required in CREATE request body | Optional: true |
| Computed | Property appears in response but not in CREATE request | Computed: true |
| ForceNew | Property exists in CREATE request but not in UPDATE request | ForceNew (forces resource recreation) |
| Enum validation | Property defined as enum | Validator 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 terraform-provider-XXX, where XXX is the provider type name. The provider type name should be lowercase alphanumeric ([a-z][a-z0-9]), 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 |
|-------------|-------------------|
| Pet | petstore_pet |
| GatewayControlPlane | konnect_gateway_control_plane |
| MeshControlPlane | konnect_mesh_control_plane |
For list data sources, use the plural PascalCase form (e.g., Pets).
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](https://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:
terraform_gpg_secret_key- Private key contentterraform_gpg_passphrase- 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](https://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 (x.y.z) - Beta:
terraform-provider-{name}-betawith0.xversioning
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 gen.yaml using 0.x versioning, and publish it alongside the stable provider.
Testing the Provider
Add Test Dependency
In .speakeasy/gen.yaml:
```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 TF_ACC=1, tests silently skip with PASS status.
What NOT to Do
- Do NOT use
#listas an operation type -- onlycreate,read,update,deleteare 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.,
id) - Do NOT skip
x-speakeasy-entityon 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 #list instead of #read | Change to Entity#read; 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 |
| ForceNew 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 go mod tidy 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 x-speakeasy-entity annotation | Add x-speakeasy-entity: EntityName to the component schema |
| Auth not working | Missing API key | Set SPEAKEASY_API_KEY env var or run speakeasy auth login |
Related Skills
start-new-sdk-project- Initial project setupmanage-openapi-overlays- Add entity annotations via overlaydiagnose-generation-failure- Troubleshoot generation errors
More from this repository10
validate-openapi-spec skill from speakeasy-api/agent-skills
start-new-sdk-project skill from speakeasy-api/agent-skills
improve-operation-ids skill from speakeasy-api/agent-skills
check-workspace-status skill from speakeasy-api/agent-skills
apply-openapi-overlay skill from speakeasy-api/agent-skills
get-ai-suggestions skill from speakeasy-api/agent-skills
fix-validation-errors-with-overlays skill from speakeasy-api/agent-skills
merge-openapi-specs skill from speakeasy-api/agent-skills
create-openapi-overlay skill from speakeasy-api/agent-skills
configure-authentication skill from speakeasy-api/agent-skills