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
status
is nowOptional
instead ofComputed
- we now have the methods
Create
,Read
,Update
andDelete
instead ofRead
only
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 returnnil
instead 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-abc
in theabc
package 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 thename
andstatus
attributes, so the resource can be read, updated and deleted. Thename
attribute is name of the file. -
Assuming you have
go
installed and properly configured,
in theterraform-provider-abc
directory,- 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
terraform
installed and properly configured,
in theterraform-provider-abc/examples
directory,- 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