Create Your First Terraform Provider: A Step-by-Step Guide

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!

Create Your First Terraform Provider: A Step-by-Step Guide - LitFeeds

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:

  1. Provider Definition (func provider()): This sets up the provider and tells Terraform how to interact with the mock API. In this case, it’s the simpleexample_resource.
  2. Resource Definition (func resourceSimpleExample()): Defines the name schema required for creating a resource and the CRUD operations (Create, Read, Update, Delete) for the resource.
  3. 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 provider simpleexample.
  • resource "simpleexample_resource" "example" { ... }: Defines a resource of type simpleexample_resource with the name example. The name 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

Related Article

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.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *