Ir para o conteúdo
Ferramenta para Plataforma1620 palavras

Terraform para Cloudflare: Guia Completo de Deploy

Existe um momento na carreira de todo engenheiro em que configurar recursos manualmente pelo painel web deixa de ser viável. Quando você tem 3 domínios, tudo bem. Quando tem 30 domínios, 200 regras de firewall, 15 Workers e configurações de cache espalhadas por múltiplos ambientes, a conversa muda completamente. Foi exatamente nesse ponto que comecei a usar Terraform com Cloudflare, e nunca mais voltei atrás.

Neste guia, vou mostrar como integrar essas duas ferramentas de forma prática. Vamos desde o setup inicial até exemplos reais de código, passando por boas práticas que aprendi ao longo de anos gerenciando infraestrutura em produção.

Por que usar Terraform com Cloudflare

O problema de configurações manuais

O painel da Cloudflare é intuitivo e bem construído. Mas ele tem uma limitação fundamental: não oferece versionamento, auditoria granular e nem reprodutibilidade. Se alguém do time altera uma regra de Page Rule às 3 da manhã durante um incidente, você não tem como saber o que mudou, quem mudou e por quê. Terraform resolve isso ao tratar toda a infraestrutura como código versionado no Git.

Benefícios concretos da combinação

A integração entre Terraform e Cloudflare brilha em cenários específicos. O primeiro é a consistência entre ambientes: você define a infraestrutura uma vez e replica para staging, production e disaster recovery sem esforço. O segundo é a automação de pipelines: com tudo em código, o deploy de infraestrutura entra no mesmo fluxo de CI/CD da aplicação. O terceiro é a documentação viva: o arquivo .tf é, por si só, a documentação mais atualizada da sua infraestrutura.

Casos de uso reais onde essa combinação se destaca incluem: empresas SaaS que gerenciam domínios customizados para cada cliente, equipes que precisam de regras de WAF consistentes em dezenas de zonas, e organizações que fazem deploy de Cloudflare Workers como parte do pipeline de aplicação.

Setup inicial do Terraform com Cloudflare

Pré-requisitos

Antes de começar, você precisa de três coisas: o Terraform CLI instalado (versão 1.5 ou superior), uma conta Cloudflare com API Token configurado e um editor de código com suporte a HCL (recomendo o VS Code com a extensão HashiCorp Terraform).

Instalação e configuração do provider

Crie um diretório para o projeto e inicialize com o seguinte arquivo:

TYPESCRIPT
// Arquivo: infrastructure/main.ts
// Usando CDKTF (Cloud Development Kit for Terraform) com TypeScript
 
import { Construct } from "constructs";
import { App, TerraformStack, TerraformOutput } from "cdktf";
import {
  provider as cloudflareProvider,
  zone as cloudflareZone,
  record as cloudflareRecord,
} from "@cdktf/provider-cloudflare";
 
class CloudflareInfrastructure extends TerraformStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);
 
    // Configura o provider da Cloudflare
    new cloudflareProvider.CloudflareProvider(this, "cloudflare", {
      apiToken: process.env.CLOUDFLARE_API_TOKEN,
    });
 
    // Cria ou importa a zona DNS
    const mainZone = new cloudflareZone.Zone(this, "main_zone", {
      account: {
        id: process.env.CLOUDFLARE_ACCOUNT_ID!,
      },
      zone: "vivodecodigo.com.br",
    });
 
    // Registro A para o domínio principal
    new cloudflareRecord.Record(this, "root_record", {
      zoneId: mainZone.id,
      name: "@",
      type: "A",
      content: "203.0.113.50",
      proxied: true,
      ttl: 1, // Auto quando proxied é true
    });
 
    // Registro CNAME para o subdomínio www
    new cloudflareRecord.Record(this, "www_record", {
      zoneId: mainZone.id,
      name: "www",
      type: "CNAME",
      content: "vivodecodigo.com.br",
      proxied: true,
      ttl: 1,
    });
 
    // Registro MX para e-mail
    new cloudflareRecord.Record(this, "mx_record", {
      zoneId: mainZone.id,
      name: "@",
      type: "MX",
      content: "mail.vivodecodigo.com.br",
      priority: 10,
      ttl: 3600,
    });
 
    // Output útil para referência em outros stacks
    new TerraformOutput(this, "zone_id", {
      value: mainZone.id,
      description: "ID da zona principal no Cloudflare",
    });
  }
}
 
const app = new App();
new CloudflareInfrastructure(app, "cloudflare-infra");
app.synth();

Para o setup equivalente em HCL puro, crie o arquivo providers.tf:

HCL
terraform {
  required_providers {
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.40"
    }
  }
  required_version = ">= 1.5.0"
}
 
provider "cloudflare" {
  api_token = var.cloudflare_api_token
}

Gerando o API Token correto

Este passo é crucial e frequentemente subestimado. Nunca use a Global API Key. Em vez disso, acesse o painel da Cloudflare, vá em "My Profile" e depois em "API Tokens". Crie um token customizado com permissões mínimas para os recursos que você vai gerenciar. Para o nosso exemplo, as permissões necessárias são: Zone DNS Edit, Zone Settings Read e Zone Settings Edit.

Execute os comandos para inicializar o projeto:

Bash
export CLOUDFLARE_API_TOKEN="seu-token-aqui"
terraform init
terraform plan
terraform apply

Exemplo prático: infraestrutura completa com Workers e WAF

Vamos ao cenário real. Imagine que você precisa configurar um domínio completo com registros DNS, um Cloudflare Worker para manipulação de headers, regras de firewall e configurações de cache. Esse é o tipo de setup que faço regularmente para novos microsserviços.

TYPESCRIPT
// Arquivo: infrastructure/complete-setup.ts
// Stack completo com CDKTF para um microsserviço em produção
 
import { Construct } from "constructs";
import { App, TerraformStack, Fn } from "cdktf";
import {
  provider as cloudflareProvider,
  record as cloudflareRecord,
  workersScript as cloudflareWorkersScript,
  workersRoute as cloudflareWorkersRoute,
  ruleset as cloudflareRuleset,
  zoneLockdown as cloudflareZoneLockdown,
  zoneSettingsOverride as cloudflareSettings,
} from "@cdktf/provider-cloudflare";
import * as fs from "fs";
 
interface ServiceConfig {
  zoneId: string;
  domain: string;
  originIp: string;
  environment: "staging" | "production";
}
 
class MicroserviceInfrastructure extends TerraformStack {
  constructor(scope: Construct, id: string, config: ServiceConfig) {
    super(scope, id);
 
    const { zoneId, domain, originIp, environment } = config;
 
    new cloudflareProvider.CloudflareProvider(this, "cloudflare", {
      apiToken: process.env.CLOUDFLARE_API_TOKEN,
    });
 
    // Registro DNS apontando para a origem
    const apiRecord = new cloudflareRecord.Record(this, "api_record", {
      zoneId: zoneId,
      name: `api-${environment}`,
      type: "A",
      content: originIp,
      proxied: true,
      ttl: 1,
    });
 
    // Deploy do Cloudflare Worker para segurança de headers
    const workerScript = new cloudflareWorkersScript.WorkersScript(
      this,
      "security_headers_worker",
      {
        accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
        scriptName: `security-headers-${environment}`,
        content: fs.readFileSync("./workers/security-headers.js", "utf-8"),
        module: true,
        plainTextBinding: [
          {
            name: "ENVIRONMENT",
            text: environment,
          },
        ],
      }
    );
 
    // Rota que ativa o Worker
    new cloudflareWorkersRoute.WorkersRoute(this, "worker_route", {
      zoneId: zoneId,
      pattern: `api-${environment}.${domain}/*`,
      scriptName: workerScript.scriptName,
    });
 
    // Regras de WAF customizadas usando Rulesets
    new cloudflareRuleset.Ruleset(this, "waf_custom_rules", {
      zoneId: zoneId,
      name: `WAF Rules - ${environment}`,
      description: "Regras customizadas de seguranca para a API",
      kind: "zone",
      phase: "http_request_firewall_custom",
      rules: [
        {
          action: "block",
          expression:
            '(http.request.uri.path contains "/admin" and not ip.src in {10.0.0.0/8 172.16.0.0/12})',
          description: "Bloqueia acesso admin fora da rede interna",
          enabled: true,
        },
        {
          action: "challenge",
          expression:
            '(http.request.method eq "POST" and http.request.uri.path contains "/login" and cf.threat_score gt 30)',
          description: "Challenge para tentativas suspeitas de login",
          enabled: true,
        },
        {
          action: "block",
          expression:
            '(http.user_agent contains "sqlmap" or http.user_agent contains "nikto")',
          description: "Bloqueia scanners conhecidos",
          enabled: true,
        },
      ],
    });
 
    // Zone Lockdown para endpoints sensíveis
    if (environment === "production") {
      new cloudflareZoneLockdown.ZoneLockdown(this, "admin_lockdown", {
        zoneId: zoneId,
        description: "Restringe acesso ao painel admin",
        urls: [`api-production.${domain}/admin/*`],
        configurations: [
          { target: "ip_range", value: "10.0.0.0/8" },
          { target: "ip_range", value: "172.16.0.0/12" },
        ],
      });
    }
  }
}
 
// Instancia stacks para diferentes ambientes
const app = new App();
 
new MicroserviceInfrastructure(app, "staging", {
  zoneId: process.env.CLOUDFLARE_ZONE_ID!,
  domain: "vivodecodigo.com.br",
  originIp: "203.0.113.10",
  environment: "staging",
});
 
new MicroserviceInfrastructure(app, "production", {
  zoneId: process.env.CLOUDFLARE_ZONE_ID!,
  domain: "vivodecodigo.com.br",
  originIp: "203.0.113.20",
  environment: "production",
});
 
app.synth();

O Worker de segurança referenciado no código

O arquivo workers/security-headers.js que o código acima referencia seria algo assim:

JAVASCRIPT
export default {
  async fetch(request, env) {
    const response = await fetch(request);
    const newHeaders = new Headers(response.headers);
 
    newHeaders.set("X-Content-Type-Options", "nosniff");
    newHeaders.set("X-Frame-Options", "DENY");
    newHeaders.set("Referrer-Policy", "strict-origin-when-cross-origin");
    newHeaders.set("Permissions-Policy", "camera=(), microphone=()");
    newHeaders.set("X-Environment", env.ENVIRONMENT);
 
    return new Response(response.body, {
      status: response.status,
      headers: newHeaders,
    });
  },
};

Boas práticas para Terraform com Cloudflare

Organização de estado e workspaces

Nunca armazene o state file localmente em produção. Use um backend remoto como S3, GCS ou Terraform Cloud. Separe os states por responsabilidade: um para DNS, outro para Workers e outro para regras de segurança. Isso evita que um terraform apply acidental em registros DNS derrube suas regras de WAF.

Bash
# Inicialização com backend S3
terraform init \
  -backend-config="bucket=meu-terraform-state" \
  -backend-config="key=cloudflare/production/dns.tfstate" \
  -backend-config="region=us-east-1"

Importação de recursos existentes

Se você já tem recursos configurados manualmente na Cloudflare, não recrie do zero. Use o comando terraform import para trazer recursos existentes para o controle do Terraform:

Bash
# Importar uma zona existente
terraform import cloudflare_zone.main_zone <zone-id>
 
# Importar um registro DNS existente
terraform import cloudflare_record.root_record <zone-id>/<record-id>
 
# Listar recursos via API para encontrar IDs
curl -s -X GET "https://api.cloudflare.com/client/v4/zones" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  -H "Content-Type: application/json" | jq '.result[] | {id, name}'

Segurança do API Token

Jamais faça commit do token no repositório. Use variáveis de ambiente no CI/CD e, se possível, integre com um vault de segredos como HashiCorp Vault ou AWS Secrets Manager. Crie tokens com escopo mínimo: se o pipeline só gerencia DNS, o token não precisa de permissão para editar Workers.

Validação antes do apply

Integre validações no pipeline de CI antes de aplicar mudanças. O comando terraform plan deve ser executado como etapa obrigatória em pull requests, e a saída deve ser revisada por pelo menos um membro do time:

Bash
# Pipeline de validação
terraform fmt -check -recursive
terraform validate
terraform plan -out=tfplan -detailed-exitcode
 
# O exit code 2 indica que há mudanças a serem aplicadas
# O exit code 0 indica que não há mudanças
# O exit code 1 indica erro

Versionamento do provider

Sempre fixe a versão do provider Cloudflare. Atualizações major podem conter breaking changes que quebram seu pipeline em produção. Atualize de forma controlada, testando primeiro em staging.

Conclusão

A combinação de Terraform com Cloudflare transforma a maneira como você gerencia infraestrutura de edge. O que antes era um processo manual e propenso a erros se torna reproduzível, auditável e integrado ao fluxo de desenvolvimento. O CDKTF com TypeScript adiciona uma camada extra de segurança com tipagem estática e permite criar abstrações reutilizáveis que escalam junto com a complexidade da sua infraestrutura.

Comece pelo básico: importe seus recursos existentes, versione tudo no Git e adicione o terraform plan ao pipeline de pull requests. A partir daí, evolua para módulos compartilhados, workspaces por ambiente e automação completa de deploys

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