The Terraform Plugin Framework is a new way to develop Terraform plugins. It has features that were missing from the Terraform Plugin SDKv2, including the ability to:
- determine whether a value was set in the config, the state, or the plan.
- determine whether a value is null, unknown, or the empty value.
- have structured types like objects.
- natively use nested attributes.
In this tutorial, you will add create and read functionality to a provider for a fictional coffee shop app (HashiCups), using the Terraform Plugin Framework. First, you will review the provider and resource schema to understand how providers can use the plugin framework to map Go structs to schema attributes. Then, you will explore the steps associated with creating and reading a resource before implementing it yourself. Finally, you will test the provider.
The HashiCups API has unprotected endpoints that let you list all coffees and ingredients for a particular coffee. Once authenticated, you can create, read, update, and delete (CRUD) orders. The HashiCups provider interfaces with the HashiCups REST API through a GoLang client library. This allows you to manage HashiCups orders through Terraform.
This framework is experimental. Do not use in production or critical environments. Submit any issues to the development team in the Terraform Plugin framework Github repository. For feedback and discussion, visit the Plugin SDK Discuss forum.
»Prerequisites
For this tutorial, you will need:
- Go 1.16+ installed and configured. the
- Terraform v1.0.3+ installed locally.
- Docker and Docker Compose to run an instance of HashiCups locally.
- jq installed.
»Set up your development environment
Clone the boilerplate
branch of the Terraform HashiCups (plugin-framework)
Provider
repository. This
serves as the boilerplate for your provider workspace.
Change into the cloned repository.
»Start HashiCups locally
The HashiCups provider requires a running instance of HashiCups. Navigate to the docker_compose
directory.
Run docker-compose up
to spin up a local instance of HashiCups on port 19090
.
Leave this process running in your terminal window. This will contain HashiCups' log messages.
In another terminal window, verify that HashiCups is running by sending a request to its health check endpoint.
»Create HashiCups user
HashiCups requires a username and password to generate an JSON web token (JWT) which is used to authenticate against protected endpoints. You will use this user to authenticate to the HashiCups provider to manage your orders.
Create a user on HashiCups named education
with the password test123
.
Set the HASHICUPS_TOKEN
environment variable to the token you retrieved from
invoking the /signup
endpoint. You will use this later in the tutorial to verify
that Terraform has created your HashiCups order.
The terminal containing your HashiCups logs will record the sign up operation.
Now that the HashiCups app is running, you are ready to start working on the Terraform provider.
»Explore the HashiCups provider
Navigate out of the docker_compose
directory.
Open the main.go
file. The main function imports the hashicups
package and
the terraform-plugin-framework/tfsdk
package. The tfsdk
package allows
Terraform to communicate with the provider.
Open hashicups/provider.go
.
First, the provider calls the New
function to call an instance of the provider. Notice that this file also defines a provider struct with a client. Like SDKv2, Terraform providers should interface with an API client instead of directly submitting requests to the API.
Next, the provider defines a schema that Terraform will use to configure the
HashiCups API client. The providerData
struct maps these schema attributes to
Go-friendly values.
»Review the Configure function
The Configure
function configures the API client and adds it to the provider.
The func (p*provider)
sets the reference data for your provider as variable
p
. The function takes three arguments:
ctx context.Context
is part of Go's standard library and commonly used to make API requests.req tfsdk.ConfigureProviderRequest
represents the request Terraform sends to the provider during the Configure step. It is a required request struct in thetfsdk
library and specifies the values from a Terraform configuration block, along with other runtime information from Terraform.resp *tfsdk.ConfigureProviderResponse
represents the response from the provider. It is a required response struct. You can use this to return error and warning messages.
The Configure
function performs the following steps:
Retrieves the provider data from the configuration. First, it creates a variable named
config
to hold the provider data defined by theproviderData
struct. Then, the provider data is retrieved from the configuration and written to theconfig
variable withreq.Config.Get(ctx, &config)
. If the provider is unable to retrieve provider data from the configuration, it will throw an error when you try to initialize the provider. This code ensures that error is returned in the response.hashicups/provider.goValidates whether the
username
,password
, orhost
exists. The provider checks ifusername
is not configured in the Terraform configuration or in an environment variable. The provider returns an error if the value is unknown. Then, the provider tries to retrieve the value from an environment variable or from your Terraform configuration. If this value is a null value or an empty string, the provider will return with a diagnostic error. The provider repeats this process for thepassword
andhost
attributes.hashicups/provider.goCreates a new API client. The provider will create a new API client with the configuration values and add it to the provider. This allows the provider to connect to and interact with the HashiCups API.
hashicups/provider.go
»Define resources and data sources
The GetResources
and GetDataSources
functions define your provider's resources and data sources. This boilerplate version of the HashiCups provider has one resource named hashicups_order
and does not define any data sources.
»Explore resource order
Open the hashicups/resource_order.go
file. As a general convention, Terraform providers put each resource in their own file, named after the resource, prefixed with resource_
.
Find the resourceOrderType
struct and its schema. The GetSchema
function defines the order resource's schema.
Next, find the NewResource
function, which creates a new instance of your resource.
Below this, find the four functions the provider uses to perform CRUD (create, read, update, delete) operations. In the later sections, you will implement the create and read functionality.
»Define order schema
The resource schema defines the resources's attributes and its metadata. Post a new order to the HashiCups API to review the structure of the data it sends.
Create a new order in the API to review the data structure the API creates for
the new orders. Pipe the response to jq
to format the json
data.
In the hashicups/resource_order.go
file, replace the GetSchema
function with the following definition. This schema maps closely to the response body for creating a new HashiCups order.
Schemas in providers, resources, and data sources follow a few rules:
- You must define either a
Type
or anAttributes
field.- The
Type
property of atfsdk.Attribute
specifies the type of the attribute. Attributes commonly have types defined in theterraform-plugin-framework/types
package, liketypes.StringType
andtypes.NumberType
. - The
Attributes
property of atfsdk.Attribute
specifies an attribute's nested attributes. Nested attributes are attributes that are grouped beneath another attribute; this can include items like a list of objects, a set of objects, or nested objects.
- The
- You must determine your
Required
,Computed
,Optional
fields.- If an attribute has
Required
set to true, the user must define it. The attribute cannot haveComputed
orOptional
set to true. - If an attribute only has
Computed
set to true, the provider can set this attribute's value and considers it read-only. - If an attribute only has
Optional
set to true, the user is free to set the attribute or omit it. - If an attribute has both
Computed
andOptional
set to true, Terraform will not validate the presence or absence of the value.
- If an attribute has
»Review HashiCups models
Open hashicups/models.go
.
This file defines a series of Go structs that map to the schema you defined above. The provider uses these models to parse the resource's configuration, plan, and state. Each field in the struct must have an associated field tag (for example, tfsdk:"_"
) so the provider knows the relationship between the field in the struct and the corresponding attribute in the schema. These structs' field tags must match the schemas' exactly or the provider will throw an error.
The create and read functions use these models to parse the resource's plan and state. state.
»Implement create functionality
The provider uses the Create
function to create a new resource based on the schema data.
The create function follows these steps:
- Checks whether the provider and API Client are configured. If they are not, the provider responds with an error.
- Retrieves values from the plan. The function will attempt to retrieve values from the plan and convert it to an
Order
struct (defined inmodels.go
). - Generates an API request body from the plan values. The function loops through each plan item and maps it to a
hashicups.OrderItem
. This is what the API client needs to create a new order. - Creates a new order. The function invokes the API client's
CreateOrder
method. - Maps response body to resource schema attributes. After the function creates an order, it maps the
hashicups.Order
response to[]OrderItem
so the provider can update the Terraform state. - Sets state to new order.
Open the hashicups/resource_order.go
file.
Replace your Create
function with the following.
»Implement read functionality
The provider uses the Read
function to retrieve the resource's information and update the Terraform state to reflect the resource's current state. The provider invokes this function before every apply to generate an accurate diff between the resource's current state and the configuration.
The read function follows these steps:
- Gets the current state. If it is unable to, the provider responds with an error.
- Retrieves order ID from state.
- Gets the order's information. The function invokes the API client's
GetOrder
method with the order ID. - Maps response body to resource schema attributes. After the function
retrieves the order, it maps the
hashicups.Order
response to[]OrderItem
so the provider can update the Terraform state. - Set state to new order.
Replace your Read
function with the following:
Replace the import
statement at the top of resource_order.go
with the following:
Periodically format your code. Use the following command from within the hashicups
directory.
If you were stuck at any step, check out the
create-read-order
branch to see the changes implemented in this tutorial.
»Build the provider
Now that you have implemented the read functionality, you are ready to build your provider.
Terraform verifies provider versions and checksums at the terraform init
phase. Terraform downloads your providers from either the provider registry, or from a local registry. However, while building your provider, you will want to try a test configuration against a development build of a provider that doesn't have an associated version number yet, and doesn't have an official set of checksums listed in a provider registry.
As a convenience for provider development, Terraform supports a special additional block dev_overrides
in a CLI configuration file called .terraformrc
. This block overrides all other configured installation methods.
Terraform searches for the .terraformrc
file in your home directory and applies any configuration settings you set.
First, find the GOBIN
path where Go installs your binaries. Your path may vary depending on how your Go environment varibales are configured.
Create a new file called .terraformrc
in the root directory (~
), then add the dev_overrides
block below. Change the <PATH>
to the value returned from the echo $GOBIN
command above.
In the terraform-provider-hashicups-pf
directory, update your go.mod
file.
Build the provider binary.
Now that the provider is in your GOBIN directory, you can use the provider in your Terraform configuration.
»Apply the provider
The Terraform provider you just modified is ready to communicate with your API endpoint to create an order. In future tutorials, you will implement further functionality for your provider.
Navigate to the examples
directory. This directory contains a sample configuration file to test create and read functionality.
Apply your configuration to create the order. Notice how the execution plan shows a proposed order, with additional information about the order. Terraform returns a warning about using the dev_overrides
block. Remember to confirm the apply step with a yes
.
Once the apply completes, the provider saves the resource's state. View the state by running terraform state show <resource_name>
.
The (known after apply)
values in the execution plan during the terraform apply state have all been populated, since the order was successfully created.
»Verify order created
When you create an order in HashiCups using Terraform, the terminal containing your HashiCups logs will have recorded operations invoked by the HashiCups provider.
The provider invoked a total of 3 operations.
- The provider invoked the first
signin
operation when you ranterraform apply
to retrieve the current state of the resources. Because there are no resources, it only authenticates the user. - The provider invoked the second
signin
operation after you confirmed the apply run. The provider authenticated using the provided credentials to retrieve and save the JWT token. - The provider invoked the
CreateOrder
operation to create the order defined by the Terraform configuration. Since this is a protected endpoint, it used the saved JWT token from thesignin
operation.
Verify that Terraform created the order by retrieving the order details via the API.
The order's properties should be the same as that of your hashicups_order.edu
resource.
»Next steps
Congratulations! You have created the order
resource with create and read
capabilities.
If you were stuck during this tutorial, checkout the
create-read-order
branch to see the changes implemented in this tutorial.
- To learn more about the Terraform Plugin Framework, refer to the Terraform Plugin Framework documentation.
- For a full capability comparison between the SDKv2 and the Plugin Framework, refer to the Which SDK Should I Use? documentation.
- The Terraform HashiCups (plugin-framework)
provider's
main
branch contains the complete HashiCups provider. It includes a data source written with the plugin framework and implements create, read, update and delete functionality for the order resource.