Skip to main content

Command Palette

Search for a command to run...

5.5 Custom Terraform Providers: How to Build Your Own

Updated
6 min read
5.5 Custom Terraform Providers: How to Build Your Own

Level Up Your Infrastructure: How to Build Your Own Custom Terraform Provider

Terraform is awesome for managing infrastructure as code. It lets you define what you want your infrastructure to look like, and Terraform makes it happen. But what if you need to manage something that Terraform doesn't natively support? That's where custom Terraform providers come in!

Think of Terraform providers like adapters for your infrastructure. You have a bunch of different devices (databases, VMs, cloud services) that all speak different languages. Terraform needs adapters to talk to each one. Standard providers, like AWS or Azure, are like universal adapters. But sometimes, you need a special adapter for a unique device – that's your custom provider!

This post will guide you through the basics of building your own custom Terraform provider. Don't worry, we'll keep it simple and practical.

Why Build a Custom Provider?

You might need a custom provider if:

  • You're using an internal service: Your company has its own home-grown platform that Terraform doesn't know about.

  • The official provider is lacking: The existing provider doesn't support a specific feature or resource you need.

  • You want to abstract away complexity: You want to simplify complex interactions with an API into a manageable Terraform resource.

The Basic Ingredients

Building a custom provider requires some Go programming knowledge. Here's a high-level overview of what's involved:

  1. Go Programming: You'll be writing the provider in Go.

  2. Terraform Plugin SDK: This SDK provides the libraries and tools you need to build a Terraform provider.

  3. Provider Schema: This defines the attributes and data types your provider supports. Think of it as the blueprint for your custom adapter.

  4. Resource Definition: This is where you define how Terraform interacts with your specific resource (e.g., create, read, update, delete). This is the "translation" process between Terraform's language and the service's language.

Let's Get Practical: Building a Simple "Dummy" Provider

Let's imagine we need to manage "Greeters." A Greeter is a simple resource that stores a greeting message. This is a trivial example, but it illustrates the core concepts.

1. Setting up your Go environment and project:

You'll need to set up your Go development environment and create a new project. This involves installing Go, setting up your GOPATH, and creating a new directory for your provider. Consult the Terraform Plugin SDK documentation for detailed instructions.

2. Define the Provider Schema:

The schema defines what configuration options the user needs to provide when using the provider. For our Greeter provider, we might need an api_endpoint for our (imaginary) Greeter service.

package main

import (
    "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func provider() *schema.Provider {
    return &schema.Provider{
        Schema: map[string]*schema.Schema{
            "api_endpoint": {
                Type:        schema.TypeString,
                Required:    true,
                Description: "The endpoint of the Greeter API",
            },
        },

        ResourcesMap: map[string]*schema.Resource{
            "greeter": resourceGreeter(), //We will define resourceGreeter below
        },

        ConfigureFunc: configureProvider, //We will define configureProvider below
    }
}

func configureProvider(d *schema.ResourceData) (interface{}, error) {
    apiEndpoint := d.Get("api_endpoint").(string)

    // In a real-world scenario, you'd use the apiEndpoint
    // to create a client to interact with your service.
    // For this example, we'll just return the endpoint.
    return apiEndpoint, nil
}

3. Define the Resource Schema and CRUD Operations:

Now, let's define the Greeter resource. This involves specifying its attributes (e.g., message) and how Terraform interacts with it (create, read, update, delete – often referred to as CRUD).

package main

import (
    "fmt"

    "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func resourceGreeter() *schema.Resource {
    return &schema.Resource{
        Schema: map[string]*schema.Schema{
            "message": {
                Type:        schema.TypeString,
                Required:    true,
                Description: "The greeting message",
            },
            "id": {
                Type:        schema.TypeString,
                Computed:    true,
                Description: "Unique ID for the greeter.",
            },
        },

        Create: resourceGreeterCreate,
        Read:   resourceGreeterRead,
        Update: resourceGreeterUpdate,
        Delete: resourceGreeterDelete,
    }
}

func resourceGreeterCreate(d *schema.ResourceData, m interface{}) error {
    message := d.Get("message").(string)
    apiEndpoint := m.(string) // Get apiEndpoint from ConfigureFunc

    // In a real application, we would POST the message to the greeter API endpoint and get a unique ID
    // For this example, we simply set the id as a combination of the endpoint and message

    id := fmt.Sprintf("%s-%s", apiEndpoint, message)

    d.SetId(id)
    return resourceGreeterRead(d, m)
}

func resourceGreeterRead(d *schema.ResourceData, m interface{}) error {
    //In a real world case, we would use the API to read the greeter's details
    //For this example, we return the state that terraform manages

    return nil
}

func resourceGreeterUpdate(d *schema.ResourceData, m interface{}) error {
    //In a real world case, we would use the API to update the greeter's details
    //For this example, we call the create function
    return resourceGreeterCreate(d, m)
}

func resourceGreeterDelete(d *schema.ResourceData, m interface{}) error {
    //In a real world case, we would use the API to delete the greeter
    //For this example, we do nothing
    d.SetId("") // Remove resource from Terraform state

    return nil
}

4. Implement the CRUD Functions:

The Create, Read, Update, and Delete functions are where you define the logic for interacting with your underlying service. In our example, these functions are placeholders. In a real-world scenario, these would involve making API calls to your service to create, read, update, and delete the Greeter resource.

5. Build and Test Your Provider:

Use the go build command to build your provider. Then, you can test it by writing Terraform configurations that use your provider.

Using Your Custom Provider

Once your provider is built, you can use it in your Terraform configuration like this:

terraform {
  required_providers {
    greeter = {
      source  = "example.com/acme/greeter" # Replace with your provider's source
      version = "1.0.0" # Replace with your provider's version
    }
  }
}

provider "greeter" {
  api_endpoint = "https://api.example.com/greeter"
}

resource "greeter" "my_greeter" {
  message = "Hello, Terraform!"
}

Challenge and Solution: State Management

One common challenge is managing the state of your resources correctly. Terraform relies on its state file to track the resources it manages. If the state becomes inconsistent with the actual infrastructure, things can go wrong.

Solution:

  • Return complete and accurate state: In your Read function, make sure you fetch all relevant data from your service and update the Terraform state with the latest values.

  • Handle errors gracefully: If an error occurs during a Create, Read, Update, or Delete operation, handle it properly and return an error to Terraform. This will prevent the state from becoming corrupted.

  • Consider resource immutability: If a resource is fundamentally immutable, the Update function should recreate the resource.

Simplified Architecture Diagram

+---------------------+      +-----------------------+      +---------------------------+
| Terraform CLI       |----->| Custom Greeter Provider|----->| Your Greeter API Service   |
+---------------------+      +-----------------------+      +---------------------------+
    |                          |   (Go + Terraform SDK)  |      |                           |
    |                          |                       |      |                           |
    v                          |                       |      |                           |
+---------------------+      +-----------------------+      +---------------------------+
| Terraform State File|      |                       |      |                           |
+---------------------+      +-----------------------+      +---------------------------+

Conclusion

Building custom Terraform providers can seem daunting at first, but it unlocks a powerful way to manage infrastructure beyond what standard providers offer. Start with simple resources like our "Greeter" example, and gradually build up your knowledge. The Terraform Plugin SDK documentation is your best friend! With a little effort, you can tailor Terraform to manage almost anything. Remember to focus on solid state management and clear error handling. Good luck!

More from this blog

TechZen

136 posts