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:
// 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:
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:
export CLOUDFLARE_API_TOKEN="seu-token-aqui"
terraform init
terraform plan
terraform applyExemplo 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.
// 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:
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.
# 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:
# 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:
# 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 erroVersionamento 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