Cloudflare IaC: Terraform + Wrangler
O Problema Central
As ferramentas do Cloudflare são divididas em dois mundos:
- Terraform gerencia infraestrutura: zonas, registros DNS, políticas de acesso, regras de firewall, configurações de conta e a existência de recursos como namespaces KV, bancos D1, buckets R2 e Filas.
- Wrangler gerencia a implantação de aplicações: código do Worker, projetos Pages, bindings para esses recursos e desenvolvimento local.
O desafio é que o Wrangler precisa de IDs de recursos que o Terraform cria. Um ID de namespace KV, um ID de banco D1, um nome de Fila — estes são outputs de terraform apply que devem fluir para wrangler.toml (ou wrangler.jsonc) antes que wrangler deploy funcione.
Este skill codifica um padrão DRY e automatizável para preencher essa lacuna.
Separação de Responsabilidades
O Terraform Possui
- Zonas e registros DNS
- Namespaces KV (
cloudflare_workers_kv_namespace) - Bancos D1 (
cloudflare_d1_database) - Buckets R2 (
cloudflare_r2_bucket) - Filas (
cloudflare_queue) - Políticas de acesso, regras WAF, page rules
- Configurações de nível de conta
- Qualquer recurso que deva existir antes da implantação do código da aplicação
O Wrangler Possui
- Código fonte do Worker e empacotamento
- Implantação de projetos Pages
- Configuração de bindings (conectar Workers aos recursos criados pelo Terraform)
- Desenvolvimento local (
wrangler dev) - Segredos (via
wrangler secret put) - Implantação (
wrangler deploy)
A Zona Cinzenta
Alguns recursos podem ser gerenciados por qualquer ferramenta. O princípio norteador: se vários Workers ou projetos compartilham um recurso, o Terraform o possui. Se está no escopo de um único Worker, o Wrangler pode possuí-lo. Em caso de dúvida, prefira o Terraform — ele oferece rastreamento de estado, detecção de drift e uma única fonte de verdade para infraestrutura.
O recurso de provisionamento automático do Wrangler (que cria recursos KV/R2/D1 na primeira implantação) é conveniente para prototipagem, mas não deve ser usado em pipelines de produção. Ele cria recursos fora do estado do Terraform, levando a drift e recursos que não podem ser gerenciados ou desmontados sistematicamente.
O Padrão de Ponte
Visão Geral
terraform apply → terraform output -json → script de ponte → wrangler.jsonc → wrangler deploy
O script de ponte é um script Node.js no projeto, invocado via package.json. Ele lê os outputs do Terraform e os escreve no arquivo de configuração do Wrangler. O mesmo script é executado localmente e em CI/CD — sem duplicação.
Passo 1: Outputs do Terraform
Defina outputs para cada ID de recurso que o Wrangler precisa:
# outputs.tf
output "kv_namespace_id" {
value = cloudflare_workers_kv_namespace.my_kv.id
}
output "d1_database_id" {
value = cloudflare_d1_database.my_db.id
}
output "r2_bucket_name" {
value = cloudflare_r2_bucket.my_bucket.name
}
output "account_id" {
value = var.cloudflare_account_id
}
output "zone_id" {
value = cloudflare_zone.my_zone.id
}
Passo 2: O Script de Ponte
Crie scripts/sync-wrangler-config.mjs — um script Node.js que lê os outputs do Terraform e gera (ou atualiza) a configuração do Wrangler:
#!/usr/bin/env node
// scripts/sync-wrangler-config.mjs
//
// Lê os outputs do Terraform e escreve wrangler.jsonc com os IDs de recursos corretos.
// Execute localmente ou em CI — mesmo comando, mesmo resultado.
import { execSync } from "node:child_process";
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, "..");
// --- Configuração ---
// Ajuste esses caminhos para corresponder à estrutura do seu projeto.
const TERRAFORM_DIR = join(ROOT, "terraform");
const WRANGLER_CONFIG_PATH = join(ROOT, "wrangler.jsonc");
const TEMPLATE_PATH = join(ROOT, "wrangler.template.jsonc");
// --- Ler outputs do Terraform ---
function getTerraformOutputs() {
try {
const raw = execSync("terraform output -json", {
cwd: TERRAFORM_DIR,
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
});
const outputs = JSON.parse(raw);
// terraform output -json envolve cada valor em { value, type, sensitive }
const flat = {};
for (const [key, entry] of Object.entries(outputs)) {
flat[key] = entry.value;
}
return flat;
} catch (err) {
console.error("Falha ao ler outputs do Terraform. Você executou 'terraform apply'?");
console.error(err.message);
process.exit(1);
}
}
// --- Gerar configuração do Wrangler ---
function generateConfig(outputs) {
if (!existsSync(TEMPLATE_PATH)) {
console.error(`Template não encontrado: ${TEMPLATE_PATH}`);
console.error("Crie um wrangler.template.jsonc com tokens __PLACEHOLDER__.");
process.exit(1);
}
let template = readFileSync(TEMPLATE_PATH, "utf-8");
// Substituir tokens __PLACEHOLDER__ pelos valores dos outputs do Terraform.
// Formato do token: __UPPER_SNAKE_CASE__ mapeando para nomes de outputs do terraform.
for (const [key, value] of Object.entries(outputs)) {
const token = `__${key.toUpperCase()}__`;
template = template.replaceAll(token, String(value));
}
// Avisar sobre tokens não substituídos
const remaining = template.match(/__[A-Z_]+__/g);
if (remaining) {
console.warn("Aviso: tokens não substituídos no template:", [...new Set(remaining)]);
}
writeFileSync(WRANGLER_CONFIG_PATH, template, "utf-8");
console.log(`Escrito ${WRANGLER_CONFIG_PATH}`);
}
// --- Principal ---
const outputs = getTerraformOutputs();
generateConfig(outputs);
Passo 3: O Template
Crie wrangler.template.jsonc — uma configuração Wrangler com tokens de espaço reservado onde os outputs do Terraform devem ir:
{
// Gerado a partir de wrangler.template.jsonc por scripts/sync-wrangler-config.mjs
// Não edite diretamente — edite o template.
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-01-01",
"account_id": "__ACCOUNT_ID__",
"kv_namespaces": [
{
"binding": "MY_KV",
"id": "__KV_NAMESPACE_ID__"
}
],
"d1_databases": [
{
"binding": "MY_DB",
"database_id": "__D1_DATABASE_ID__",
"database_name": "my-db"
}
],
"r2_buckets": [
{
"binding": "MY_BUCKET",
"bucket_name": "__R2_BUCKET_NAME__"
}
]
}
Passo 4: Scripts do package.json
{
"scripts": {
"sync": "node scripts/sync-wrangler-config.mjs",
"predeploy": "npm run sync",
"deploy": "wrangler deploy",
"dev": "npm run sync && wrangler dev"
}
}
npm run deploy executa automaticamente sync primeiro (via o hook predeploy). O mesmo comando npm run deploy funciona localmente e em CI/CD — DRY.
Passo 5: .gitignore
# Gerado pelo script de sincronização — não commitar
wrangler.jsonc
# Terraform
terraform/.terraform/
terraform/*.tfstate*
O wrangler.jsonc gerado não deve ser commitado. O template e o script de sincronização são a fonte da verdade.
Integração com CI/CD
O pipeline de CI/CD executa exatamente os mesmos comandos que um desenvolvedor executa localmente. A única diferença é como o estado e as credenciais do Terraform são fornecidos.
Estrutura Genérica do Pipeline
# Pseudocódigo — adapte para a sintaxe da sua plataforma de CI/CD
steps:
# 1. Terraform aplica mudanças na infraestrutura
- name: terraform
run: |
cd terraform
terraform init
terraform apply -auto-approve
# 2. Script de ponte sincroniza outputs do Terraform na configuração do Wrangler
# 3. Wrangler implanta a aplicação
- name: deploy-worker
run: npm run deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
É isso. npm run deploy chama predeploy → sync → wrangler deploy. Igual ao local.
Credenciais
- Terraform: precisa de um token de API do Cloudflare com permissão para gerenciar recursos de infraestrutura. Forneça via variável de ambiente
CLOUDFLARE_API_TOKENou na configuração do provider. - Wrangler: precisa de um token de API do Cloudflare com permissão para implantar Workers. Frequentemente é o mesmo token, mas pode ser escopo separado para privilégio mínimo.
- Estado do Terraform: use um backend remoto (S3, GCS, Terraform Cloud, etc.) para que CI e local compartilhem o mesmo estado.
Promoção de Ambiente
Para configurações multi-ambiente (staging, produção), use workspaces do Terraform ou arquivos de estado separados e ambientes do Wrangler:
// Em wrangler.template.jsonc
{
"name": "my-worker",
"env": {
"staging": {
"kv_namespaces": [{ "binding": "MY_KV", "id": "__STAGING_KV_NAMESPACE_ID__" }]
},
"production": {
"kv_namespaces": [{ "binding": "MY_KV", "id": "__PRODUCTION_KV_NAMESPACE_ID__" }]
}
}
}
O script de sincronização lida com isso naturalmente — basta adicionar os outputs correspondentes do Terraform.
Alternativa: Bindings Gerenciados pelo Terraform
Para equipes que desejam o Terraform como fonte única de verdade para tudo — incluindo implantação — o recurso cloudflare_worker_version pode gerenciar bindings diretamente. Neste modelo, o Wrangler apenas empacota o código e o Terraform lida com o upload e a conexão dos bindings:
resource "cloudflare_worker_version" "my_worker" {
account_id = var.cloudflare_account_id
worker_id = cloudflare_worker.my_worker.id
main_module = "index.js"
compatibility_date = "2025-01-01"
bindings = [
{
type = "kv_namespace"
name = "MY_KV"
namespace_id = cloudflare_workers_kv_namespace.my_kv.id
},
{
type = "d1"
name = "DB"
id = cloudflare_d1_database.my_db.id
},
{
type = "r2_bucket"
name = "MY_BUCKET"
bucket_name = cloudflare_r2_bucket.my_bucket.name
}
]
}
Com esta abordagem:
wrangler deploy --dry-run --outdir distempacota o código- O Terraform faz o upload do pacote e conecta todos os bindings via referências de recursos
- Sem script de ponte, sem template — o Terraform resolve os IDs nativamente
Trade-off: Você perde a ergonomia de desenvolvimento local do Wrangler para configuração de bindings. Ainda precisará de um wrangler.toml para wrangler dev, o que significa alguma duplicação. O padrão de ponte (acima) é geralmente melhor quando os desenvolvedores usam ativamente wrangler dev, enquanto a abordagem apenas com Terraform é adequada para fluxos de trabalho centrados em CI onde o desenvolvimento local usa mocks ou uma configuração mínima.
Escolha o padrão de ponte quando a equipe executa wrangler dev regularmente. Escolha bindings gerenciados pelo Terraform quando o Terraform já é a ferramenta de implantação e o desenvolvimento local não precisa de bindings reais.
Segurança de Tipos: wrangler types --check
Após gerar ou atualizar a configuração do Wrangler, execute wrangler types para regenerar os bindings TypeScript. Em CI, use --check para falhar o build se os tipos estiverem desatualizados:
{
"scripts": {
"typecheck": "wrangler types --check",
"predeploy": "npm run sync && npm run typecheck",
"deploy": "wrangler deploy"
}
}
Isso detecta incompatibilidades entre recursos gerenciados pelo Terraform e os tipos TypeScript do Worker antes da implantação.
Adaptando a um Projeto Existente
Ao aplicar este padrão a um projeto que já existe:
-
Audite o que existe. Veja o
wrangler.toml/wrangler.jsoncatual em busca de IDs de recursos codificados. Verifique se o Terraform já está em uso. Veja a configuração de CI/CD. -
Identifique os recursos. Liste cada ID de recurso na configuração do Wrangler. Determine quais já estão no Terraform e quais precisam ser importados.
-
Importe recursos existentes para o Terraform usando
terraform importem vez de recriá-los. Isso evita tempo de inatividade e perda de dados. -
Crie o template. Copie a configuração existente do Wrangler para
wrangler.template.jsonce substitua IDs codificados por tokens__PLACEHOLDER__. -
Escreva o script de ponte. Comece com o script de referência acima e ajuste caminhos e nomes de outputs.
-
Configure o package.json. Adicione scripts
sync,predeployedeploy. -
Atualize o CI/CD. Substitua qualquer passo manual de passagem de IDs ou geração de configuração por
npm run deploy. -
Adicione
wrangler.jsoncao.gitignoreassim que o arquivo gerado estiver funcionando.
Padrões Comuns
Múltiplos Workers em um Monorepo
Quando um repositório contém vários Workers, cada um recebe seu próprio template e o script de sincronização lida com todos:
projeto/
├── terraform/
├── workers/
│ ├── api/
│ │ ├── src/
│ │ └── wrangler.template.jsonc
│ └── cron/
│ ├── src/
│ └── wrangler.template.jsonc
├── scripts/
│ └── sync-wrangler-config.mjs
└── package.json
O script de sincronização itera sobre os diretórios de workers e aplica a mesma lógica de substituição de tokens a cada template.
Segredos
Segredos não devem passar pelo script de ponte ou template. Use wrangler secret put (interativo) ou wrangler secret bulk (de um arquivo JSON, útil em CI). Segredos são gerenciados por Worker na API do Cloudflare, não em arquivos de configuração.
Em CI:
echo '{"API_KEY":"...","DB_PASSWORD":"..."}' | wrangler secret bulk
Ambientes de Preview
Para implantações de preview baseadas em PR, o script de sincronização pode aceitar um argumento de ambiente:
{
"scripts": {
"deploy:preview": "npm run sync -- --env preview && wrangler deploy --env preview"
}
}
Solução de Problemas
| Sintoma | Causa Provável |
|---|---|
sync falha com "Falha ao ler outputs do Terraform" | O Terraform não foi aplicado, ou você está no diretório errado. Execute terraform apply primeiro. |
Tokens __TOKENS__ não substituídos na configuração gerada | O nome do output do Terraform não corresponde ao token. Verifique se os nomes em terraform output correspondem ao padrão de token em maiúsculas. |
| Implantação do Wrangler falha com "recurso não encontrado" | O ID do recurso do Terraform está desatualizado ou é do ambiente errado. Reexecute terraform apply e depois npm run sync. |
| Drift entre o estado do Terraform e o Cloudflare | Um recurso foi modificado fora do Terraform (possivelmente pelo provisionamento automático do Wrangler). Execute terraform plan para detectar e reconciliar. |
Referência
scripts/sync-wrangler-config.mjs— O script de ponte (veja o diretórioscripts/para uma versão pronta para produção com análise de argumentos e suporte a múltiplos workers)references/terraform-wrangler-mapping.md— Mapeia Terra
… [truncado para tradução]