Ir para o conteúdo
Ferramenta para Plataforma1475 palavras

Terraform para DigitalOcean: Guia Completo de Deploy

Provisionar servidores manualmente pelo painel da DigitalOcean funciona quando você tem dois ou três Droplets. Quando sua infraestrutura cresce para dez, vinte ou cinquenta recursos, o processo manual se torna um pesadelo de inconsistências e configurações esquecidas. É exatamente aqui que o Terraform entra como sua ferramenta de sobrevivência.

Neste guia, vou mostrar como usar Terraform com a DigitalOcean do zero ao deploy de uma infraestrutura completa, com exemplos práticos, comandos reais e as boas práticas que aprendi gerenciando ambientes de produção nos últimos anos.

Por que usar Terraform com DigitalOcean

A DigitalOcean é conhecida pela simplicidade. O Terraform é conhecido por transformar infraestrutura em código. Quando você combina os dois, ganha o melhor dos mundos: uma cloud acessível e previsível, gerenciada por arquivos declarativos que podem ser versionados, revisados e replicados.

Infraestrutura como Código na prática

O conceito de Infrastructure as Code (IaC) não é apenas uma buzzword. Na prática, significa que toda a sua infraestrutura vive em arquivos de texto que passam por code review, têm histórico no Git e podem ser aplicados de forma idempotente. Se alguém apagar um Load Balancer por acidente, você executa um comando e ele volta exatamente como estava.

Casos de uso reais onde essa combinação brilha

A combinação Terraform e DigitalOcean se destaca em cenários específicos. Startups que precisam replicar ambientes de staging e produção com configurações idênticas encontram nessa dupla uma solução eficiente. Times que gerenciam múltiplos projetos de clientes conseguem versionar cada infraestrutura separadamente. Empresas que precisam escalar horizontalmente durante picos de demanda automatizam a criação de Droplets com autoscaling baseado em métricas.

Outro caso muito comum: equipes de desenvolvimento que precisam subir e destruir ambientes efêmeros para testes de integração. Com Terraform, você cria toda a infraestrutura necessária com um comando e a destrói quando os testes terminam, pagando apenas pelo tempo utilizado.

Setup inicial do Terraform com DigitalOcean

Instalação do Terraform

No macOS com Homebrew:

TYPESCRIPT
// Nota: estes são comandos de terminal, não TypeScript.
// Incluímos aqui para referência do setup.
 
// brew install terraform
// terraform --version

No Ubuntu/Debian, o processo envolve adicionar o repositório da HashiCorp:

Text
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

Gerando o token de API na DigitalOcean

Acesse o painel da DigitalOcean, vá em "API" no menu lateral e gere um Personal Access Token com permissões de leitura e escrita. Guarde esse token com segurança, pois ele será usado pelo Terraform para autenticar todas as chamadas à API.

A maneira mais segura de fornecê-lo ao Terraform é via variável de ambiente:

Text
export DIGITALOCEAN_TOKEN="dop_v1_seu_token_aqui"

Estrutura de diretórios recomendada

Organizar seus arquivos Terraform desde o início evita dor de cabeça futura. Esta é a estrutura que utilizo em projetos de produção:

Text
infra/
  main.tf
  variables.tf
  outputs.tf
  terraform.tfvars
  modules/
    droplet/
      main.tf
      variables.tf
      outputs.tf
    database/
      main.tf
      variables.tf
      outputs.tf

Exemplo prático: provisionando infraestrutura completa

Vamos criar uma infraestrutura real que inclui um Droplet, um banco de dados gerenciado, um firewall e um domínio configurado. Começamos com o arquivo de configuração do provider.

Configuração do provider DigitalOcean

Text
terraform {
  required_providers {
    digitalocean = {
      source  = "digitalocean/digitalocean"
      version = "~> 2.34"
    }
  }
  required_version = ">= 1.6.0"
}
 
variable "do_token" {
  description = "Token de API da DigitalOcean"
  type        = string
  sensitive   = true
}
 
variable "ssh_key_fingerprint" {
  description = "Fingerprint da chave SSH cadastrada na DigitalOcean"
  type        = string
}
 
provider "digitalocean" {
  token = var.do_token
}

Criando Droplets com Terraform na DigitalOcean

Text
resource "digitalocean_droplet" "web" {
  count    = 2
  name     = "web-${count.index + 1}"
  region   = "nyc3"
  size     = "s-2vcpu-4gb"
  image    = "ubuntu-22-04-x64"
  ssh_keys = [var.ssh_key_fingerprint]
 
  user_data = <<-EOF
    #!/bin/bash
    apt-get update
    apt-get install -y nginx
    systemctl enable nginx
    systemctl start nginx
    ufw allow 'Nginx Full'
  EOF
 
  tags = ["web", "production"]
}
 
resource "digitalocean_loadbalancer" "public" {
  name   = "lb-web-production"
  region = "nyc3"
 
  forwarding_rule {
    entry_port     = 80
    entry_protocol = "http"
    target_port     = 80
    target_protocol = "http"
  }
 
  forwarding_rule {
    entry_port     = 443
    entry_protocol = "https"
    target_port     = 80
    target_protocol = "http"
    tls_passthrough = false
    certificate_name = digitalocean_certificate.cert.name
  }
 
  healthcheck {
    port     = 80
    protocol = "http"
    path     = "/health"
  }
 
  droplet_ids = digitalocean_droplet.web[*].id
}
 
resource "digitalocean_certificate" "cert" {
  name    = "cert-production"
  type    = "lets_encrypt"
  domains = ["app.seudominio.com"]
}
 
resource "digitalocean_firewall" "web" {
  name = "fw-web-production"
 
  droplet_ids = digitalocean_droplet.web[*].id
 
  inbound_rule {
    protocol         = "tcp"
    port_range       = "22"
    source_addresses = ["seu.ip.fixo/32"]
  }
 
  inbound_rule {
    protocol                  = "tcp"
    port_range                = "80"
    source_load_balancer_uids = [digitalocean_loadbalancer.public.id]
  }
 
  outbound_rule {
    protocol              = "tcp"
    port_range            = "1-65535"
    destination_addresses = ["0.0.0.0/0"]
  }
 
  outbound_rule {
    protocol              = "udp"
    port_range            = "1-65535"
    destination_addresses = ["0.0.0.0/0"]
  }
}
 
resource "digitalocean_database_cluster" "postgres" {
  name       = "db-production"
  engine     = "pg"
  version    = "15"
  size       = "db-s-1vcpu-2gb"
  region     = "nyc3"
  node_count = 1
}
 
resource "digitalocean_database_firewall" "db_fw" {
  cluster_id = digitalocean_database_cluster.postgres.id
 
  rule {
    type  = "droplet"
    value = digitalocean_droplet.web[0].id
  }
 
  rule {
    type  = "droplet"
    value = digitalocean_droplet.web[1].id
  }
}
 
output "lb_ip" {
  value = digitalocean_loadbalancer.public.ip
}
 
output "db_uri" {
  value     = digitalocean_database_cluster.postgres.uri
  sensitive = true
}

Aplicando a infraestrutura com comandos CLI

A sequência de comandos para provisionar tudo:

Text
terraform init
terraform plan -var="do_token=$DIGITALOCEAN_TOKEN" -var="ssh_key_fingerprint=sua:fingerprint:aqui"
terraform apply -var="do_token=$DIGITALOCEAN_TOKEN" -var="ssh_key_fingerprint=sua:fingerprint:aqui" -auto-approve

O terraform plan mostra exatamente o que será criado antes de qualquer alteração real. Sempre revise o plano antes de aplicar em produção.

Automatizando o deploy com TypeScript e a API do Terraform

Uma prática que adoto em projetos maiores é criar scripts em TypeScript para orquestrar o Terraform e integrar com pipelines de CI/CD. Aqui vai o primeiro exemplo prático em TypeScript:

TYPESCRIPT
import { execSync } from "child_process";
import * as fs from "fs";
 
interface TerraformConfig {
  workspace: string;
  varsFile: string;
  autoApprove: boolean;
}
 
interface DeployResult {
  success: boolean;
  outputs: Record<string, string>;
  duration: number;
}
 
function runTerraform(command: string): string {
  try {
    const result = execSync(command, {
      encoding: "utf-8",
      cwd: "./infra",
      env: {
        ...process.env,
        TF_IN_AUTOMATION: "true",
        TF_INPUT: "false",
      },
    });
    return result;
  } catch (error: unknown) {
    const execError = error as { stderr?: string };
    console.error(`Falha ao executar: ${command}`);
    console.error(execError.stderr || "Erro desconhecido");
    process.exit(1);
  }
}
 
function deployInfrastructure(config: TerraformConfig): DeployResult {
  const startTime = Date.now();
 
  console.log(`Inicializando Terraform no workspace: ${config.workspace}`);
  runTerraform("terraform init -backend-config=backend.hcl");
 
  const workspaces = runTerraform("terraform workspace list");
  if (!workspaces.includes(config.workspace)) {
    runTerraform(`terraform workspace new ${config.workspace}`);
  }
  runTerraform(`terraform workspace select ${config.workspace}`);
 
  console.log("Executando plan...");
  const planOutput = runTerraform(
    `terraform plan -var-file=${config.varsFile} -out=tfplan`
  );
  console.log(planOutput);
 
  if (config.autoApprove) {
    console.log("Aplicando mudancas...");
    runTerraform("terraform apply tfplan");
  }
 
  const outputsRaw = runTerraform("terraform output -json");
  const outputs = JSON.parse(outputsRaw);
 
  const parsedOutputs: Record<string, string> = {};
  for (const [key, value] of Object.entries(outputs)) {
    const typedValue = value as { value: string };
    parsedOutputs[key] = typedValue.value;
  }
 
  const duration = Date.now() - startTime;
  return { success: true, outputs: parsedOutputs, duration };
}
 
const environment = process.argv[2] || "staging";
const result = deployInfrastructure({
  workspace: environment,
  varsFile: `envs/${environment}.tfvars`,
  autoApprove: environment !== "production",
});
 
console.log(`Deploy concluido em ${result.duration}ms`);
console.log("Outputs:", JSON.stringify(result.outputs, null, 2));

O segundo exemplo em TypeScript mostra como validar a infraestrutura após o deploy, verificando se os recursos estão realmente acessíveis:

TYPESCRIPT
import fetch from "node-fetch";
 
interface HealthCheckTarget {
  name: string;
  url: string;
  expectedStatus: number;
  timeoutMs: number;
}
 
interface HealthCheckResult {
  target: string;
  healthy: boolean;
  statusCode: number | null;
  responseTimeMs: number;
  error: string | null;
}
 
async function checkHealth(
  target: HealthCheckTarget
): Promise<HealthCheckResult> {
  const start = Date.now();
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), target.timeoutMs);
 
  try {
    const response = await fetch(target.url, {
      method: "GET",
      signal: controller.signal,
    });
 
    clearTimeout(timeout);
    const responseTime = Date.now() - start;
 
    return {
      target: target.name,
      healthy: response.status === target.expectedStatus,
      statusCode: response.status,
      responseTimeMs: responseTime,
      error: null,
    };
  } catch (err: unknown) {
    clearTimeout(timeout);
    const errorMessage = err instanceof Error ? err.message : "Erro desconhecido";
    return {
      target: target.name,
      healthy: false,
      statusCode: null,
      responseTimeMs: Date.now() - start,
      error: errorMessage,
    };
  }
}
 
async function validateInfrastructure(
  lbIp: string
): Promise<boolean> {
  const targets: HealthCheckTarget[] = [
    {
      name: "Load Balancer HTTP",
      url: `http://${lbIp}/health`,
      expectedStatus: 200,
      timeoutMs: 10000,
    },
    {
      name: "Load Balancer HTTPS",
      url: `https://app.seudominio.com/health`,
      expectedStatus: 200,
      timeoutMs: 10000,
    },
  ];
 
  console.log("Validando infraestrutura pos-deploy...\n");
 
  const results = await Promise.all(targets.map(checkHealth));
  let allHealthy = true;
 
  for (const result of results) {
    const status = result.healthy ? "OK" : "FALHA";
    console.log(
      `[${status}] ${result.target}: status=${result.statusCode}, tempo=${result.responseTimeMs}ms`
    );
    if (result.error) {
      console.log(`  Erro: ${result.error}`);
    }
    if (!result.healthy) {
      allHealthy = false;
    }
  }
 
  console.log(
    allHealthy
      ? "\nTodos os checks passaram."
      : "\nAlguns checks falharam. Verifique a infraestrutura."
  );
 
  return allHealthy;
}
 
const loadBalancerIp = process.argv[2];
if (!loadBalancerIp) {
  console.error("Uso: ts-node validate.ts <IP_DO_LOAD_BALANCER>");
  process.exit(1);
}
 
validateInfrastructure(loadBalancerIp).then((healthy) => {
  process.exit(healthy ? 0 : 1);
});

Boas práticas de Terraform na DigitalOcean

Gerenciamento de estado remoto

Nunca armazene o arquivo terraform.tfstate localmente em produção. Use

Guias Relacionados

Explore mais guias técnicos

Mais de 100 guias práticos de integração entre frameworks, ferramentas e plataformas.

Ver todos os guias