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
- Por que Go e Terraform?
- Arquitetura de Providers Terraform
- Criando seu Primeiro Provider
- Gerenciando Recursos com Go
- Testando Providers
- Integração com Aplicações Go
- Padrões de Produção
- 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:
- Crie um repositório GitHub público
- Use tags semânticas (
v1.0.0) - Crie releases GitHub com binários
- Siga o formato de naming:
terraform-provider-<nome> - 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
- Go e Kubernetes - Orquestre containers com Go
- Go Observability - Logs, métricas e traces
- 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.