Agent Skills: generate-terraform-provider

>-

UncategorizedID: speakeasy-api/agent-skills/generate-terraform-provider

Install this agent skill to your local

pnpm dlx add-skill https://github.com/speakeasy-api/skills/tree/HEAD/skills/generate-terraform-provider

Skill Files

Browse the full folder contents for generate-terraform-provider.

Download Skill

Loading file tree…

skills/generate-terraform-provider/SKILL.md

Skill Metadata

Name
generate-terraform-provider
Description
>-

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 x-speakeasy-entity and x-speakeasy-entity-operation
  • Mapping API operations to Terraform CRUD methods
  • Understanding Terraform type inference from OpenAPI schemas
  • Configuring workflow.yaml 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., 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

  1. Speakeasy CLI installed and authenticated
  2. OpenAPI 3.0 or 3.1 specification with entity annotations
  3. Go installed (Terraform providers are written in Go)
  4. Authentication: Set SPEAKEASY_API_KEY env var or run speakeasy auth login
export SPEAKEASY_API_KEY="<your-api-key>"

Run speakeasy auth login to authenticate interactively, or set the SPEAKEASY_API_KEY environment variable.

Command

First-time generation (quickstart)

speakeasy quickstart --skip-interactive --output console \
  -s <spec-path> \
  -t terraform \
  -n <ProviderName> \
  -p <package-name>

Regenerate after changes

speakeasy run --output console

Regenerate a specific target

speakeasy run -t <target-name> --output console

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:

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:

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

# 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:

resource "petstore_pet" "my_pet" {
  name  = "Buddy"
  price = 1500
}

Workflow Configuration

Local spec

# .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:

# .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:

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:

terraform import petstore_pet.my_pet my_pet_id

Composite keys

For resources with multiple ID fields, pass a JSON-encoded object:

terraform import my_test_resource.my_example \
  '{ "primary_key_one": "9cedad30-...", "primary_key_two": "e20c40a0-..." }'

Or use an import block:

import {
  id = jsonencode({
    primary_key_one: "9cedad30-..."
    primary_key_two: "e20c40a0-..."
  })
  to = my_test_resource.my_example
}

Then generate configuration:

terraform plan -generate-config-out=generated.tf

Publishing to the Terraform Registry

Prerequisites

  1. Public repository named terraform-provider-{name} (lowercase)
  2. GPG signing key for release signing
  3. GoReleaser configuration
  4. Registration at registry.terraform.io

Step 1: Generate GPG Key

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 content
  • terraform_gpg_passphrase - Key passphrase

Step 3: Add Release Workflow

# .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

  1. Go to registry.terraform.io
  2. Sign in with GitHub (org admin required)
  3. 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}-beta with 0.x 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 gen.yaml using 0.x versioning, and publish it alongside the stable provider.

Testing the Provider

Add Test Dependency

In .speakeasy/gen.yaml:

terraform:
  additionalDependencies:
    github.com/hashicorp/terraform-plugin-testing: v1.13.3

Acceptance Test Structure

// 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

# 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 #list as an operation type -- only create, read, update, delete 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., id)
  • Do NOT skip x-speakeasy-entity 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 #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 setup
  • manage-openapi-overlays - Add entity annotations via overlay
  • diagnose-generation-failure - Troubleshoot generation errors