Terraform
Code Walkthrough
Terraform providers let Terraform communicate with third parties, such as cloud providers, SaaS providers, and other APIs. Terraform and Terraform providers use gRPC to communicate. Terraform operates as a gRPC client and providers operate as gRPC servers.
Each provider defines resources that let Terraform manage infrastructure objects and data sources that let Terraform read data. Terraform practitioners then write configuration to define resources, such as compute storage or networking resources. Terraform then communicates this configuration to the provider, and the provider creates the infrastructure.
This example provider shows the relationship between the required provider components. The resources and data sources in a typical provider interact with a cloud provider through an API, but the example only stores values in state.
Core Provider Components
A Terraform plugin provider requires at least the following components:
The provider wraps the resource(s) and/or data source(s), and can be used to configure a client which communicates with a 3rd party service via an API. Resources are used to manage infrastructure objects. Data sources are used to read infrastructure objects.
Provider Server
Each provider must implement a gRPC server that supports Terraform-specific connection and handshake handling on startup. A provider server is required in order for a Terraform provider to:
- expose resources that can be managed by Terraform core.
- expose data sources that can be read by Terraform core.
The main()
function is used for defining a provider server.
The provider.New()
returns a function which returns a type that satisfies the provider.Provider
interface. The provider.Provider
interface defines functions for obtaining the resource(s) and/or data source(s) from a provider.
package main
import (
"context"
"flag"
"log"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/example_namespace/terraform-provider-example/internal/provider"
)
func main() {
var debug bool
flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve")
flag.Parse()
opts := providerserver.ServeOpts{
Address: "registry.terraform.io/example_namespace/example",
Debug: debug,
}
err := providerserver.Serve(context.Background(), provider.New(), opts)
if err != nil {
log.Fatal(err.Error())
}
}
Refer to Provider Servers for more details.
Provider
The provider wraps resources and data sources which are typically used for interacting with cloud providers, SaaS providers, or other APIs.
In this example the provider wraps a resource and a data source which simply interact with Terraform state. Refer to the tutorial for an example of provider configuration that configures an API client.
New()
returns a function which returns a type that satisfies the provider.Provider
interface. The New()
function is called by the provider server to obtain the provider.
The exampleProvider
struct implements the provider.Provider
interface. This interface defines the following functions:
Schema
: This function returns a providerschema.Schema
struct that defines the provider schema. Schemas specify the constraints of Terraform configuration blocks. They define what fields a provider, resource, or data source configuration block has, and give Terraform metadata about those fields.Configure
: This function lets you configure provider-level data or clients. These configuration values may be from the practitioner Terraform configuration as defined by the schema, environment variables, or other means such as reading vendor-specific configuration files.Resources
: This function returns a slice of functions that return types that implement theresource.Resource
interface. Resources let Terraform manage infrastructure objects, such as a compute instance, an access policy, or disk.Data Sources
: This function returns a slice of functions that return types which implement thedatasource.DataSource
interface. Data sources let Terraform reference external data. For example a database instance.
The exampleProvider
struct also implements the provider.ProviderWithMetadata
interface which defines the Metadata
function. The Metadata
function returns metadata for the provider such as a TypeName
and Version
. The TypeName
is used as a prefix within a provider by for naming resources and data sources.
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ provider.Provider = (*exampleProvider)(nil)
var _ provider.ProviderWithMetadata = (*exampleProvider)(nil)
type exampleProvider struct{}
func New() func() provider.Provider {
return func() provider.Provider {
return &exampleProvider{}
}
}
func (p *exampleProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
}
func (p *exampleProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "example"
}
func (p *exampleProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewDataSource,
}
}
func (p *exampleProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewResource,
}
}
func (p *exampleProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
}
Refer to Providers for more details and configuration examples.
Resource
A resource is typically used to manage infrastructure objects such as virtual networks and compute instances.
In this example the resource simply interacts with Terraform state.
NewResource()
returns a function which returns a type that satisfies the resource.Resource
interface. The provider calls the NewResource()
function within provider.Resources
to obtain an instance of the resource.
The exampleResource
struct implements the resource.Resource
interface. This interface defines the following functions:
Metadata
: This function returns the full name (TypeName
) of the resource. The full name is used in Terraform configuration asresource <full name> <alias>
.Schema
: This function returns a resourceschema.Schema
struct that defines the resource schema. The schema specifies the constraints of the resource Terraform configuration block. It defines what fields a resource configuration block has, and gives Terraform metadata about those fields. For instance, defining whether a field is required.Create
: This function lets the provider create a new resource of this type.Read
: This function lets the provider read resource values in order to update state.Update
: This function lets the provider update the resource and state.Delete
: This function lets the provider delete the resource.
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
var _ resource.Resource = (*exampleResource)(nil)
type exampleResource struct {
provider exampleProvider
}
func NewResource() resource.Resource {
return &exampleResource{}
}
func (e *exampleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_resource"
}
func (e *exampleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"configurable_attribute": schema.StringAttribute{
Optional: true,
},
"id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
}
}
type exampleResourceData struct {
ConfigurableAttribute types.String `tfsdk:"configurable_attribute"`
Id types.String `tfsdk:"id"`
}
func (e *exampleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data exampleResourceData
diags := req.Config.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Create resource using 3rd party API.
data.Id = types.StringValue("example-id")
tflog.Trace(ctx, "created a resource")
diags = resp.State.Set(ctx, &data)
resp.Diagnostics.Append(diags...)
}
func (e *exampleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data exampleResourceData
diags := req.State.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Read resource using 3rd party API.
diags = resp.State.Set(ctx, &data)
resp.Diagnostics.Append(diags...)
}
func (e *exampleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data exampleResourceData
diags := req.Plan.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Update resource using 3rd party API.
diags = resp.State.Set(ctx, &data)
resp.Diagnostics.Append(diags...)
}
func (e *exampleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data exampleResourceData
diags := req.State.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Delete resource using 3rd party API.
}
Refer to Resources for more details and configuration examples.
Data Source
A data source is typically used to provide a read-only view of infrastructure objects.
In this example the data source simply interacts with Terraform state.
NewDataSource()
returns a function which returns a type that satisfies the datasource.DataSource
interface. The NewDataSource()
function is used within the provider.DataSources
function to make the data source available to the provider.
The exampleDataSource
struct implements the datasource.DataSource
interface. This interface defines the following functions:
Metadata
: This function returns the full name (TypeName
) of the data source. The full name is used in Terraform configuration asdata <full name> <alias>
.Schema
: This function returns a data sourceschema.Schema
struct that defines the data source schema. The schema specifies the constraints of the data source Terraform configuration block. It defines what fields a data source configuration block has, and gives Terraform metadata about those fields. For instance, defining whether a field is optional.Read
: This function lets the provider read data source values in order to update state.
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
var _ datasource.DataSource = (*exampleDataSource)(nil)
type exampleDataSource struct {
provider exampleProvider
}
func NewDataSource() datasource.DataSource {
return &exampleDataSource{}
}
func (e *exampleDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_datasource"
}
func (e *exampleDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"configurable_attribute": schema.StringAttribute{
MarkdownDescription: "Example configurable attribute",
Optional: true,
},
"id": schema.StringAttribute{
MarkdownDescription: "Example identifier",
Computed: true,
},
},
}
}
type exampleDataSourceData struct {
ConfigurableAttribute types.String `tfsdk:"configurable_attribute"`
Id types.String `tfsdk:"id"`
}
func (e *exampleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data exampleDataSourceData
diags := req.Config.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Interact with 3rd party API to read data source.
data.Id = types.StringValue("example-id")
tflog.Trace(ctx, "read a data source")
diags = resp.State.Set(ctx, &data)
resp.Diagnostics.Append(diags...)
}
Refer to Data Sources for more details and configuration examples.
Terraform Configuration
Refer to terraform-provider-scaffolding-framework for details on how to wire together a provider server, provider, resource and data source.
Once wired together, run the provider by specifying configuration and executing terraform apply
.
Resource Configuration
resource "example_resource" "example" {
configurable_attribute = "some-value"
}
The configurable_attribute
is defined within the schema as a string type attribute.
Examples of the various types of attributes and their representation within Terraform configuration and schema definitions are detailed in Terraform Concepts.
Data Source Configuration
data "example_datasource" "example" {
configurable_attribute = "some-value"
}
The configurable_attribute
is defined within the schema as a string type attribute.
Examples of the various types of attributes and their representation within Terraform configuration and schema definitions are detailed in Terraform Concepts.