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- 
abc- config.go
- datasource_abc_xyz.go
- provider.go
- resource_abc_xyz.go
 
- 
api- client.go
- xyz.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