Cloudflare IaC: Terraform + Wrangler은(는) 무엇을 하나요?
핵심 문제
Cloudflare의 도구는 두 가지 세계로 나뉘어 있습니다:
- Terraform은 인프라를 관리합니다: 영역(zone), DNS 레코드, 액세스 정책, 방화벽 규칙, 계정 수준 설정, 그리고 KV 네임스페이스, D1 데이터베이스, R2 버킷, 큐와 같은 리소스의 존재를 관리합니다.
- Wrangler는 애플리케이션 배포를 관리합니다: Worker 코드, Pages 프로젝트, 해당 리소스에 대한 바인딩, 그리고 로컬 개발.
문제는 Wrangler가 Terraform이 생성하는 리소스 ID를 필요로 한다는 점입니다. KV 네임스페이스 ID, D1 데이터베이스 ID, 큐 이름 — 이것들은 terraform apply의 결과물이며, wrangler deploy가 작동하기 전에 wrangler.toml(또는 wrangler.jsonc)로 전달되어야 합니다.
이 스킬은 그 격차를 해소하기 위한 DRY하고 자동화 가능한 패턴을 코드화합니다.
책임 분리
Terraform 소유
- 영역 및 DNS 레코드
- KV 네임스페이스 (
cloudflare_workers_kv_namespace) - D1 데이터베이스 (
cloudflare_d1_database) - R2 버킷 (
cloudflare_r2_bucket) - 큐 (
cloudflare_queue) - 액세스 정책, WAF 규칙, 페이지 규칙
- 계정 수준 설정
- 애플리케이션 코드가 배포되기 전에 존재해야 하는 모든 리소스
Wrangler 소유
- Worker 소스 코드 및 번들링
- Pages 프로젝트 배포
- 바인딩 구성 (Worker를 Terraform이 생성한 리소스에 연결)
- 로컬 개발 (
wrangler dev) - 시크릿 (
wrangler secret put사용) - 배포 (
wrangler deploy)
회색 영역
일부 리소스는 두 도구 모두에서 관리할 수 있습니다. 기본 원칙은 다음과 같습니다: 여러 Worker 또는 프로젝트가 리소스를 공유하면 Terraform이 소유합니다. 단일 Worker로 범위가 지정되면 Wrangler가 소유할 수 있습니다. 모호한 경우 Terraform을 선호하세요 — 상태 추적, 드리프트 감지, 그리고 인프라에 대한 단일 정보 소스를 제공합니다.
Wrangler의 자동 프로비저닝 기능(첫 배포 시 KV/R2/D1 리소스를 생성)은 프로토타입 제작에 편리하지만 프로덕션 파이프라인에서는 사용하지 말아야 합니다. 이는 Terraform 상태 외부에서 리소스를 생성하여 드리프트를 유발하고 체계적으로 관리하거나 제거할 수 없는 리소스를 만듭니다.
브리지 패턴
개요
terraform apply → terraform output -json → 브리지 스크립트 → wrangler.jsonc → wrangler deploy
브리지 스크립트는 프로젝트에 있는 Node.js 스크립트이며, package.json을 통해 호출됩니다. Terraform 출력을 읽고 Wrangler의 설정 파일에 씁니다. 동일한 스크립트가 로컬과 CI/CD에서 실행됩니다 — 중복 없음.
1단계: Terraform 출력
Wrangler가 필요로 하는 모든 리소스 ID에 대한 출력을 정의합니다:
# 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
}
2단계: 브리지 스크립트
scripts/sync-wrangler-config.mjs — Terraform 출력을 읽고 Wrangler 설정을 생성(또는 패치)하는 Node.js 스크립트를 생성합니다:
#!/usr/bin/env node
// scripts/sync-wrangler-config.mjs
//
// Terraform 출력을 읽고 올바른 리소스 ID로 wrangler.jsonc를 작성합니다.
// 로컬 또는 CI에서 실행 — 동일한 명령어, 동일한 결과.
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, "..");
// --- 구성 ---
// 프로젝트 레이아웃에 맞게 이러한 경로를 조정하세요.
const TERRAFORM_DIR = join(ROOT, "terraform");
const WRANGLER_CONFIG_PATH = join(ROOT, "wrangler.jsonc");
const TEMPLATE_PATH = join(ROOT, "wrangler.template.jsonc");
// --- 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은 각 값을 { value, type, sensitive }로 래핑합니다
const flat = {};
for (const [key, entry] of Object.entries(outputs)) {
flat[key] = entry.value;
}
return flat;
} catch (err) {
console.error("Terraform 출력을 읽지 못했습니다. 'terraform apply'를 실행했습니까?");
console.error(err.message);
process.exit(1);
}
}
// --- Wrangler 설정 생성 ---
function generateConfig(outputs) {
if (!existsSync(TEMPLATE_PATH)) {
console.error(`템플릿을 찾을 수 없음: ${TEMPLATE_PATH}`);
console.error("__PLACEHOLDER__ 토큰이 있는 wrangler.template.jsonc를 생성하세요.");
process.exit(1);
}
let template = readFileSync(TEMPLATE_PATH, "utf-8");
// __PLACEHOLDER__ 토큰을 Terraform 출력 값으로 교체합니다.
// 토큰 형식: __UPPER_SNAKE_CASE__ (terraform 출력 이름에 매핑)
for (const [key, value] of Object.entries(outputs)) {
const token = `__${key.toUpperCase()}__`;
template = template.replaceAll(token, String(value));
}
// 대체되지 않은 토큰에 대한 경고
const remaining = template.match(/__[A-Z_]+__/g);
if (remaining) {
console.warn("경고: 템플릿에서 대체되지 않은 토큰:", [...new Set(remaining)]);
}
writeFileSync(WRANGLER_CONFIG_PATH, template, "utf-8");
console.log(`작성됨 ${WRANGLER_CONFIG_PATH}`);
}
// --- 메인 ---
const outputs = getTerraformOutputs();
generateConfig(outputs);
3단계: 템플릿
wrangler.template.jsonc — Terraform 출력이 들어갈 자리에 플레이스홀더 토큰이 있는 Wrangler 설정을 생성합니다:
{
// wrangler.template.jsonc에서 scripts/sync-wrangler-config.mjs에 의해 생성됨
// 직접 편집하지 마세요 — 대신 템플릿을 편집하세요.
"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__"
}
]
}
4단계: 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는 (predeploy 훅을 통해) sync를 먼저 자동으로 실행합니다. 동일한 npm run deploy 명령어가 로컬과 CI/CD에서 작동합니다 — DRY.
5단계: .gitignore
# 동기화 스크립트에 의해 생성됨 — 커밋하지 마세요
wrangler.jsonc
# Terraform
terraform/.terraform/
terraform/*.tfstate*
생성된 wrangler.jsonc는 커밋되지 않아야 합니다. 템플릿과 동기화 스크립트가 정보의 원천입니다.
CI/CD 통합
CI/CD 파이프라인은 개발자가 로컬에서 실행하는 것과 정확히 동일한 명령어를 실행합니다. 유일한 차이점은 Terraform 상태와 자격 증명이 제공되는 방식입니다.
일반적인 파이프라인 구조
# 의사 코드 — CI/CD 플랫폼의 구문에 맞게 조정하세요
steps:
# 1. Terraform이 인프라 변경 사항을 적용합니다
- name: terraform
run: |
cd terraform
terraform init
terraform apply -auto-approve
# 2. 브리지 스크립트가 Terraform 출력을 Wrangler 설정에 동기화합니다
# 3. Wrangler가 애플리케이션을 배포합니다
- name: deploy-worker
run: npm run deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
그게 전부입니다. npm run deploy는 predeploy → sync → wrangler deploy를 호출합니다. 로컬과 동일합니다.
자격 증명
- Terraform: 인프라 리소스를 관리할 권한이 있는 Cloudflare API 토큰이 필요합니다.
CLOUDFLARE_API_TOKEN환경 변수 또는 공급자 구성을 통해 제공합니다. - Wrangler: Worker를 배포할 권한이 있는 Cloudflare API 토큰이 필요합니다. 종종 동일한 토큰이지만, 최소 권한을 위해 별도로 범위를 지정할 수 있습니다.
- Terraform 상태: CI와 로컬이 동일한 상태를 공유하도록 원격 백엔드(S3, GCS, Terraform Cloud 등)를 사용합니다.
환경 승격
다중 환경 설정(스테이징, 프로덕션)의 경우 Terraform 워크스페이스 또는 별도의 상태 파일과 Wrangler 환경을 사용합니다:
// 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__" }]
}
}
}
동기화 스크립트는 이를 자연스럽게 처리합니다 — 해당 Terraform 출력을 추가하기만 하면 됩니다.
대안: Terraform 관리 바인딩
모든 것(배포 포함)에 대해 Terraform을 단일 정보 소스로 원하는 팀의 경우 cloudflare_worker_version 리소스가 바인딩을 직접 관리할 수 있습니다. 이 모델에서 Wrangler는 코드만 번들링하고 Terraform이 업로드 및 바인딩 연결을 처리합니다:
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
}
]
}
이 접근 방식의 장점:
wrangler deploy --dry-run --outdir dist는 코드를 번들링합니다- Terraform이 번들을 업로드하고 리소스 참조를 통해 모든 바인딩을 연결합니다
- 브리지 스크립트, 템플릿 없음 — Terraform이 ID를 기본적으로 확인합니다
트레이드오프: 바인딩 구성에 대한 Wrangler의 로컬 개발 편의성을 잃게 됩니다. wrangler dev를 위한 wrangler.toml이 여전히 필요하므로 약간의 중복이 발생합니다. 위의 브리지 패턴은 개발자가 wrangler dev를 적극적으로 사용할 때 일반적으로 더 좋으며, Terraform 전용 접근 방식은 로컬 개발이 모의 객체나 최소 구성을 사용하는 CI 우선 워크플로우에 적합합니다.
팀이 정기적으로 wrangler dev를 실행하는 경우 브리지 패턴을 선택하세요. Terraform이 이미 배포 도구이고 로컬 개발에 실제 바인딩이 필요하지 않은 경우 Terraform 관리 바인딩을 선택하세요.
타입 안전성: wrangler types --check
Wrangler 설정을 생성하거나 업데이트한 후 wrangler types를 실행하여 TypeScript 바인딩을 재생성합니다. CI에서는 --check를 사용하여 타입이 오래된 경우 빌드를 실패시킵니다:
{
"scripts": {
"typecheck": "wrangler types --check",
"predeploy": "npm run sync && npm run typecheck",
"deploy": "wrangler deploy"
}
}
이렇게 하면 배포 전에 Terraform 관리 리소스와 Worker의 TypeScript 타입 간의 불일치를 포착할 수 있습니다.
기존 프로젝트에 적용
이미 존재하는 프로젝트에 이 패턴을 적용할 때:
-
기존 상태를 감사합니다. 현재
wrangler.toml/wrangler.jsonc에서 하드코딩된 리소스 ID를 확인합니다. Terraform이 이미 사용 중인지 확인합니다. CI/CD 구성을 확인합니다. -
리소스를 식별합니다. Wrangler 설정의 모든 리소스 ID를 나열합니다. 이미 Terraform에 있는 것과 가져와야 하는 것을 확인합니다.
-
기존 리소스를 Terraform으로 가져옵니다. 재생성하는 대신
terraform import를 사용합니다. 이렇게 하면 가동 중지 시간과 데이터 손실을 방지할 수 있습니다. -
템플릿을 생성합니다. 기존 Wrangler 설정을
wrangler.template.jsonc로 복사하고 하드코딩된 ID를__PLACEHOLDER__토큰으로 바꿉니다. -
브리지 스크립트를 작성합니다. 위의 참조 스크립트로 시작하여 경로와 출력 이름을 조정합니다.
-
package.json을 연결합니다.
sync,predeploy,deploy스크립트를 추가합니다. -
CI/CD를 업데이트합니다. 수동 ID 전달 또는 구성 생성 단계를
npm run deploy로 바꿉니다. -
생성된 파일이 작동하는지 확인한 후
wrangler.jsonc를.gitignore에 추가합니다.
일반적인 패턴
모노레포의 여러 Worker
리포지토리에 여러 Worker가 포함된 경우 각 Worker는 자체 템플릿을 가지며 동기화 스크립트가 모든 것을 처리합니다:
project/
├── terraform/
├── workers/
│ ├── api/
│ │ ├── src/
│ │ └── wrangler.template.jsonc
│ └── cron/
│ ├── src/
│ └── wrangler.template.jsonc
├── scripts/
│ └── sync-wrangler-config.mjs
└── package.json
동기화 스크립트는 Worker 디렉토리를 반복하고 각 템플릿에 동일한 토큰 교체 논리를 적용합니다.
시크릿
시크릿은 브리지 스크립트나 템플릿을 통해 전달되지 않아야 합니다. wrangler secret put(대화형) 또는 wrangler secret bulk(JSON 파일에서, CI에 유용)를 사용하세요. 시크릿은 Cloudflare API에서 Worker별로 관리되며 설정 파일에 있지 않습니다.
CI에서:
echo '{"API_KEY":"...","DB_PASSWORD":"..."}' | wrangler secret bulk
미리보기 환경
PR 기반 미리보기 배포의 경우 동기화 스크립트가 환경 인수를 받을 수 있습니다:
{
"scripts": {
"deploy:preview": "npm run sync -- --env preview && wrangler deploy --env preview"
}
}
문제 해결
| 증상 | 가능한 원인 |
|---|---|
sync 실패 "Failed to read Terraform outputs" | Terraform이 적용되지 않았거나 잘못된 디렉토리에 있습니다. 먼저 terraform apply를 실행하세요. |
생성된 설정에 대체되지 않은 __TOKENS__ | Terraform 출력 이름이 토큰과 일치하지 않습니다. terraform output 이름이 대문자 토큰 패턴과 일치하는지 확인하세요. |
| Wrangler 배포 실패 "resource not found" | Terraform의 리소스 ID가 오래되었거나 잘못된 환경에서 가져온 것입니다. terraform apply를 다시 실행한 다음 npm run sync를 실행하세요. |
| Terraform 상태와 Cloudflare 간의 드리프트 | 리소스가 Terraform 외부에서 수정되었습니다(아마도 Wrangler 자동 프로비저닝에 의해). terraform plan을 실행하여 감지하고 조정하세요. |
참조
scripts/sync-wrangler-config.mjs— 브리지 스크립트 (인수 구문 분석 및 다중 Worker 지원이 포함된 프로덕션 준비 버전은scripts/디렉토리 참조)references/terraform-wrangler-mapping.md— Terra
…
(번역 중단됨)