There is already a bunch of articles out there to help you create a Terraform provider. However after having done it myself I wanted to write about it. Mostly to keep track of how I did it but also to try to give you a few hints to write your own. The idea here is to go through the entire process.
If you are reading this post it’s likely that you already know which provider you want to implement. You also probably know that the API you will be using to write your plugin will need to implement the default CRUD functions. If not, you should take a look at the external data source resource.
Before starting, some assumptions:
- You use Terraform and are familiar with all the core concepts (in particular providers and resources).
- You know how to write in Go. No need to be an expert but you should be able to use any kind of SDK or API by reading the documentation.
Writing a Terraform provider
Basically a provider is composed of two parts : the “provider” itself and some resources. If you are using Terraform to manage your AWS infrastructure, you have something like :
Here, the “aws” provider part is taking care of setting up your AWS Client and authenticate you to the AWS API. Then the “aws_vpc” resource will create an AWS VPC with the correct CIDR block for you, using this AWS Client.
If you take a look at the existing providers, you will notice that the structure is almost always something like :
provider.go: Implement the “core” of the Provider.
config.go: Configure the API client with the credentials from the Provider.
resource_<resource_name>.go: Implement a specific resource handler with the CRUD functions.
import_<resource_name>.go: Make possible to import existing resources. We won’t expand on resources in this post.
data_source_<resource_name>.go: Used to fetch data from outside of Terraform to be used in other resources. For example, you will be able to fetch the latest AWS AMI ID and use it for an AWS instance. Same as “import”, we won’t go further on this in this post.
Writing the “provider” is the easiest part so let’s start here.
To write your provider, you need to implement a terraform.ResourceProvider. It might seem complicated at first but it’s actually pretty easy.
This is how you start :
Basically we have a function
Provider() which is returning a
terraform.ResourceProvider with all the required configuration to do the job :
Schemais where you list the parameters of your provider. For instance, with AWS we have the access_key and the secret_key.
ResourceMapis the list of the resources managed by your provider.
ConfigureFuncis the function which, among other things, instantiates and configures the client you use to interact with the targeted API (AWS SDK for example).
Let’s go ahead and write a unit test for this provider. Even is the Provider is not operational right now, I always try to write the test as soon as possible.
init() function set the
testAccProvider variable with our provider and we’ll just make sure here that Terraform is happy with our implementation.
Next you need to configure the client for the API you want to interact with. Let’s say we are writing a provider for a really simple API. This API requires a user and a token for authentication.
This will be in the Schema of your provider :
Those two variables are strings so we use the type
schema.TypeString. Also we make sure that they are set by using
Get yourself familiar with
schema.Schema, you will also use it for the Resources. For instance, check the different types supported here :
You can now use those parameters to configure the client within
configureProvider. This function should return a configured API client. Hence the usage of
At this point you have a working provider ! Well, you don’t have any resource so even if you use it in a Terraform manifest, you will see nothing happening :)
The first resource
This is where the fun begins. As we did above with the
Provider, let’s define the skeleton of your first resource :
First things we have here is the definition of the CRUD functions :
Createwill simply create a new instance of your resource. The is also where you will have to set the ID (has to be an Int) of your resource. If the API you are using doesn’t provide an ID, you can always use a random Int.
Readwill fetch the data of a resource.
Updateis optional if your Resource doesn’t support update. For example, I’m not using update in the Terraform LDAP Provider. I just destroy and recreate the resource everytime there is a change.
Existsis called before
Readand obviously makes sure the resource exists.
Then, as you can see, we have the
Schema again ! Nothing really new here compare to the provider except if you have to use more complex attributes than string.
For instance, if you have something like a list of tags :
You will want to use the
schema.TypeList type. The declaration will be :
To retrieve those
tags within the CRUD function, you can do something like :
Things can quickly get tricky here. For example, if you need to implement attributes which can be specified multiple times like in the
aws_security_group resource :
You will use the type
schema.TypeMap and use another level of
schema.Schema to define all the parameters of the
egress rules. See resource_aws_security_group.go#L83.
In the case of the
schema.TypeSet, you will have the use the function
Set() in order to retrieve all the values. For instance :
Build it and try it
Throughout this post we have been using the package
main. We did this mostly because our plugin is
standalone. If you want to Pull Request your plugin within the Terraform Github, you will
have to change
main by the name of your plugin.
Since our plugin is standalone, we need a main :
Build it with a name that Terraform can understand :
Tell Terraform where to find your plugin :
Write a quick manifest :
Plan and apply !
terraform plan terraform apply
This post is a quick walkthrough to give you a starting point to write Terraform Providers. There is much more details to talk about like
Data Sources, or also
Also I didn’t talk about how to test the Resources. There are really damn good examples out there. Take for instance this one : resource_datadog_monitor_test.go.