Implementing A Terraform Provider
I recently started working on Terraform Provider plugins, but there doesn’t seem to be much information about plugin-development out there on the internet. So I decided to collect some of my experience in a couple of posts.

For more information about the Terraform resource lifecycle:
For more information about Terraform Provider plugins:
Let’s implement a Terraform provider abc with a data-source abc_xyz and a resource abc_xyz. Most Terraform provider follow the same pattern.
We are using the following directory/file structure.
-
github.com/stefaanc/terraform-provider-abc-
abcconfig.godatasource_abc_xyz.goprovider.goresource_abc_xyz.go
-
apiclient.goxyz.go
main.go
-
The main.go file will look something like
// github.com/stefaanc/terraform-provider-abc/main.go
package main
import (
"github.com/hashicorp/terraform-plugin-sdk/plugin"
"github.com/stefaanc/terraform-provider-abc/abc"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: abc.Provider,
})
}
The Provider 🔗
We need a provider. To keep things simple, our provider’s configuration will have one attribute: name. We are not going to use this attribute in this post, but imagine this is the name of the host where the resources are residing, under control of this provider.
// github.com/stefaanc/terraform-provider-abc/abc/provider.go
package abc
import (
"github.com/hashicorp/terraform-plugin-sdk/terraform"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)
func Provider() terraform.ResourceProvider {
return &schema.Provider{
Schema: map[string]*schema.Schema {
// config attributes
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "my-host",
},
},
DataSourcesMap: map[string]*schema.Resource {
"abc_xyz": dataSourceABCXYZ(),
},
ResourcesMap: map[string]*schema.Resource{
"abc_xyz": resourceABCXYZ(),
},
ConfigureFunc: providerConfigure,
}
}
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := Config{
Name: d.Get("name").(string),
}
return config.Client()
}
We need a provider config.
// github.com/stefaanc/terraform-provider-abc/abc/config.go
package abc
import (
"github.com/stefaanc/terraform-provider-abc/api"
)
type Config struct {
// config attributes
Name string
}
func (c *Config) Client() (interface {}, error) {
// process the attributes of the provider's configuration `c`, and initialize the provider API
client := new(api.ABCClient)
client.Name = c.Name
return client, nil
}
And we need a provider API.
// github.com/stefaanc/terraform-provider-abc/api/client.go
package api
import (
)
type ABCClient struct {
Name string
}
Data-Sources 🔗
We need a schema for our data-source abc_xyz. Our data-source will have two attributes: name to identify the resource, and status read from the infrastructure. We will not really read anything from infrastructure, but will just return some values to emulate a real data-source in the infrastructure.
// github.com/stefaanc/terraform-provider-abc/abc/datasource_abc_xyz
package abc
import (
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/stefaanc/terraform-provider-abc/api"
)
func dataSourceABCXYZ() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"status": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
Read: dataSourceABCXYZRead,
}
}
We need a data-source API
// github.com/stefaanc/terraform-provider-abc/api/xyz.go
package api
import (
)
type XYZ struct {
Name string
Status string
}
Terraform needs a Read-method.
// github.com/stefaanc/terraform-provider-abc/abc/datasource_abc_xyz
func dataSourceABCXYZRead(d *schema.ResourceData, m interface{}) error {
c := m.(*api.ABCClient)
// get the identifying attributes of the data-source
name := d.Get("name").(string)
// read the data-source's information from the infrastructure
xyz, err := c.ReadXYZ(name)
if err != nil {
return err
}
// set Terraform state
d.Set("name", xyz.Name)
d.Set("status", xyz.Status)
// set id
d.SetId(name)
return nil
}
And the data-source API needs a Read-method
// github.com/stefaanc/terraform-provider-abc/api/xyz.go
func (c *ABCClient) ReadXYZ(name string) (xyz *XYZ, err error) {
// read the data-source's information from the infrastructure
// for this post, we are just returning some values
xyz = new(XYZ)
xyz.Name = name
xyz.Status = "open"
return xyz, nil
}
Resources 🔗
We need a schema for our resource abc_xyz. Our resource will have two attributes: name to identify the resource, and status to create, read, update in/from the infrastructure. We will not really create anything in the infrastructure, and will just return some values when reading, to emulate a real resource in the infrastructure.
// github.com/stefaanc/terraform-provider-abc/abc/resource_abc_xyz
package abc
import (
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/stefaanc/terraform-provider-abc/api"
)
func ResourceABCXYZ() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"status": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "closed",
},
},
Create: resourceABCXYZCreate,
Read: resourceABCXYZRead,
Update: resourceABCXYZUpdate,
Delete: resourceABCXYZDelete,
}
}
Compared to the data-source:
- the attribute
statusis nowOptionalinstead ofComputed- we now have the methods
Create,Read,UpdateandDeleteinstead ofReadonly
We reuse the same API as for the data-source
// github.com/stefaanc/terraform-provider-abc/api/xyz.go
package api
import (
)
type XYZ struct {
Name string
Status string
}
Terraform needs a Create-method.
// github.com/stefaanc/terraform-provider-abc/abc/resource_abc_xyz
func resourceABCXYZCreate(d *schema.ResourceData, m interface{}) error {
c := m.(*api.ABCClient)
// get the configured attributes of the resource
name := d.Get("name").(string)
status := d.Get("status").(string)
// create the resource in the infrastructure
err := c.CreateXYZ(name, status)
if err != nil {
return err
}
// set id
d.SetId(name)
return resourceABCXYZRead(d, m)
}
Note that theCreate-method calls theRead-method when returning.
The resource API needs a Create-method
// github.com/stefaanc/terraform-provider-abc/api/xyz.go
func (c *ABCClient) CreateXYZ(name string, status string) error {
// create the resource in the infrastructure
// for this post, we do nothing
return nil
}
Terraform needs a Read-method.
// github.com/stefaanc/terraform-provider-abc/abc/resource_abc_xyz
func resourceABCXYZRead(d *schema.ResourceData, m interface{}) error {
c := m.(*api.ABCClient)
// get the identifying attributes of the resource
name := d.Get("name").(string)
// read the data-source's information from the infrastructure
xyz, err := c.ReadXYZ(name)
if err != nil {
d.SetId("")
return nil
}
// set Terraform state
d.Set("name", xyz.Name)
d.Set("status", xyz.Status)
return nil
}
Compared to the data-source:
- When the API
Read-method returns an error, we set the ID to""and returnnilinstead of returning the error. This allows this resource to be deleted from the Terraform state when Terraform refreshes its state.- The function doesn’t set the resource’s ID, since this was already set when the resource was created.
We reuse the same API Read-method as for the data-source
// github.com/stefaanc/terraform-provider-abc/api/xyz.go
func (c *ABCClient) ReadXYZ(name string) (xyz *XYZ, err error) {
// read the data-source's information from the infrastructure
// for this post, we are just returning some values
xyz = new(XYZ)
xyz.Name = name
xyz.Status = "open"
return xyz, nil
}
Terraform needs an Update-method.
// github.com/stefaanc/terraform-provider-abc/abc/resource_abc_xyz
func resourceABCXYZUpdate(d *schema.ResourceData, m interface{}) error {
c := m.(*api.ABCClient)
// get the configured attributes of the resource
name := d.Get("name").(string)
status := d.Get("status").(string)
// update the resource in the infrastructure
err := c.UpdateXYZ(name, status)
if err != nil {
return err
}
return resourceABCXYZRead(d, m)
}
Note that theUpdate-method calls theRead-method when returning.
The resource API needs an Update-method
// github.com/stefaanc/terraform-provider-abc/api/xyz.go
func (c *ABCClient) UpdateXYZ(name string, status string) error {
// update the resource in the infrastructure
// for this post, we do nothing
return nil
}
Terraform needs a Delete-method.
// github.com/stefaanc/terraform-provider-abc/abc/resource_abc_xyz
func resourceABCXYZDelete(d *schema.ResourceData, m interface{}) error {
c := m.(*api.ABCClient)
// get the identifying attributes of the resource
name := d.Get("name").(string)
// delete the resource from the infrastructure
err := c.DeleteXYZ(name)
if err != nil {
return err
}
// set id
d.SetId("")
return nil
}
And the resource API needs a Delete-method
// github.com/stefaanc/terraform-provider-abc/api/xyz.go
func (c *ABCClient) DeleteXYZ(name string) error {
// delete the resource from the infrastructure
// for this post, we do nothing
return nil
}
Building & Running It 🔗
I prepared a small package for this example provider, in case you want to play with it.
To build the provider:
-
Create a repository, for instance called
terraform-provider-abc -
Download the content from the
terraform-provider-abcin theabcpackage into your repository
To make this a fully working Terraform provider, we extended the infrastructure-API presented in this post, creating a JSON-file with thenameandstatusattributes, so the resource can be read, updated and deleted. Thenameattribute is name of the file. -
Assuming you have
goinstalled and properly configured,
in theterraform-provider-abcdirectory,- run
go mod tidy - run
go build -o "$env:APPDATA/terraform.d/plugins"(on Windows using Powershell)
orgo build -o "%APPDATA%\terraform.d\plugins"(on Windows using CMD)
orgo build -o ~/.terraform.d/plugins(on Linux)
- run
To run the provider:
-
Assuming you have
terraforminstalled and properly configured,
in theterraform-provider-abc/examplesdirectory,- run
terraform init - run
terraform plan - run
terraform apply - run
terraform destroy
- run
EDIT 18-02-2020: code-corrections + added Building & Running section
EDIT 19-02-2020: extended the infrastructure-API in the abc package
Leave a comment