Terraform
TerraformIntermediate

Creating Custom Terraform Providers - A Beginner's Guide

DevHub Team
4 min read
IaCDevOpsGoProvider Development

TL;DR

Learn how to develop your own Terraform provider to extend Terraform's capabilities

Creating Custom Terraform Providers

Learn how to develop your own Terraform provider to extend Terraform's functionality and integrate with custom services.

$1

Terraform providers are plugins that enable Terraform to manage resources in various platforms and services. While HashiCorp and the community maintain many providers, you might need to create a custom provider for:

1. Internal services

2. Proprietary platforms

3. Unsupported APIs

4. Specialized requirements

$1

Before creating a custom provider, you need:

``bash

Install Go

brew install go

Install Terraform

brew install terraform

Set up Go workspace

mkdir -p $GOPATH/src/github.com/yourusername/terraform-provider-example

cd $GOPATH/src/github.com/yourusername/terraform-provider-example

`

$1

A typical provider project structure:

`

terraform-provider-example/

├── main.go

├── provider/

│ ├── provider.go

│ ├── resource_example.go

│ └── data_source_example.go

├── examples/

│ └── main.tf

└── go.mod

`

$1

$1

`go

// provider/provider.go

package provider

import (

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

)

func Provider() *schema.Provider {

return &schema.Provider{

ResourcesMap: map[string]*schema.Resource{

"example_resource": resourceExample(),

},

DataSourcesMap: map[string]*schema.Resource{

"example_data": dataSourceExample(),

},

Schema: map[string]*schema.Schema{

"api_token": {

Type: schema.TypeString,

Required: true,

Sensitive: true,

},

},

}

}

`

$1

`go

// provider/resource_example.go

package provider

import (

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

)

func resourceExample() *schema.Resource {

return &schema.Resource{

Create: resourceExampleCreate,

Read: resourceExampleRead,

Update: resourceExampleUpdate,

Delete: resourceExampleDelete,

Schema: map[string]*schema.Schema{

"name": {

Type: schema.TypeString,

Required: true,

},

"description": {

Type: schema.TypeString,

Optional: true,

},

},

}

}

func resourceExampleCreate(d *schema.ResourceData, m interface{}) error {

// Implementation for creating a resource

name := d.Get("name").(string)

// API calls to create resource

d.SetId("generated-id")

return nil

}

`

$1

`go

// provider/data_source_example.go

package provider

import (

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

)

func dataSourceExample() *schema.Resource {

return &schema.Resource{

Read: dataSourceExampleRead,

Schema: map[string]*schema.Schema{

"name": {

Type: schema.TypeString,

Required: true,

},

"value": {

Type: schema.TypeString,

Computed: true,

},

},

}

}

`

$1

`go

// main.go

package main

import (

"github.com/hashicorp/terraform-plugin-sdk/v2/plugin"

"github.com/yourusername/terraform-provider-example/provider"

)

func main() {

plugin.Serve(&plugin.ServeOpts{

ProviderFunc: provider.Provider,

})

}

`

$1

$1

`go

// provider/resource_example_test.go

package provider

import (

"testing"

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

)

func TestAccExampleResource_basic(t *testing.T) {

resource.Test(t, resource.TestCase{

PreCheck: func() { testAccPreCheck(t) },

Providers: testAccProviders,

Steps: []resource.TestStep{

{

Config: testAccExampleResourceConfig_basic,

Check: resource.ComposeTestCheckFunc(

resource.TestCheckResourceAttr(

"example_resource.test", "name", "test"),

),

},

},

})

}

`

$1

`hcl

Create test configuration

provider "example" {

api_token = "test-token"

}

resource "example_resource" "test" {

name = "test-resource"

description = "Test resource description"

}

`

$1

`bash

Build the provider

go build -o terraform-provider-example

Install locally

mkdir -p ~/.terraform.d/plugins/registry.terraform.io/yourusername/example/1.0.0/darwin_amd64

cp terraform-provider-example ~/.terraform.d/plugins/registry.terraform.io/yourusername/example/1.0.0/darwin_amd64/

`

$1

`hcl

terraform {

required_providers {

example = {

source = "yourusername/example"

version = "1.0.0"

}

}

}

provider "example" {

api_token = var.api_token

}

resource "example_resource" "my_resource" {

name = "custom-resource"

description = "Created with custom provider"

}

`

$1

1. Error Handling

`go

if err != nil {

return fmt.Errorf("error creating resource: %s", err)

}

`

2. Resource Validation

`go

Schema: map[string]*schema.Schema{

"port": {

Type: schema.TypeInt,

Required: true,

ValidateFunc: validation.IntBetween(1, 65535),

},

}

`

3. Documentation

`go

// Add descriptions to schema fields

Schema: map[string]*schema.Schema{

"name": {

Type: schema.TypeString,

Required: true,

Description: "The name of the resource",

},

}

`

$1

$1

`go

func validateName(v interface{}, k string) (ws []string, es []error) {

value := v.(string)

if len(value) > 50 {

es = append(es, fmt.Errorf("name cannot be longer than 50 characters"))

}

return

}

`

$1

`go

func resourceExampleV0() *schema.Resource {

return &schema.Resource{

Schema: map[string]*schema.Schema{

"old_field": {

Type: schema.TypeString,

Required: true,

},

},

}

}

func resourceExampleStateUpgradeV0(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) {

// Migration logic

return rawState, nil

}

`

$1

1. Enable Logging

`bash

export TF_LOG=DEBUG

export TF_LOG_PATH=terraform.log

`

2. Use Debugger

`go

import "log"

log.Printf("[DEBUG] Resource data: %#v", d)

``

$1

1. Create a GitHub repository

2. Tag releases with semantic versioning

3. Document usage and examples

4. Submit to Terraform Registry (optional)

$1

Creating a custom provider allows you to:

  • Extend Terraform's capabilities
  • Integrate with internal services
  • Maintain consistency in infrastructure
  • Automate custom workflows
  • Remember to:

    1. Follow Go best practices

    2. Handle errors gracefully

    3. Write comprehensive tests

    4. Document your provider

    5. Maintain backward compatibility

    Why This Matters

    Understanding the business and technical context helps you make informed decisions rather than blindly following patterns.

    Trade-offs to Consider

    Every architectural decision involves trade-offs. Consider your specific requirements, team expertise, and scale when evaluating options.

    When NOT to Use This

    Knowing when a solution doesn't apply is as valuable as knowing when it does. Consider alternatives for your specific situation.

    Decision Framework

    Use this framework to evaluate whether this approach is right for your use case based on your specific constraints and requirements.