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:
// Nota: estes são comandos de terminal, não TypeScript.
// Incluímos aqui para referência do setup.
// brew install terraform
// terraform --versionNo Ubuntu/Debian, o processo envolve adicionar o repositório da HashiCorp:
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 terraformGerando 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:
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:
infra/
main.tf
variables.tf
outputs.tf
terraform.tfvars
modules/
droplet/
main.tf
variables.tf
outputs.tf
database/
main.tf
variables.tf
outputs.tfExemplo 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
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
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:
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-approveO 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:
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:
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