Go e Terraform: Infrastructure as Code com Go

O Terraform revolucionou a forma como gerenciamos infraestrutura, permitindo definir recursos como código. Embora o Terraform use sua própria linguagem de configuração (HCL), a linguagem Go é fundamental para estender suas capacidades através de providers personalizados.

Neste guia completo, você aprenderá a criar providers Terraform em Go, integrar Terraform com aplicações Go, e automatizar infraestrutura usando as melhores práticas de produção.

Índice

  1. Por que Go e Terraform?
  2. Arquitetura de Providers Terraform
  3. Criando seu Primeiro Provider
  4. Gerenciando Recursos com Go
  5. Testando Providers
  6. Integração com Aplicações Go
  7. Padrões de Produção
  8. Deploy e Distribuição

Por que Go e Terraform?

Vantagens da Combinação

1. Performance Nativa Providers Terraform são executados como binários nativos, e Go oferece excelente performance com compilação estática.

2. Ecossistema Rico O próprio Terraform é escrito em Go, o que significa que você pode estudar o código-fonte e aprender com os melhores.

3. Bibliotecas Cloud Maturas Go possui SDKs oficiais para AWS, Azure, GCP, Kubernetes e praticamente qualquer serviço cloud.

4. Binário Único Providers compilados em Go são binários auto-contidos, facilitando distribuição e execução.

// Vantagem: provider como binário único
// terraform-provider-example v1.0.0
// ├── Não requer runtime
// ├── Cross-platform (Linux, macOS, Windows)
// └── Baixo consumo de memória

Casos de Uso Comuns

  • Providers Customizados: Integrar APIs internas ou serviços específicos
  • Módulos Reutilizáveis: Criar abstrações para sua organização
  • Automação: Executar Terraform a partir de aplicações Go
  • Validação: Implementar políticas e validações customizadas

Arquitetura de Providers Terraform

Componentes Principais

Um provider Terraform consiste em três componentes principais:

┌─────────────────────────────────────┐
│         Terraform Core              │
│   (Escrito em Go - Open Source)     │
└─────────────┬───────────────────────┘
              │ gRPC
┌─────────────────────────────────────┐
│      Terraform Provider             │
│   (Seu código em Go)                │
│                                     │
│  • Provider Configuration          │
│  • Data Sources                    │
│  • Resources                       │
└─────────────┬───────────────────────┘
              │ HTTP/HTTPS
┌─────────────────────────────────────┐
│      API do Serviço                 │
│   (AWS, Azure, sua API, etc)       │
└─────────────────────────────────────┘

SDK do Terraform

O Terraform Provider SDK v2 é a biblioteca oficial para criar providers:

go get github.com/hashicorp/terraform-plugin-sdk/v2

Estrutura de um Provider:

package main

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

func main() {
    plugin.Serve(&plugin.ServeOpts{
        ProviderFunc: func() *schema.Provider {
            return &schema.Provider{
                // Configuração do provider
                Schema: map[string]*schema.Schema{
                    "api_key": {
                        Type:        schema.TypeString,
                        Required:    true,
                        Sensitive:   true,
                        Description: "API key for authentication",
                    },
                },
                // Recursos gerenciados
                ResourcesMap: map[string]*schema.Resource{
                    "example_server": resourceServer(),
                },
                // Data sources
                DataSourcesMap: map[string]*schema.Resource{
                    "example_regions": dataSourceRegions(),
                },
                ConfigureContextFunc: providerConfigure,
            }
        },
    })
}

Criando seu Primeiro Provider

Estrutura do Projeto

terraform-provider-example/
├── main.go                    # Entry point
├── go.mod                     # Módulo Go
├── go.sum                     # Checksums
├── example/                   # Configurações de exemplo
│   └── main.tf
├── internal/
│   ├── provider/
│   │   ├── provider.go        # Configuração do provider
│   │   ├── resource_server.go # Resource exemplo
│   │   └── data_source.go     # Data source exemplo
│   └── client/
│       └── api.go             # Cliente HTTP da API
└── Makefile                   # Build automation

Implementação Completa

1. Configuração do Provider (internal/provider/provider.go)

package provider

import (
    "context"
    "os"

    "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
    "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    "terraform-provider-example/internal/client"
)

func New() *schema.Provider {
    return &schema.Provider{
        Schema: map[string]*schema.Schema{
            "host": {
                Type:        schema.TypeString,
                Optional:    true,
                DefaultFunc: schema.EnvDefaultFunc("EXAMPLE_HOST", "https://api.example.com"),
                Description: "URL base da API",
            },
            "api_key": {
                Type:        schema.TypeString,
                Required:    true,
                Sensitive:   true,
                DefaultFunc: schema.EnvDefaultFunc("EXAMPLE_API_KEY", nil),
                Description: "API Key para autenticação",
            },
            "timeout": {
                Type:        schema.TypeInt,
                Optional:    true,
                Default:     30,
                Description: "Timeout em segundos para requisições",
            },
        },
        ResourcesMap: map[string]*schema.Resource{
            "example_server": resourceServer(),
            "example_database": resourceDatabase(),
        },
        DataSourcesMap: map[string]*schema.Resource{
            "example_regions": dataSourceRegions(),
        },
        ConfigureContextFunc: configureProvider,
    }
}

func configureProvider(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
    var diags diag.Diagnostics

    host := d.Get("host").(string)
    apiKey := d.Get("api_key").(string)
    timeout := d.Get("timeout").(int)

    // Validação
    if apiKey == "" {
        diags = append(diags, diag.Diagnostic{
            Severity: diag.Error,
            Summary:  "API Key necessária",
            Detail:   "A API key deve ser fornecida via configuração ou variável de ambiente EXAMPLE_API_KEY",
        })
        return nil, diags
    }

    // Cria cliente da API
    config := client.Config{
        Host:    host,
        APIKey:  apiKey,
        Timeout: time.Duration(timeout) * time.Second,
    }

    c, err := client.New(config)
    if err != nil {
        diags = append(diags, diag.Diagnostic{
            Severity: diag.Error,
            Summary:  "Falha ao criar cliente",
            Detail:   err.Error(),
        })
        return nil, diags
    }

    return c, diags
}

2. Cliente HTTP (internal/client/api.go)

package client

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

type Config struct {
    Host    string
    APIKey  string
    Timeout time.Duration
}

type Client struct {
    httpClient *http.Client
    config     Config
}

type Server struct {
    ID       string            `json:"id"`
    Name     string            `json:"name"`
    Region   string            `json:"region"`
    Size     string            `json:"size"`
    Status   string            `json:"status"`
    Metadata map[string]string `json:"metadata"`
}

func New(config Config) (*Client, error) {
    if config.Host == "" {
        return nil, fmt.Errorf("host é obrigatório")
    }

    return &Client{
        httpClient: &http.Client{
            Timeout: config.Timeout,
        },
        config: config,
    }, nil
}

func (c *Client) CreateServer(ctx context.Context, server *Server) (*Server, error) {
    body, err := json.Marshal(server)
    if err != nil {
        return nil, err
    }

    req, err := http.NewRequestWithContext(ctx, "POST", 
        c.config.Host+"/servers", bytes.NewReader(body))
    if err != nil {
        return nil, err
    }

    req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
    req.Header.Set("Content-Type", "application/json")

    resp, err := c.httpClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusCreated {
        return nil, fmt.Errorf("falha ao criar servidor: %s", resp.Status)
    }

    var result Server
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, err
    }

    return &result, nil
}

func (c *Client) GetServer(ctx context.Context, id string) (*Server, error) {
    req, err := http.NewRequestWithContext(ctx, "GET",
        c.config.Host+"/servers/"+id, nil)
    if err != nil {
        return nil, err
    }

    req.Header.Set("Authorization", "Bearer "+c.config.APIKey)

    resp, err := c.httpClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode == http.StatusNotFound {
        return nil, nil
    }

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("falha ao obter servidor: %s", resp.Status)
    }

    var server Server
    if err := json.NewDecoder(resp.Body).Decode(&server); err != nil {
        return nil, err
    }

    return &server, nil
}

func (c *Client) UpdateServer(ctx context.Context, server *Server) error {
    body, err := json.Marshal(server)
    if err != nil {
        return err
    }

    req, err := http.NewRequestWithContext(ctx, "PUT",
        c.config.Host+"/servers/"+server.ID, bytes.NewReader(body))
    if err != nil {
        return err
    }

    req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
    req.Header.Set("Content-Type", "application/json")

    resp, err := c.httpClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("falha ao atualizar servidor: %s", resp.Status)
    }

    return nil
}

func (c *Client) DeleteServer(ctx context.Context, id string) error {
    req, err := http.NewRequestWithContext(ctx, "DELETE",
        c.config.Host+"/servers/"+id, nil)
    if err != nil {
        return err
    }

    req.Header.Set("Authorization", "Bearer "+c.config.APIKey)

    resp, err := c.httpClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
        return fmt.Errorf("falha ao deletar servidor: %s", resp.Status)
    }

    return nil
}

3. Resource (internal/provider/resource_server.go)

package provider

import (
    "context"
    "time"

    "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
    "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
    "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    "terraform-provider-example/internal/client"
)

func resourceServer() *schema.Resource {
    return &schema.Resource{
        Description: "Gerencia servidores na plataforma Example Cloud",

        CreateContext: resourceServerCreate,
        ReadContext:   resourceServerRead,
        UpdateContext: resourceServerUpdate,
        DeleteContext: resourceServerDelete,

        Importer: &schema.ResourceImporter{
            StateContext: schema.ImportStatePassthroughContext,
        },

        Timeouts: &schema.ResourceTimeout{
            Create: schema.DefaultTimeout(10 * time.Minute),
            Update: schema.DefaultTimeout(10 * time.Minute),
            Delete: schema.DefaultTimeout(10 * time.Minute),
        },

        Schema: map[string]*schema.Schema{
            "id": {
                Description: "ID único do servidor",
                Type:        schema.TypeString,
                Computed:    true,
            },
            "name": {
                Description: "Nome do servidor",
                Type:        schema.TypeString,
                Required:    true,
            },
            "region": {
                Description: "Região onde o servidor será criado",
                Type:        schema.TypeString,
                Required:    true,
                ForceNew:    true, // Mudança requer recriação
            },
            "size": {
                Description: "Tamanho do servidor (small, medium, large)",
                Type:        schema.TypeString,
                Required:    true,
                ValidateFunc: validation.StringInSlice([]string{
                    "small", "medium", "large",
                }, false),
            },
            "status": {
                Description: "Status atual do servidor",
                Type:        schema.TypeString,
                Computed:    true,
            },
            "metadata": {
                Description: "Metadados customizados",
                Type:        schema.TypeMap,
                Optional:    true,
                Elem:        &schema.Schema{Type: schema.TypeString},
            },
            "created_at": {
                Description: "Data de criação",
                Type:        schema.TypeString,
                Computed:    true,
            },
        },
    }
}

func resourceServerCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
    c := meta.(*client.Client)

    server := &client.Server{
        Name:   d.Get("name").(string),
        Region: d.Get("region").(string),
        Size:   d.Get("size").(string),
    }

    if v, ok := d.GetOk("metadata"); ok {
        metadata := make(map[string]string)
        for k, v := range v.(map[string]interface{}) {
            metadata[k] = v.(string)
        }
        server.Metadata = metadata
    }

    created, err := c.CreateServer(ctx, server)
    if err != nil {
        return diag.FromErr(err)
    }

    d.SetId(created.ID)

    // Aguarda o servidor ficar pronto
    createStateConf := &resource.StateChangeConf{
        Pending: []string{"creating", "pending"},
        Target:  []string{"running", "active"},
        Refresh: func() (interface{}, string, error) {
            s, err := c.GetServer(ctx, created.ID)
            if err != nil {
                return nil, "", err
            }
            if s == nil {
                return nil, "", fmt.Errorf("servidor não encontrado após criação")
            }
            return s, s.Status, nil
        },
        Timeout:    d.Timeout(schema.TimeoutCreate),
        Delay:      10 * time.Second,
        MinTimeout: 5 * time.Second,
    }

    _, err = createStateConf.WaitForStateContext(ctx)
    if err != nil {
        return diag.FromErr(err)
    }

    return resourceServerRead(ctx, d, meta)
}

func resourceServerRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
    c := meta.(*client.Client)
    var diags diag.Diagnostics

    server, err := c.GetServer(ctx, d.Id())
    if err != nil {
        return diag.FromErr(err)
    }

    if server == nil {
        d.SetId("")
        return diags
    }

    d.Set("name", server.Name)
    d.Set("region", server.Region)
    d.Set("size", server.Size)
    d.Set("status", server.Status)
    d.Set("metadata", server.Metadata)

    return diags
}

func resourceServerUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
    c := meta.(*client.Client)

    server := &client.Server{
        ID:     d.Id(),
        Name:   d.Get("name").(string),
        Region: d.Get("region").(string),
        Size:   d.Get("size").(string),
    }

    if v, ok := d.GetOk("metadata"); ok {
        metadata := make(map[string]string)
        for k, v := range v.(map[string]interface{}) {
            metadata[k] = v.(string)
        }
        server.Metadata = metadata
    }

    if err := c.UpdateServer(ctx, server); err != nil {
        return diag.FromErr(err)
    }

    return resourceServerRead(ctx, d, meta)
}

func resourceServerDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
    c := meta.(*client.Client)
    var diags diag.Diagnostics

    if err := c.DeleteServer(ctx, d.Id()); err != nil {
        return diag.FromErr(err)
    }

    d.SetId("")

    return diags
}

Gerenciando Recursos com Go

Ciclo de Vida de Recursos

O Terraform segue um ciclo de vida bem definido:

terraform apply
┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   Create()   │────▶│   Read()     │◀────│   Update()   │
│  (Criação)   │     │  (Leitura)   │     │ (Atualização)│
└──────────────┘     └──────────────┘     └──────────────┘
                                            ┌──────────────┐
                                            │   Delete()   │
                                            │  (Remoção)   │
                                            └──────────────┘

Implementando Retry e Backoff

// retry.go - Utilitários para retry com backoff exponencial
package provider

import (
    "context"
    "math"
    "time"
)

type RetryConfig struct {
    MaxRetries  int
    BaseDelay   time.Duration
    MaxDelay    time.Duration
    Retryable   func(error) bool
}

func retryWithBackoff(ctx context.Context, config RetryConfig, fn func() error) error {
    var err error
    
    for attempt := 0; attempt < config.MaxRetries; attempt++ {
        err = fn()
        if err == nil {
            return nil
        }
        
        if !config.Retryable(err) {
            return err
        }
        
        // Calcula delay com backoff exponencial
        delay := time.Duration(math.Pow(2, float64(attempt))) * config.BaseDelay
        if delay > config.MaxDelay {
            delay = config.MaxDelay
        }
        
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-time.After(delay):
            continue
        }
    }
    
    return err
}

// Uso no resource
func resourceServerCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
    c := meta.(*client.Client)
    
    err := retryWithBackoff(ctx, RetryConfig{
        MaxRetries: 3,
        BaseDelay:  1 * time.Second,
        MaxDelay:   30 * time.Second,
        Retryable: func(err error) bool {
            // Retry em erros temporários
            return isTemporaryError(err)
        },
    }, func() error {
        _, err := c.CreateServer(ctx, server)
        return err
    })
    
    if err != nil {
        return diag.FromErr(err)
    }
    
    // ... resto do código
}

Testando Providers

Testes de Aceitação

O SDK do Terraform fornece ferramentas para testes de aceitação:

// provider_test.go
package provider

import (
    "os"
    "testing"

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

var testAccProvider *schema.Provider

func init() {
    testAccProvider = New()
}

func TestProvider(t *testing.T) {
    if err := New().InternalValidate(); err != nil {
        t.Fatalf("erro na validação interna: %s", err)
    }
}

func testAccPreCheck(t *testing.T) {
    if v := os.Getenv("EXAMPLE_API_KEY"); v == "" {
        t.Fatal("EXAMPLE_API_KEY deve ser definida para testes de aceitação")
    }
}

func TestAccResourceServer_basic(t *testing.T) {
    resource.Test(t, resource.TestCase{
        PreCheck:          func() { testAccPreCheck(t) },
        ProviderFactories: testAccProviderFactories,
        CheckDestroy:      testAccCheckServerDestroy,
        Steps: []resource.TestStep{
            {
                Config: testAccResourceServerConfig_basic(),
                Check: resource.ComposeTestCheckFunc(
                    testAccCheckServerExists("example_server.test"),
                    resource.TestCheckResourceAttr("example_server.test", "name", "test-server"),
                    resource.TestCheckResourceAttr("example_server.test", "region", "us-east-1"),
                    resource.TestCheckResourceAttr("example_server.test", "size", "small"),
                    resource.TestCheckResourceAttrSet("example_server.test", "id"),
                    resource.TestCheckResourceAttrSet("example_server.test", "status"),
                ),
            },
            {
                Config: testAccResourceServerConfig_update(),
                Check: resource.ComposeTestCheckFunc(
                    resource.TestCheckResourceAttr("example_server.test", "name", "test-server-updated"),
                    resource.TestCheckResourceAttr("example_server.test", "size", "medium"),
                ),
            },
        },
    })
}

func testAccCheckServerExists(n string) resource.TestCheckFunc {
    return func(s *terraform.State) error {
        rs, ok := s.RootModule().Resources[n]
        if !ok {
            return fmt.Errorf("não encontrado: %s", n)
        }

        if rs.Primary.ID == "" {
            return fmt.Errorf("ID não definido")
        }

        // Verifica se o servidor existe na API
        client := testAccProvider.Meta().(*client.Client)
        _, err := client.GetServer(context.Background(), rs.Primary.ID)
        if err != nil {
            return err
        }

        return nil
    }
}

func testAccCheckServerDestroy(s *terraform.State) error {
    client := testAccProvider.Meta().(*client.Client)

    for _, rs := range s.RootModule().Resources {
        if rs.Type != "example_server" {
            continue
        }

        server, err := client.GetServer(context.Background(), rs.Primary.ID)
        if err != nil {
            return err
        }

        if server != nil {
            return fmt.Errorf("servidor ainda existe: %s", rs.Primary.ID)
        }
    }

    return nil
}

func testAccResourceServerConfig_basic() string {
    return `
provider "example" {
    api_key = "test-api-key"
    host    = "http://localhost:8080"
}

resource "example_server" "test" {
    name   = "test-server"
    region = "us-east-1"
    size   = "small"
    
    metadata = {
        environment = "test"
        managed_by  = "terraform"
    }
}
`
}

func testAccResourceServerConfig_update() string {
    return `
provider "example" {
    api_key = "test-api-key"
    host    = "http://localhost:8080"
}

resource "example_server" "test" {
    name   = "test-server-updated"
    region = "us-east-1"
    size   = "medium"
}
`
}

Executando Testes

# Testes unitários
go test ./...

# Testes de aceitação (requer servidor real ou mock)
TF_ACC=1 go test ./internal/provider/... -v -timeout 120m

# Testes específicos
TF_ACC=1 go test -run TestAccResourceServer_basic -v

Integração com Aplicações Go

Executando Terraform de Go

// terraform_runner.go
package main

import (
    "context"
    "fmt"
    "os"
    "os/exec"
    "path/filepath"
)

type TerraformRunner struct {
    workingDir string
    envVars    map[string]string
}

func NewTerraformRunner(workingDir string) *TerraformRunner {
    return &TerraformRunner{
        workingDir: workingDir,
        envVars:    make(map[string]string),
    }
}

func (r *TerraformRunner) SetEnv(key, value string) {
    r.envVars[key] = value
}

func (r *TerraformRunner) Init(ctx context.Context) error {
    cmd := exec.CommandContext(ctx, "terraform", "init")
    cmd.Dir = r.workingDir
    cmd.Env = r.buildEnv()
    
    output, err := cmd.CombinedOutput()
    if err != nil {
        return fmt.Errorf("terraform init falhou: %w\n%s", err, output)
    }
    
    return nil
}

func (r *TerraformRunner) Plan(ctx context.Context) (string, error) {
    cmd := exec.CommandContext(ctx, "terraform", "plan", "-out=tfplan")
    cmd.Dir = r.workingDir
    cmd.Env = r.buildEnv()
    
    output, err := cmd.CombinedOutput()
    if err != nil {
        return "", fmt.Errorf("terraform plan falhou: %w\n%s", err, output)
    }
    
    return string(output), nil
}

func (r *TerraformRunner) Apply(ctx context.Context) error {
    cmd := exec.CommandContext(ctx, "terraform", "apply", "-auto-approve", "tfplan")
    cmd.Dir = r.workingDir
    cmd.Env = r.buildEnv()
    
    output, err := cmd.CombinedOutput()
    if err != nil {
        return fmt.Errorf("terraform apply falhou: %w\n%s", err, output)
    }
    
    fmt.Printf("Apply output:\n%s\n", output)
    return nil
}

func (r *TerraformRunner) Destroy(ctx context.Context) error {
    cmd := exec.CommandContext(ctx, "terraform", "destroy", "-auto-approve")
    cmd.Dir = r.workingDir
    cmd.Env = r.buildEnv()
    
    output, err := cmd.CombinedOutput()
    if err != nil {
        return fmt.Errorf("terraform destroy falhou: %w\n%s", err, output)
    }
    
    return nil
}

func (r *TerraformRunner) Output(ctx context.Context, name string) (string, error) {
    cmd := exec.CommandContext(ctx, "terraform", "output", "-raw", name)
    cmd.Dir = r.workingDir
    cmd.Env = r.buildEnv()
    
    output, err := cmd.Output()
    if err != nil {
        return "", err
    }
    
    return string(output), nil
}

func (r *TerraformRunner) buildEnv() []string {
    env := os.Environ()
    for k, v := range r.envVars {
        env = append(env, fmt.Sprintf("%s=%s", k, v))
    }
    return env
}

// Uso em aplicação
func deployInfrastructure(ctx context.Context, config Config) error {
    runner := NewTerraformRunner("./terraform")
    runner.SetEnv("TF_VAR_api_key", config.APIKey)
    runner.SetEnv("TF_VAR_region", config.Region)
    
    if err := runner.Init(ctx); err != nil {
        return err
    }
    
    plan, err := runner.Plan(ctx)
    if err != nil {
        return err
    }
    
    fmt.Println("Planned changes:", plan)
    
    if err := runner.Apply(ctx); err != nil {
        return err
    }
    
    serverID, err := runner.Output(ctx, "server_id")
    if err != nil {
        return err
    }
    
    fmt.Printf("Servidor criado: %s\n", serverID)
    return nil
}

Padrões de Produção

1. Validação e Normalização

func validateRegion(val interface{}, key string) (warns []string, errs []error) {
    validRegions := []string{
        "us-east-1", "us-west-2", "eu-west-1",
        "ap-southeast-1", "sa-east-1",
    }
    
    region := val.(string)
    for _, valid := range validRegions {
        if region == valid {
            return
        }
    }
    
    errs = append(errs, fmt.Errorf(
        "%q deve ser uma região válida: %v",
        key, validRegions,
    ))
    return
}

// Uso no schema
"region": {
    Type:         schema.TypeString,
    Required:     true,
    ValidateFunc: validateRegion,
}

2. Logging e Observabilidade

import "github.com/hashicorp/terraform-plugin-log/tflog"

func resourceServerCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
    tflog.Info(ctx, "Criando servidor", map[string]interface{}{
        "name":   d.Get("name"),
        "region": d.Get("region"),
    })
    
    // ... código de criação
    
    tflog.Debug(ctx, "Servidor criado com sucesso", map[string]interface{}{
        "id":     created.ID,
        "status": created.Status,
    })
    
    return resourceServerRead(ctx, d, meta)
}

3. Tratamento de Erros

func handleAPIError(err error) diag.Diagnostics {
    var diags diag.Diagnostics
    
    switch e := err.(type) {
    case *client.AuthError:
        diags = append(diags, diag.Diagnostic{
            Severity: diag.Error,
            Summary:  "Erro de autenticação",
            Detail:   "Verifique sua API key",
        })
    case *client.RateLimitError:
        diags = append(diags, diag.Diagnostic{
            Severity: diag.Error,
            Summary:  "Rate limit excedido",
            Detail:   fmt.Sprintf("Aguarde %v antes de tentar novamente", e.RetryAfter),
        })
    case *client.NotFoundError:
        // Recurso não existe mais - limpa state
        return nil
    default:
        diags = append(diags, diag.FromErr(err)...)
    }
    
    return diags
}

Deploy e Distribuição

Build e Release

# Makefile
VERSION ?= 1.0.0
BINARY := terraform-provider-example
OS_ARCH := $(shell go env GOOS)_$(shell go env GOARCH)

build:
	go build -o bin/$(BINARY)_v$(VERSION)

install: build
	mkdir -p ~/.terraform.d/plugins/registry.terraform.io/example/example/$(VERSION)/$(OS_ARCH)
	cp bin/$(BINARY)_v$(VERSION) ~/.terraform.d/plugins/registry.terraform.io/example/example/$(VERSION)/$(OS_ARCH)/$(BINARY)_v$(VERSION)

release:
	GOOS=darwin GOARCH=amd64 go build -o bin/$(BINARY)_v$(VERSION)_darwin_amd64
	GOOS=darwin GOARCH=arm64 go build -o bin/$(BINARY)_v$(VERSION)_darwin_arm64
	GOOS=linux GOARCH=amd64 go build -o bin/$(BINARY)_v$(VERSION)_linux_amd64
	GOOS=linux GOARCH=arm64 go build -o bin/$(BINARY)_v$(VERSION)_linux_arm64
	GOOS=windows GOARCH=amd64 go build -o bin/$(BINARY)_v$(VERSION)_windows_amd64.exe

test:
	go test ./... -v

testacc:
	TF_ACC=1 go test ./internal/provider/... -v -timeout 120m

.PHONY: build install release test testacc

Registro no Terraform Registry

Para publicar seu provider no Terraform Registry:

  1. Crie um repositório GitHub público
  2. Use tags semânticas (v1.0.0)
  3. Crie releases GitHub com binários
  4. Siga o formato de naming: terraform-provider-<nome>
  5. Documente no README
# Uso por consumidores
terraform {
  required_providers {
    example = {
      source  = "example/example"
      version = "~> 1.0"
    }
  }
}

provider "example" {
  api_key = var.example_api_key
}

resource "example_server" "web" {
  name   = "web-server"
  region = "us-east-1"
  size   = "medium"
}

Conclusão

Neste guia completo, você aprendeu:

Fundamentos: Arquitetura de providers Terraform em Go ✅ Implementação: Criação completa de provider com CRUD ✅ Cliente HTTP: Padrões para comunicação com APIs ✅ Testes: Testes unitários e de aceitação ✅ Integração: Executando Terraform de aplicações Go ✅ Produção: Validação, logging, tratamento de erros ✅ Distribuição: Build multi-plataforma e publicação

Próximos Passos

  1. Go e Kubernetes - Orquestre containers com Go
  2. Go Observability - Logs, métricas e traces
  3. Go Microservices - Arquitetura distribuída

Recursos Adicionais


FAQ

Q: Posso usar Terraform com qualquer API? R: Sim! Qualquer API REST (ou mesmo GraphQL/gRPC) pode ser integrada através de um provider customizado.

Q: Qual a diferença entre SDK v1 e v2? R: O SDK v2 oferece melhor suporte a contextos, logging estruturado e melhor performance. Novos providers devem usar v2.

Q: É necessário publicar no Registry? R: Não. Você pode distribuir binários diretamente ou usar registries privados.

Q: Como testar sem API real? R: Use mocks, servidores de teste locais, ou implemente modos “dry-run” no seu cliente.