인프라는 두 층이다 — Control Plane은 IaC, Data Plane은 API
딸깍 PaaS를 '테라폼 같은 IaC로 정교하게' 만들 수 있냐는 질문에서 출발. 7개 PaaS(Vercel·Netlify·Railway·Render·Fly·Supabase·Cloudflare) 실제 방식과 1차 출처를 확인해보니 답은 '두 층으로 나눠라'였다. 우리 코드가 이미 가진 것과 빠진 것까지 정리한 학습노트.
질문
딸깍 PaaS는 지금 유저 앱을 배포할 때 Cloudflare REST API를 직접 호출한다. 문득 든 의심: "이거 좀 손으로 짠 느낌인데, 테라폼 같은 IaC로 하면 더 정교한 거 아냐?"
직감으로 답하기 싫어서 실제 PaaS 7곳이 어떻게 하는지 1차 출처로 확인했다. 결론이 생각보다 깔끔해서 학습노트로 박제한다.
핵심 멘탈모델 — 인프라는 한 종류가 아니다
같은 "인프라"라도 성격이 정반대인 두 층이 있고, 맞는 도구가 층마다 뒤집힌다.
| Control Plane (내 것) | Data Plane (유저별) | |
|---|---|---|
| 누가·언제 만드나 | 나, 가끔 (셋업·설정 변경) | 모든 유저, 클릭할 때마다 |
| 개수 | 몇 개 안 되고 오래 삶 | 수천 개, 가입자 수 따라 증가 |
| 트리거 | 운영자가 커밋 | 실시간 유저 액션 |
| 성격 | 선언적·정적·상태보존 | 명령적·이벤트기반·실시간 |
| 맞는 도구 | Terraform / IaC | API 직접 호출 |
도구가 뒤집히는 이유는 비용 구조가 뒤집히기 때문이다. IaC의 강점(리뷰된 단일 desired-state, 잠긴 상태파일, plan→수렴 사이클)은 사람이 가끔 기반 인프라를 바꿀 땐 자산이지만, 똑같은 기계가 낯선 사람들의 버튼 클릭에 시간당 수천 번 얻어맞으면 그대로 부채(잠금 경합·지연·반쯤 적용된 깨진 상태)가 된다.
업계 표준 프레이밍은 AWS의 "control plane vs application plane" 백서고, Omnistrate는 아예 한 문장으로 못 박는다: "Terraform은 선언적·정적이다. 실시간 유저 액션으로 테넌트를 동적으로 띄우려면 API 기반 프로비저닝 모델의 control plane이 필요하다."
숫자와 사실 — 7개 PaaS 전부 유저 배포는 API다
per-tenant 런타임 프로비저닝을 terraform apply로 하는 곳은 한 곳도 없었다. 전부
프로그래밍 API다:
- Vercel —
POST /v13/deployments(문서가 "multi-tenant applications에 특히 유용"이라 명시) - Netlify — Netlify API로 사이트·배포 생성 ("create sites programmatically")
- Railway — 대시보드를 굴리는 바로 그 GraphQL API
- Render — REST API, 문서가 "고객별 격리 환경을 띄우는 SaaS 플랫폼"을 콕 집음
- Fly.io — Machines API ("Fly Launch의 오케스트레이션을 이 API로 만든다")
- Supabase — Management API
POST /v1/projects("유저를 대신해 프로젝트 생성") - Cloudflare WfP — "고객 코드를 받아 API 요청으로 user Worker를 네임스페이스에 배포"
특히 Cloudflare는 두 가지를 못 박는다:
- 유저 배포 경로 = API 요청 (테라폼 언급 자체가 없음)
- "고객마다 네임스페이스 만들지 마라" — 전 고객이 하나의 dispatch 네임스페이스를 공유
왜 유저 배포에 테라폼이 "틀린" 도구인가 (그냥 '불필요'가 아니라)
- v5 프로바이더는 그 동작을 아예 못 한다. Cloudflare TF 프로바이더 v5에는 dispatch
네임스페이스에 유저 스크립트를 올리는 인자(
dispatch_namespace)가 삭제됐다(v4엔 있었음, v4.27에서 이슈 #2426 해결로 추가됐던 게 v5 재작성 때 누락). 네임스페이스 컨테이너 자체(cloudflare_workers_for_platforms_dispatch_namespace)는 만들 수 있지만 그 안의 테넌트 스크립트는 못 넣는다. - 상태 잠금이 모든 배포를 직렬화한다. 테라폼은 상태를 쓰는 모든 작업에 자동으로 락을 건다 → 공유 상태 하나면 모든 유저 배포가 뮤텍스 하나 뒤에 줄 선다.
- 테넌트별 상태파일은 폭발한다. 락을 피하려 고객마다 상태를 나누면 → 수천 개의 상태 백엔드를 저장·보안·잠금·백업·GC 해야 함. 가입할 때마다 프로그래밍으로 상태를 찍어내는 건 테라폼이 설계된 용도가 아니다.
- apply는 API 호출이 아니라 plan+수렴 사이클이다. 그리고 실패 시 "테라폼은 인프라를 깨진 상태로 남긴다" — 비개발자가 방금 "배포" 누른 상황에 용납 불가.
- Cloudflare가 이미 상태파일이 주려던 보장을 준다. WfP의 첫 User Worker 업로드는 이제
동기식 —
200 OK면 스크립트가 완전히 프로비저닝돼 트래픽 받을 준비 완료. 상태파일 없이도 "클릭 = 프로비저닝 완료"가 성립.
그럼 IaC는 어디에 쓰나 (Control Plane)
우리가 가끔 만드는 기반 객체엔 테라폼이 딱이다 — dispatch 네임스페이스 1개(고객마다가 아니라 전체 1개니 진짜 control-plane), 라우팅하는 dispatcher Worker, DNS/zone, 플랫폼 자체를 받치는 공유 D1/KV/R2, 계정 설정. 여기서 얻는 게 IaC의 진짜 값: 리뷰(대시보드 오조작이 아니라 diff), 재현(같은 코드로 스테이징/2번째 계정 세움), 롤백(커밋 되돌리고 재적용).
예외는? — Pulumi Automation API
embedded IaC(Pulumi Automation API: up/destroy를 라이브러리 호출로, 테넌트별 stack 격리)는
진짜 예외다. 단 테넌트마다 인프라가 다중 리소스 그래프일 때(전용 DB 클러스터·VPC·K8s).
CockroachDB·Snowflake·Lemonade가 이렇게 쓴다. 하지만 우리 테넌트 단위는 공유 네임스페이스에
스크립트 하나 올리는 것 — 동기식으로 멱등하게 프로비저닝되는 API 객체 하나다. 여기에
Automation API를 쓰면 런타임 CLI 의존성 + 테넌트별 상태 백엔드 + 지연을 사서 거의 안 쓰는
보장을 얻는 꼴. 변곡점은 테넌트 수가 아니라 테넌트당 그래프 복잡도 + teardown/drift 위험.
(CDKTF는 고려 말 것 — HashiCorp가 2025-12-10 아카이브함.)
우리 코드는 지금 어디까지? — 가진 것 vs 빠진 것
코드를 file:line으로 감사한 결과. 어려운 절반(상태머신+멱등성)은 이미 있고, 나머지 절반(control loop: 인벤토리→reconcile→teardown)이 없다.
| IaC 원칙 | 우리 코드 | 할 일 |
|---|---|---|
| 상태머신 | ✅ 있음·강제 (deployStore.ts 전이표 + sqliteStores.ts compare-and-swap) | 유지 |
| 멱등성 | ✅ 2겹 (createD1 재사용, PUT 업서트, idempotency_key 원자 dedup) | 프로바이더 단계까지 확장 |
| 리소스 인벤토리 | ⚠️ 부분 — 배포 후 D1 uuid를 버린다(cloudflareExecutor.ts가 cf_ref만 기록) | 만든 CF 리소스 id 전부 저장 |
| drift 감지 | ❌ 프리미티브(getScriptStatus)는 있는데 호출하는 곳이 없음 | 주기적 sweep에 연결 |
| teardown | ❌ 전무 — DELETE 라우트 0, rolled_back은 선언만 되고 아무도 전이 안 함 | 삭제 경로 추가 (안 하면 리소스 무한 누수) |
| 반쯤 실패 복구 | ❌ failed가 종점, 보상 없음 → deployScript 실패 시 D1 고아 발생 | 재진입 또는 보상 로직 |
가장 급한 건 teardown + 인벤토리: 지금은 실패/삭제된 앱마다 Worker와 D1이 조용히 새고, 뭘 지워야 하는지 기록조차 없다. 비개발자 클릭 하나하나가 쌓이는 비용·정합성 버그.
결론 한 줄
"IaC로 전부"가 정교한 게 아니다. 층마다 맞는 도구가 정교한 거다. Control plane엔 테라폼(리뷰·재현·롤백), data plane엔 Cloudflare API 직접(그게 CF 문서·업계 표준이고 테라폼은 여기선 덜 정교함). 그리고 IaC의 도구가 아니라 원칙(인벤토리·reconcile·teardown·멱등성)을 우리 손코딩 REST 클라이언트 위에 얹는 게 진짜 "제대로."
자기점검 질문 (나중의 나에게)
- 어떤 리소스가 control plane이고 어떤 게 data plane인지 5초 안에 구분할 기준은? (누가·얼마나 자주 만드나)
- 유저 배포마다
terraform apply가 왜 직렬화되나? (상태 잠금) - "테넌트마다 상태파일"의 대가는? 왜 테라폼이 그걸 못 견디나?
- Cloudflare가 상태파일 없이도 "클릭=완료"를 주는 근거 한 줄은? (동기식 200 OK)
- Pulumi Automation API를 꺼낼 변곡점은 테넌트 '수'인가 '무엇'인가?
- 우리 코드가 이미 가진 IaC 원칙 2개와, 없어서 리소스가 새는 원칙 1개는?