If you’re reading this, chances are, you’re interested in creating a custom Terraform provider. But let me ask you this: Have you ever wished you could manage resources from your own application using Terraform? If you have, then you’re in the right place!
Creating a provider allows you to extend Terraform’s functionality by interacting with APIs, databases, or anything you want to manage. This tutorial will walk you through how to build a custom provider using Go and demonstrate the key components in the Terraform configuration file.
Let’s dive in!
Terraform Provider : What it is?
In simple terms, a provider is the bridge between Terraform and the APIs or services you want to manage. Think of it as the translator between Terraform and a resource, helping Terraform communicate with services like AWS, Azure, Google Cloud, or even your own APIs.
With a provider, you can manage infrastructure as code, automate repetitive tasks, and integrate your own custom APIs seamlessly into Terraform workflows. If you ever wanted Terraform to interact with something that’s not supported out of the box, you need to write your own provider!
Setting Up a Basic one
Here’s where the fun begins. Let’s say we want to create a provider that interacts with a mock API (because we don’t always want to connect to a real-world service while learning).
We’ll start by setting up a basic provider using Go. In this blog, I’ll walk you through both the Go code and the Terraform configuration. By the end, you’ll have a working provider, and you’ll know exactly how to make your own provider in the future.
A Simple Golang script
The first thing you need is Go code that defines the provider and resource logic. This code connects your custom provider to the mock API and defines how resources are created, read, updated, and deleted.
Here’s how the Go code looks:
package main
import (
"fmt"
"net/http"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/plugin"
"io/ioutil"
"encoding/json"
)
const apiURL = "http://localhost:8080"
// Provider function
func provider() *schema.Provider {
return &schema.Provider{
ResourcesMap: map[string]*schema.Resource{
"simpleexample_resource": resourceSimpleExample(),
},
}
}
// Resource definition
func resourceSimpleExample() *schema.Resource {
return &schema.Resource{
Create: resourceSimpleExampleCreate,
Read: resourceSimpleExampleRead,
Update: resourceSimpleExampleUpdate,
Delete: resourceSimpleExampleDelete,
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
},
},
}
}
// Create operation
func resourceSimpleExampleCreate(d *schema.ResourceData, m interface{}) error {
name := d.Get("name").(string)
resp, err := http.Post(fmt.Sprintf("%s/create?name=%s", apiURL, name), "application/json", nil)
if err != nil {
return fmt.Errorf("Error creating resource: %s", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("Error creating resource, status code: %d", resp.StatusCode)
}
body, _ := ioutil.ReadAll(resp.Body)
var result map[string]string
if err := json.Unmarshal(body, &result); err != nil {
return fmt.Errorf("Error parsing response: %s", err)
}
d.SetId(result["id"])
return nil
}
// Read operation
func resourceSimpleExampleRead(d *schema.ResourceData, m interface{}) error {
id := d.Id()
resp, err := http.Get(fmt.Sprintf("%s/read?id=%s", apiURL, id))
if err != nil {
return fmt.Errorf("Error reading resource: %s", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Error reading resource, status code: %d", resp.StatusCode)
}
body, _ := ioutil.ReadAll(resp.Body)
var result map[string]string
if err := json.Unmarshal(body, &result); err != nil {
return fmt.Errorf("Error parsing response: %s", err)
}
d.Set("name", result["name"])
return nil
}
// Update operation
func resourceSimpleExampleUpdate(d *schema.ResourceData, m interface{}) error {
id := d.Id()
name := d.Get("name").(string)
req, err := http.NewRequest("PUT", fmt.Sprintf("%s/update?id=%s&name=%s", apiURL, id, name), nil)
if err != nil {
return fmt.Errorf("Error creating update request: %s", err)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("Error updating resource: %s", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Error updating resource, status code: %d", resp.StatusCode)
}
return nil
}
// Delete operation
func resourceSimpleExampleDelete(d *schema.ResourceData, m interface{}) error {
id := d.Id()
req, err := http.NewRequest("DELETE", fmt.Sprintf("%s/delete?id=%s", apiURL, id), nil)
if err != nil {
return fmt.Errorf("Error creating delete request: %s", err)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("Error deleting resource: %s", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("Error deleting resource, status code: %d", resp.StatusCode)
}
d.SetId("")
return nil
}
func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: provider,
})
}
Key Elements of the Code:
- Provider Definition (
func provider()
): This sets up the provider and tells Terraform how to interact with the mock API. In this case, it’s thesimpleexample_resource
. - Resource Definition (
func resourceSimpleExample()
): Defines thename
schema required for creating a resource and the CRUD operations (Create
,Read
,Update
,Delete
) for the resource. - CRUD Functions:
- Create: Sends a
POST
request to create the resource. - Read: Sends a
GET
request to fetch the resource’s current state. - Update: Sends a
PUT
request to modify the resource. - Delete: Sends a
DELETE
request to remove the resource.
Writing the Terraform Configuration File
Now that you have the Go code for the provider, let’s take a look at the Terraform configuration file (main.tf
) that will use this provider.
provider "simpleexample" {}
resource "simpleexample_resource" "example" {
name = "test-resource"
}
Explanation:
provider "simpleexample" {}
: Defines the provider and tells Terraform we’re using the custom providersimpleexample
.resource "simpleexample_resource" "example" { ... }
: Defines a resource of typesimpleexample_resource
with the nameexample
. Thename
field specifies the resource name as"test-resource"
, which will be sent to the mock API when created.
Why Build a Custom Terraform Provider?
A provider allows you to automate the management of resources outside the standard cloud providers like AWS, Azure, or Google Cloud. This is particularly useful if you’re working with custom APIs, internal systems, or want to learn how Terraform interacts with external services.
Conclusion
Building a custom provider extends Terraform’s functionality to fit your needs. It lets you manage virtually any service or API with Terraform’s powerful automation. By following this guide, you learned how to create a simple provider, interact with APIs using CRUD operations, and manage resources with Terraform.