--- title: Terraform | Stainless description: Generate production-ready Terraform providers from your OpenAPI specification --- Terraform is [experimental](https://www.stainless.com/changelog/terraform-provider-general-availability). Contact us at for assistance. The Stainless Terraform provider generator creates idiomatic Terraform providers from your OpenAPI specification. Generated providers include Hashicorp docs, handle complex datatypes, and support custom acceptance tests and state migrations. **Example repositories:** - [cloudflare/terraform-provider-cloudflare](https://github.com/cloudflare/terraform-provider-cloudflare) ## Considerations A Terraform provider enables users to manage your API resources declaratively through infrastructure-as-code. Users can define resource configurations in Terraform files, check them into source control, and apply changes with a consistent workflow. This is particularly common for infrastructure provisioning, monitoring configuration, billing rules, and security workflows. Terraform providers require more maintenance than our other SDKs and impose some additional constraints on your API. ### A CRUD-like API The Terraform provider works best for APIs where the endpoints on a given resource are uniform — the create, update, and read requests and responses all have the same shape and field names, and conceptually “set” all the properties in a straightforward way. For example, a CRUD-like API for a resource called `Product` in an e-commerce application might look like this: - `POST /products` - Create a product, and return the product (with ID) in the response - `GET /products` - List the products (potentially filtering with query params) - `PUT /products/{product_id}` - Update the product fields and return the product - `GET /products/{product_id}` - Return the product given the ID - `DELETE /products/{product_id}` - Delete the product given the ID In OpenAPI terms, it might look something like this: ``` paths: /products: get: responses: '200': $ref: '#/components/schemas/ProductListResponse' post: requestBody: $ref: '#/components/requestBodies/ProductRequest' responses: '200': $ref: '#/components/schemas/ProductResponse' /products/{product_id}: get: parameters: - $ref: '#/components/parameters/ProductID' responses: '200': $ref: '#/components/schemas/ProductResponse' put: parameters: - $ref: '#/components/parameters/ProductID' requestBody: $ref: '#/components/requestBodies/ProductRequest' responses: '200': $ref: '#/components/schemas/ProductResponse' delete: parameters: - $ref: '#/components/parameters/ProductID' components: parameters: ProductID: name: product_id in: path required: true schema: type: string requestBodies: ProductRequest: required: true content: application/json: schema: $ref: '#/components/schemas/Product' responses: ProductResponse: content: application/json: schema: $ref: '#/components/schemas/Product' ProductArrayResponse: content: application/json: schema: type: array items: $ref: '#/components/schemas/Product' schemas: Product: properties: product_id: type: string readonly: true name: type: string description: type: string image_url: type: string price: type: integer ``` An example resource for the above schema: ``` resource "ecommerce_product" "the_cube" { name = "The Cube" description = "A large stainless steel cube" image_url = "https://images.squarespace-cdn.com/content/v1/5488f21fe4b055c9cb360909/1427135193771-IM8B5KC0OUVQCRG7YCQD/2013-10+Steel+Cubes+-2.jpg?format=750w" price = 1000 } ``` Your API doesn’t have to be perfectly regular, but the more CRUD-like it is, the less configuration it needs. If needed, you can conform your API to have more CRUD-like behavior using the [Stainless config](/docs/reference/config#resource/index.md), OpenAPI spec, or custom code. ### Acceptance tests against real infrastructure Because not all server behavior is captured in the OpenAPI spec, we recommend testing your Terraform provider against real infrastructure to make sure that it behaves correctly. You can write [acceptance tests](https://developer.hashicorp.com/terraform/plugin/framework/acctests) using Hashicorp’s built-in testing framework. They execute against your provider and can run against your real infrastructure. You’ll want to have a suitable demo or dev environment such that you can create and delete resources safely. To ensure that data correctly round-trips to your server and back, we recommend writing at least one acceptance test per resource and data source. ### State migrations for breaking changes Unlike our other SDKs, Terraform providers have [a concept of “state”](https://developer.hashicorp.com/terraform/language/state). Every time a user creates or edits resources, the state of that resource is saved into a file with the extension `*.tfstate` (or saved to the cloud). This state is used for future runs to preserve mappings of IDs and version information and to determine which resources already exist. This means that if you make a breaking change to your API, you need to write a [state migration](https://developer.hashicorp.com/terraform/plugin/framework/resources/state-upgrade) to automatically upgrade the user’s state to the new version. A state migration is a Go function within the provider codebase that describes how to go from one schema version to the next for a given resource, much like a database migration for Terraform state. Breaking changes in Terraform are similar to breaking changes in your API or other SDKs. Some examples of breaking changes include: - Renaming a Terraform resource - Renaming an attribute of the resource - Removing an attribute See [Hashicorp’s documentation](https://developer.hashicorp.com/terraform/plugin/best-practices/versioning) for the full list and guidance on how to version properly. To write state migrations, implement deprecations, and do other customization, edit your Terraform repository using Stainless’s [custom code](/docs/guides/add-custom-code/index.md) feature — we’ll preserve the changes. ## Configuration To generate a Terraform provider, add the `terraform` target to your Stainless configuration file. The Terraform provider is built on top of the [Go SDK](/docs/targets/go/index.md), so you’ll need to enable both targets: ``` targets: go: package_name: github.com/my-company/my-sdk-go production_repo: my-org/my-sdk-go terraform: package_name: my-provider production_repo: my-org/terraform-provider-my-provider edition: terraform.2025-10-08 ``` The production repo for a Terraform provider must be in the form `terraform-provider-`. For a complete list of configuration options, see the [Terraform target reference](/docs/reference/config#terraform/index.md). ### Choosing resources You can control which Terraform resources and data sources are generated by editing the Stainless config. The `infer_all_services` option in the [`terraform` target](/docs/reference/config#terraform/index.md) enables generation of all Terraform resources in your project. ``` terraform: package_name: my-provider options: infer_all_services: true ``` This option is set to `true` for all new projects. If it’s `false` or absent, we don’t generate any Terraform resources or data sources by default. To enable or disable Terraform for a specific resource, you can annotate the [resource](/docs/reference/config#resource/index.md) with `terraform: true` or `terraform: false`. ``` resources: accounts: terraform: true methods: create: post /accounts edit: put /accounts/{account_id} get: get /accounts/{account_id} delete: delete /accounts/{account_id} ``` For more fine-grained control, you can specify Terraform configuration options for the resource: ``` resources: accounts: terraform: name: my_cool_resource resource: true # do generate Terraform resource data_source: false # don't generate Terraform data sources for this resource methods: create: post /accounts edit: put /accounts/{account_id} get: get /accounts/{account_id} delete: delete /accounts/{account_id} ``` See the [Stainless config reference](/docs/reference/config#resource/index.md) for the full list of supported options. ### Resource-specific configuration In addition to setting `terraform: true`, you can provide an object to further customize a resource. ``` resources: accounts: terraform: resource: true # whether to enable the resource data_source: true # whether to enable the data sources name: my_custom_name # a custom name for the resource/data source methods: ... ``` See the [resource reference](/docs/reference/config#resource/index.md) for more information. ### Changing endpoint mapping and ID properties Sometimes your API endpoints don’t fully match CRUD semantics. You can customize which endpoints have which Terraform behavior (create, read, update, delete), and how to find the ID parameter. - `method` specifies which CRUD operation the endpoint should be used for - `id_property` for create, specifies the request or response property that represents the ID - `id_path_param` for read, update, and delete, specifies the path param that represents the ID See the following config where each of the default options is explicitly set: ``` products: terraform: true methods: list: endpoint: get /products terraform: method: list create: endpoint: post /products terraform: id_property: product_id method: create retrieve: endpoint: get /products/{product_id} terraform: id_path_param: product_id method: read update: endpoint: put /products/{product_id} terraform: id_path_param: product_id method: update delete: endpoint: delete /products/{product_id} terraform: id_path_param: product_id method: delete ``` ### Custom configurability Every “attribute” (aka property) in a Terraform resource’s schema has a “configurability” setting attached to it. The “configurability” of an attribute determines how it can change over time: - **required**: must be provided by the user at all times, cannot be null - **optional**: must be provided by the user at all times, can be null - **computed**: cannot be provided by the user, is provided by the API or provider - **computed and optional**: can be provided by the user or the API or provider There isn’t quite enough information in the OpenAPI spec to specify each of these precisely, so you can add an annotation to your OpenAPI spec if we don’t infer it correctly. By default, we infer configurability like so: - **required**: marked as required in the create endpoint’s request - **optional**: not marked as required - **computed**: a property only defined in the response of the create or update endpoint, or marked as `read-only` in the OpenAPI spec To customize the configurability of a given property, specify the `x-stainless-terraform-configurability` extension property on your schema. For example: ``` cardholder_name: type: string description: The cardholder's name x-stainless-terraform-configurability: computed_optional # required, optional, computed, computed_optional are all valid values ``` ## Editions Editions allow Stainless to make improvements to SDKs that aren’t backwards-compatible. You can explicitly opt in to new editions when you’re ready. See the [SDK and config editions reference](/docs/reference/editions/index.md) for more information. #### terraform.2025-10-08 - Initial edition for Terraform (used by default if no edition is specified) ## Testing and validation ### Address diagnostics When you switch to the Terraform tab in Stainless Studio, you may see some diagnostics that relate to the resources you enabled. View our [diagnostics reference](/docs/reference/diagnostics#terraform/index.md) for instructions on how to resolve them. You don’t have to address them all right away, but consider addressing any `Error` diagnostics to maximize the chances that the provider will work while testing. ### View your generated provider Once the provider is generating, take a look at the repo to see the generated output. You can find generated documentation and examples in the `./docs/resources` and `./docs/data-sources` folders. You can use Terraform’s [documentation preview tool](https://registry.terraform.io/tools/doc-preview) to see a formatted version of each resource and confirm that they make sense. ### Installation To test your generated provider locally: 1. [Install Terraform](https://developer.hashicorp.com/terraform/install) on your machine. 2. Clone the generated Terraform repository from GitHub, and `cd` into it. 3. Run `./scripts/bootstrap` to install `go` and dependencies. 4. Run `go build -o terraform-provider-` to build the binary. 5. Edit (or create) your `~/.terraformrc` file and configure it to point to the directory where you put the Terraform provider. This ensures that when you try out the provider yourself, it looks locally instead of at the registry: ``` provider_installation { dev_overrides { "/" = "/path/to/local/terraform/directory" } direct {} } ``` 6. Copy an example resource from the `./examples` directory of your generated Terraform repo to a file called `main.tf`, which should look similar to: ``` terraform { required_providers { = { source = "/" version = "~> 1.0.0" } } } provider "" { api_key = "" } # copied from examples/resources resource "_" "example" { name = "The Cube" description = "A large stainless steel cube" image_url = "https://images.squarespace-cdn.com/content/v1/5488f21fe4b055c9cb360909/1427135193771-IM8B5KC0OUVQCRG7YCQD/2013-10+Steel+Cubes+-2.jpg?format=750w" price = 1000 } ``` 7. Run `terraform apply` to see the resource get created. ### Writing acceptance tests Now that you have tested some resources manually, write automated tests that cover all relevant cases and ensure that the provider is ready to ship. Follow [Hashicorp’s acceptance test guide](https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests) to run a test against the resource. A test typically looks like: ``` package example // example.Widget represents a concrete Go type that represents an API resource func TestAccExampleWidget_basic(t *testing.T) { var widgetBefore, widgetAfter example.Widget rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckExampleResourceDestroy, Steps: []resource.TestStep{ { Config: testAccExampleResource(rName), ConfigStateChecks: []statecheck.StateCheck{ stateCheckExampleResourceExists("example_widget.foo", &widgetBefore), }, }, { Config: testAccExampleResource_removedPolicy(rName), ConfigStateChecks: []statecheck.StateCheck{ stateCheckExampleResourceExists("example_widget.foo", &widgetAfter), }, }, }, }) } ``` Our recommended approach is to have at least: - A basic test with only required parameters - A more complete or complex test that covers optional parameters We also recommend implementing some [test sweepers](https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/sweepers) to clear out straggling entities on the server that don’t get deleted in the event a test fails. **Run the test** Because acceptance tests run against real infrastructure and can be slow, `go test` will not run them by default (and instead just runs unit tests). The way that you run the acceptance tests is by setting the `TF_ACC` environment variable before running `go test`: Terminal window ``` TF_ACC=1 go test ./internal/services/my_service -count 1 ``` **Set up acceptance tests in CI** You can run your acceptance tests in CI to gain ongoing confidence in the correctness of the provider. Sometimes the tests are too slow to be run upon every commit, but you could have them run on a daily schedule, or just manually run via Github’s Actions tab. See the [GitHub Actions workflow](https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests#github-actions-workflow) on Hashicorp’s site. It’s highly recommended that you ensure the tests all pass before merging a release PR. ### Release a beta Once your resources are all tested, and you’ve resolved all error diagnostics, you can release a beta terraform provider. To issue the beta release, set up a production repo and follow the instructions in the Publishing section below. ## Publishing to Terraform Registry Publish your Terraform provider to the [Terraform Registry](https://registry.terraform.io/) for distribution. If you have more custom requirements, you can also refer to the [official HashiCorp publishing documentation](https://developer.hashicorp.com/terraform/registry/providers/publishing#preparing-and-adding-a-signing-key). Before publishing, you need to [link a production repository](/docs/guides/publish#link-production-repos/index.md) where Stainless will push your provider code. The repository name must be in the form `terraform-provider-`. Connect Terraform Registry to your production repo 1. Log in or sign up at [the Terraform Registry](https://registry.terraform.io/publish/provider). 2. Select `Publish Provider`. 3. Follow the instructions to install the Hashicorp GitHub App. 4. In the [provider publishing page](https://registry.terraform.io/publish/provider), select the connected user / organization. 5. Select your production repo and follow the instructions to install the webhooks required to publish the provider. Generate a PGP key 1. Install [GnuPG](https://gnupg.org/). Note: The Terraform Registry API accepts both RSA and DSA keys, but not the default ECC type. 1. In a terminal, run `gpg --full-generate-key`. Follow the prompts to select an RSA/DSA key, and input your organization’s information. 2. Run `gpg --list-keys --keyid-format short`: Terminal window ``` % gpg --list-keys --keyid-format short [keyboxd] --------- pub rsa3072/69568313 2025-03-22 [SC] [expires: 2025-03-23] 1577714D2E1545CC87291B3A1703FF1469568313 uid [ultimate] Your Organization sub rsa3072/26D93FE8 2025-03-22 [E] [expires: 2025-03-23] ``` 3. Export the key in ASCII-armored format. In the above example, the short key ID is `69568313`. Terminal window ``` $ gpg --export-secret-keys --armor 69568313 -----BEGIN PGP PRIVATE KEY BLOCK----- lQWGBGffFm0BDADEUBBMBLQ2qRg8YPKBln9VKolAcFiq6CwehhnHw1B8xr9BiVnY 0ekDmOkAWIhU1zG+AFULhsvWowld55pMglPW1Mptpfb2AqJdbglr2iStUjSn+Jn1 riRKaP3YQErtV0/dBRFcM8F4LdGK8NhSYnrEyGb1eFCBawHxU/P09+Otcxs+9mvy jSFiWBrrK45H62sDQq2HYrjLXcytD0WpqLAxgxLN6cGwtwmfd6TAMQeJ248Qa97M ... CriOVP9mAUyhez1k6tuVi5U0mDp9KeMj3zJcsTjTEvqe+8wQQoRgtqyY60yXkJJ6 HvapRVLjxiU8/x+VltFn7sxm+LDoSEc8hcsIrT19uIyA4HOEo9etJI/VT31Tt39S XW4n8ELhMIdxDq0jZp/93ONsPBmjZ0L/qbDH6ytT0FSEuSt05CY7 =VDjv -----END PGP PRIVATE KEY BLOCK----- ``` 4. Publish your PGP public key. Terminal window ``` $ gpg --keyserver keyserver.ubuntu.com --send-keys 69568313 ``` If you get the following error: ``` gpg: sending key XXXXXX to hkp://keyserver.ubuntu.com gpg: keyserver send failed: No route to host ``` then your computer might be trying to use IPv6 and failing. Run the following to get keyserver.ubuntu.com’s IPv4 addresses: Terminal window ``` $ host keyserver.ubuntu.com keyserver.ubuntu.com has address XXX.XXX.XXX.XX keyserver.ubuntu.com has address XXX.XXX.XXX.XX keyserver.ubuntu.com has IPv6 address XXXX:XX:XXXX:XXXX::XXX keyserver.ubuntu.com has IPv6 address XXXX:XX:XXXX:XXXX::XXX ``` Then rerun the `gpg` command with one of the IPv4 addresses: Terminal window ``` $ gpg --keyserver XXX.XXX.XXX.XX --send-keys 69568313 ``` Add secrets to your production repo 1. In the production repo, navigate to **Secrets and variables** > **Actions** > **New repository secret**. The URL should look like `https://github.com///settings/secrets/actions/new`. 2. Add the following secrets: - `GPG_SIGNING_KEY`: your PGP private key. It should be the entire output of `gpg --export-secret-keys --armor `. - `GPG_SIGNING_PASSWORD`: the passphrase you entered when creating the keypair. Update your Stainless config 1. Update the [Stainless config](/docs/reference/config#terraform/index.md) and save. ``` targets: terraform: publish: hashicorp_registry: true ``` Merge your release PR and see it appear in the Registry Merge your release PR when you’re ready. This should then create a tag, use [goreleaser](https://goreleaser.com/) to build artifacts, and attach it to the release. Hashicorp will then import the release into their registry. You might need to wait a few minutes for it to show up on your [provider page](https://registry.terraform.io/publish/provider).