English {#english}
Prego SaaS Unified Architecture — Hetzner, Cloudflare, Zuplo
Document version: 1.1
Purpose: From a Global Principal Architect perspective, unify and document the Prego folder structure and SaaS automation architecture that integrates Hetzner, Cloudflare, and Zuplo. Define how the existing 6-step onboarding logic and IaC (Pulumi + Ansible) operate within the current structure. Intelligent automation: End-to-end automation Pulumi→Ansible→Zuplo→provision_jobs, Control Plane resource and usage monitoring, and AI-based server placement decisions (§9).
Code scope: This document is planning only; code generation is done on separate request.
1. Unified Folder Structure (Refined)
Target structure that keeps the existing layout and places IaC (Infra) and onboarding logic (App) according to SRE best practices.
1.1 Target Directory Structure
Prego/├── .github/workflows/ # CI/CD: Pulumi run and tenant deploy automation├── infra/ # Pulumi: Hetzner servers, Cloudflare DNS/SSL│ ├── providers/ # Hetzner, Cloudflare provider config│ ├── index.ts (or pulumi/) # Main infra definition (cx21 server spec)│ └── zuplo_sync.ts # Zuplo Gateway API auto-register script├── config/ # Ansible: Docker install, Frappe+HR setup│ ├── roles/ # Docker engine and Frappe Bench roles│ └── playbook.yml # Multitenancy (new-site) automation├── apps/ # Applications (Cloudflare Pages / Next.js etc.)│ ├── client-web/ # Employee onboarding and app UI│ │ ├── app/ # Routes, pages, API Routes│ │ ├── lib/ # validations (Zod), formulas (Pro-rata etc.)│ │ └── content/ # PDPA notices (i18n)│ └── auth-worker/ # (Optional) Magic Link, OTP edge logic├── prego-zuplo/ # Zuplo API Gateway: D1, Rate Limit, CORS└── docs/ # PDPA notices, runbooks, planning docs1.2 Mapping to Current Prego Structure
| Target structure | Current Prego | Notes |
|---|---|---|
| Pulumi | prego-pulumi/ (Python Pulumi + Cloudflare) | Add Hetzner provider and zuplo_sync |
config/ (Ansible) | infra/ansible/ or separate repo | Consolidate Docker, Frappe Bench, new-site roles |
apps/client-web | apps/client-web/ (Next.js App Router) | 6-step onboarding, Pages deploy |
apps/client-web/lib | lib/schemas/, lib/data/ | Zod validation, Pro-rata formulas |
apps/client-web/content | content/onboarding-notice/, onboarding-affirmation/ | PDPA and consent i18n |
| API, D1, Magic Link | prego-zuplo + Auth Worker | Via Zuplo or direct Worker |
docs/ | docs/planning/, docs/deployment/ | Planning and Cloudflare docs |
1.3 IaC and Onboarding Integration Points
- Infra: Pulumi creates per-tenant server, DNS, SSL → Ansible creates Frappe site and Administrator API Key.
- App: Cloudflare Pages (client-web) serves onboarding UI and API Routes (Pages Functions) or calls Zuplo backend.
- Gateway: Zuplo manages per-tenant Rate Limiting, CORS, API Keys;
zuplo_sync.tsauto-registers new tenants when needed.
1.4 Auto Tenant Creation After Signup, Plan Request, and Payment (Queue, GitHub, Worker)
When a new user completes signup → plan request → payment on the web, tenant provisioning runs automatically according to plan. See tenant-provisioning-flow.md §A for steps.
| Step | Owner | Description |
|---|---|---|
| 1. Payment complete | Stripe | Sends webhook on Checkout complete or Subscription create/update |
| 2. Webhook receive, verify, idempotency | Control Plane Worker | POST /webhooks/stripe — signature verification, provider_events to avoid duplicates |
| 3. Enqueue | D1 | No separate queue. Insert row in provision_jobs with status=Pending. Also update tenants_master, billing_customers, subscriptions, trace_events, audit_logs |
| 4. Placement decision and provisioning trigger | Control Plane | AI placement engine uses monitoring data to choose existing server vs new server. Calls GitHub workflow_dispatch with job_id, tenant_id, region, (optional) target_server_id — see §9 |
| 5. Infra, config, gateway in one run | GitHub Actions / Provision Worker | Pulumi Up (if needed) → Ansible (Docker+Frappe+new-site+API Key) → Zuplo Sync → provision_jobs.status → Completed, tenants_master update — full automation (§9.2) |
| 6. Completion update | Control Plane | Workflow/Worker callback sets D1 provision_jobs.status=Completed, tenants_master.status=Active, updates tenant_runtime (server, site, API Key) |
Summary: Signup → plan → payment → Stripe Webhook → Control Plane Worker → D1 provision_jobs (queue) + AI placement → workflow_dispatch (or Provision Worker) → Pulumi→Ansible→Zuplo→D1 update, all automated. See §9 for intelligent automation.
2. Technical Workflow: One-Click Tenant Build
When the user clicks 「Create new tenant」 or payment completes (§1.4) and a webhook is received, the following process runs atomically.
2.1 Infra Layer (Pulumi)
- Provision cx21 server instances via Hetzner API.
- Register tenant DNS with approach C (internal canonical + user CNAME). See tenant-subdomain-dns-design.md.
| Step | Content | Owner |
|---|---|---|
| 1 | Provision cx21 server instance via Hetzner API | Pulumi Hetzner Provider |
| 2 | Per-tenant DNS: internal tenant-{short_id}.pregoi.com (short_id = first 8 chars of tenant_id UUID) → server/Tunnel; user {subdomain_slug}.pregoi.com CNAME → canonical | Provisioning step, Cloudflare API |
| 3 | Enable SSL (Full/Full Strict etc.) in Cloudflare | Pulumi Cloudflare |
- Chosen approach: Only internal
tenant-a1b2c3d4.pregoi.comis fixed 1:1 to server and Frappe site. Users reach e.g. acme.pregoi.com; routing uses only the canonical name. - Note:
prego-pulumi/integrates Cloudflare and Hetzner; Hetzner resources live in region stacks (sg/us/eu). Per-tenant DNS records are created dynamically via Cloudflare API in the provisioning workflow/Worker, not by Pulumi.
2.2 Config Layer (Ansible)
- Run Frappe HR on Docker and create DB-isolated environments with
bench new-site. - After install, create Administrator API Key.
| Step | Content | Owner |
|---|---|---|
| 1 | Install Docker engine on target server | Ansible role |
| 2 | Set up Frappe + HR on Docker | Ansible role |
| 3 | bench new-site for per-tenant DB isolation | playbook |
| 4 | Create Administrator API Key after install | playbook / script |
- Trigger: After Pulumi success (or when AI placement picks an existing server, Pulumi is skipped), run Ansible Playbook — GitHub Actions or Provision Worker runs it automatically (§9.2).
2.3 Gateway Layer (Zuplo)
- Register the generated API Key in Zuplo env vars so the edge can enforce per-tenant Rate Limiting and CORS.
| Step | Content | Owner |
|---|---|---|
| 1 | Register generated API Key in Zuplo env (or Secrets) | Auto: same pipeline runs zuplo_sync after Ansible (§9.2) |
| 2 | Apply per-tenant Rate Limiting and CORS at the edge | Zuplo Dashboard / Config as Code |
- Automation: Pipeline uses
zuplo_sync(or equivalent) to push new tenant API Keys to Zuplo. No manual step.
2.4 Workflow Order Summary
[Click Create new tenant / Payment complete] → Control Plane: D1 queue + AI placement (existing vs new server) (§9.4) → Pulumi: (if new server) Hetzner server + Cloudflare DNS/SSL → Ansible: Docker + Frappe new-site + API Key → Zuplo: API Key registration + tenant policy → Control Plane: provision_jobs and tenants_master update (full automation, §9) → (Optional) Dashboard or notification on completion3. Employee Onboarding & Data Security (Singapore PDPA)
How the existing 6-step onboarding ties into IaC and the gateway.
3.1 Multilingual Name Handling (Step 1)
| Item | Design |
|---|---|
| Mother Tongue selection | Step 1: when selected, switch to the local name input form. |
| Validation | Use Zod discriminatedUnion (or conditional schema) for mother-tongue–specific rules. |
| Location | apps/client-web/lib/schemas/onboarding and Step 1 UI. |
3.2 Sensitive Data: NRIC/FIN
| Item | Design |
|---|---|
| Legal notice | Place PDPA and MOM notices above the input field. |
| Masking | Mask on input and display to limit secondary exposure. |
| Storage | Encrypt in transit; minimize storage access. |
3.3 Retention — D1 to Frappe Transfer
| Item | Design |
|---|---|
| Temporary storage | Use Cloudflare D1 for onboarding session and metadata. |
| Delete after transfer | On successful Frappe transfer, delete the D1 record immediately to avoid data left behind. |
| On failure | Per §4.3 Fail Sanely: keep D1 data and send Alert. |
3.4 6-Step Onboarding and Current Implementation
- Current steps: Profile → Contact → Address+Banking → Work+Leave+Approvers → Review → Affirmation (6 steps).
- Relation to IaC: Onboarding URL and company status API are exposed only for tenants where creation (Pulumi+Ansible+Zuplo) has completed.
4. Principal Architect Operational Recommendations
4.1 KISS Principle
| Recommendation | Content |
|---|---|
| Single deployment unit | Avoid many separate Workers; use Pages Functions to keep frontend and backend in one project and reduce toil. |
| Application | Choose one consistent path for onboarding API (Pages Functions or Zuplo backend) to keep maintenance cost low. |
4.2 Magic Link Security
| Recommendation | Content |
|---|---|
| TTL | Set 48-hour TTL on edit links sent to personal email. |
| Extra auth | Recommend extra check (e.g. date of birth) on access to limit impact if token is leaked. |
4.3 Error Handling: Fail Sanely
| Recommendation | Content |
|---|---|
| On Frappe sync failure | Do not delete D1 data immediately; keep it. |
| Alert | Send Alert to iPrego tech support so they can act or retry. |
| Goal | Avoid data loss and preserve time to respond. |
5. Next Steps (Suggested Implementation Order)
Below are possible next tasks from this plan. No code is generated in this document; that is done on separate request.
| Order | Module | Description |
|---|---|---|
| 1 | Pulumi server creation | Hetzner cx21 server and Cloudflare DNS/SSL in prego-pulumi/. |
| 2 | D1 table spec | Document D1 schema and migrations for onboarding session and metadata. Include server_metrics, servers (§9.3). |
| 3 | Ansible playbook | Single playbook/roles for Docker + Frappe Bench + new-site + API Key. |
| 4 | Zuplo sync script | Spec for zuplo_sync (or equivalent) to register new tenant API Keys in Zuplo. |
| 5 | End-to-end automation | GitHub Actions or Provision Worker: Pulumi → Ansible → Zuplo → provision_jobs update (full automation §9.2). |
| 6 | Control Plane resource and usage monitoring | Collect and store per-server CPU/memory/disk/tenant count and service usage (D1/server_metrics etc.). §9.3. |
| 7 | AI-based server placement | Use monitoring to decide existing server vs new server. Rules engine or model. §9.4. |
Once you decide which module to implement first, we can propose an implementation plan or code for that module.
6. References
| Document | Purpose |
|---|---|
docs/planning/tenant-provisioning-flow.md | Signup and payment → queue (provision_jobs) → AI placement and full automation (§A, §B); Pulumi→Ansible→Zuplo→Cloudflare; Control Plane monitoring and AI placement (§B). |
docs/planning/intelligent-automation-implementation-plan.md | Intelligent automation implementation order, deliverables, dependencies, and verification checklist (for §9). |
docs/planning/provision-tenant-workflow-design.md | Full automation workflow (provision-tenant.yml / Provision Worker): design, I/O, callback spec. |
docs/planning/rag-ai-edge-architecture-plan.md | RAG AI full edge architecture, Zuplo, Workers, Vectorize, D1, R2, enterprise features, Phase 1–4 roadmap. |
docs/planning/tenant-subdomain-dns-design.md | Tenant DNS approach C: internal canonical tenant-{short_id}.pregoi.com + user acme.pregoi.com CNAME; short_id = first 8 chars of tenant_id UUID; routing via canonical only. |
docs/planning/cloudflare-orchestration-and-gtape-backup-plan.md | Cloudflare Workers dynamic tenant routing (KV), Tunnel Gatekeeper (Zero Trust), GTape 4-phase backup and restore (Soft Delete, R2, DiRT). |
docs/planning/onboarding-multitenant-d1-zuplo-plan.md | Onboarding, D1, Zuplo API design. |
docs/planning/onboarding-step2-step7-defaults-and-affirmation-plan.md | Step defaults and consent text. |
docs/deployment/pulumi-setup-and-placement.md | Pulumi execution location and project layout. |
docs/deployment/cloudflare-pages-integration.md | Cloudflare Pages deployment. |
infra/README.md | Infra directory policy. |
Sections 7 (Enterprise-Grade Module), 8 (LogPath Event Tracking), and 9 (Intelligent Automation) are translated in the same structure below in the Korean section. See 한국어 for full detail including §7–§9.
한국어 {#korean}
Prego SaaS 통합 아키텍처 기획서 — Hetzner·Cloudflare·Zuplo 연동
문서 버전: 1.1
작성 목적: 30년 경력 Global Principal Architect 관점에서 Prego 프로젝트의 폴더 구조와 Hetzner, Cloudflare, Zuplo를 연동하는 SaaS 자동화 아키텍처를 하나로 통합·정리. 기존 6단계 온보딩 로직과 IaC(Pulumi + Ansible) 환경이 현재 구조 내에서 유기적으로 동작하도록 정의. 지능형 자동화: Pulumi→Ansible→Zuplo→provision_jobs 전 구간 자동화, Control Plane 리소스·사용량 모니터링, AI 기반 서버 배치 결정 포함(§9).
코드 생성 범위: 본 문서는 기획만 포함하며, 코드 생성은 별도 요청 시 진행.
1. 통합 폴더 구조 (Refined)
기존 구조를 유지하면서 **IaC(Infra)**와 **온보딩 로직(App)**을 SRE 베스트 프랙티스에 따라 배치한 목표 구조입니다.
1.1 목표 디렉터리 구조
Prego/├── .github/workflows/ # CI/CD: Pulumi 실행 및 테넌트 배포 자동화├── infra/ # Pulumi: Hetzner 서버, Cloudflare DNS/SSL 관리│ ├── providers/ # Hetzner, Cloudflare 프로바이더 설정│ ├── index.ts (또는 pulumi/) # 메인 인프라 정의 (cx21 서버 사양 반영)│ └── zuplo_sync.ts # Zuplo Gateway API 자동 등록 스크립트├── config/ # Ansible: Docker 설치 및 Frappe+HR 모듈 셋업│ ├── roles/ # Docker 엔진 및 Frappe Bench 실행 롤│ └── playbook.yml # 멀티테넌시(new-site) 자동화 실행서├── apps/ # 애플리케이션 (Cloudflare Pages / Next.js 등)│ ├── client-web/ # 직원 온보딩·앱 UI│ │ ├── app/ # 라우트, 페이지, API Routes│ │ ├── lib/ # validations(Zod), formulas(Pro-rata 등)│ │ └── content/ # PDPA 고지문(다국어)│ └── auth-worker/ # (선택) Magic Link·OTP 등 엣지 로직├── prego-zuplo/ # Zuplo API Gateway: D1 연동, Rate Limit, CORS└── docs/ # PDPA 고지문 정리, 운영 매뉴얼, 기획서1.2 현재 Prego 구조와의 매핑
| 목표 구조 | 현재 Prego 구조 | 비고 |
|---|---|---|
| Pulumi | prego-pulumi/ (Python Pulumi + Cloudflare) | Hetzner 프로바이더·zuplo_sync 추가 검토 |
config/ (Ansible) | infra/ansible/ 또는 별도 repo | Docker·Frappe Bench·new-site 롤 정리 |
apps/client-web | apps/client-web/ (Next.js App Router) | 6단계 온보딩, Pages 배포 |
apps/client-web/lib | lib/schemas/, lib/data/ | Zod 검증, Pro-rata 공식 |
apps/client-web/content | content/onboarding-notice/, onboarding-affirmation/ | PDPA·동의서 다국어 |
| API·D1·Magic Link | prego-zuplo + Auth Worker | Zuplo 경유 또는 Worker 직접 호출 |
docs/ | docs/planning/, docs/deployment/ | 기획서·Pulumi·Cloudflare 문서 |
1.3 IaC와 온보딩 로직의 연동 포인트
- 인프라: Pulumi로 테넌트별 서버·DNS·SSL 생성 → Ansible로 Frappe 사이트·Administrator API Key 생성.
- 앱: Cloudflare Pages(client-web)에서 온보딩 UI·API Routes(Pages Functions) 또는 Zuplo 백엔드 호출.
- 게이트웨이: Zuplo에서 테넌트별 Rate Limiting·CORS·API Key 관리; 필요 시
zuplo_sync.ts로 신규 테넌트 자동 등록.
1.4 신규 사용자 가입·패키지 신청·결제 후 자동 테넌트 생성 (Queue·GitHub·Worker)
새 사용자가 웹에서 가입 → 패키지 신청 → 결제를 완료하면, **패키지(플랜)**에 따라 즉시 테넌트 프로비저닝이 자동으로 진행되도록 설계되어 있다. 상세 단계는 tenant-provisioning-flow.md §A 참조.
| 단계 | 담당 | 설명 |
|---|---|---|
| 1. 결제 완료 | Stripe | Checkout 완료 또는 Subscription 생성/갱신 시 Webhook 발송 |
| 2. Webhook 수신·검증·멱등 | Control Plane Worker | POST /webhooks/stripe — 서명 검증, provider_events로 중복 방지 |
| 3. 큐에 넣기 | D1 | 별도 Queue 서비스 없음. provision_jobs 테이블에 status=Pending 행 삽입 = 큐 적재. 동시에 tenants_master, billing_customers, subscriptions, trace_events, audit_logs 갱신 |
| 4. 배치 결정·프로비저닝 트리거 | Control Plane | AI 배치 엔진이 모니터링 데이터를 바탕으로 기존 서버 배치 vs 신규 서버 생성 결정. GitHub API workflow_dispatch 호출 시 job_id, tenant_id, region, (선택) target_server_id 전달 — 상세 §9 |
| 5. 인프라·설정·게이트웨이 일괄 실행 | GitHub Actions / Provision Worker | Pulumi Up(필요 시) → Ansible(Docker+Frappe+new-site+API Key) → Zuplo Sync → provision_jobs.status → Completed, tenants_master 갱신까지 전 구간 자동화 (§9.2) |
| 6. 완료 반영 | Control Plane | 워크플로/Worker 콜백으로 D1 provision_jobs.status=Completed, tenants_master.status=Active, tenant_runtime(서버·사이트·API Key) 갱신 |
요약: 가입·패키지·결제 → Stripe Webhook → Control Plane Worker → D1 provision_jobs(큐) + AI 배치 결정 → workflow_dispatch(또는 Provision Worker) → Pulumi→Ansible→Zuplo→D1 갱신 전 구간 자동 실행. 지능형 자동화 상세는 §9 참조.
2. 기술적 워크플로우: 버튼 하나로 테넌트 구축
사용자가 「신규 테넌트 생성」 버튼을 누르거나, 결제 완료(§1.4) 로 Webhook이 들어오면, 다음 프로세스가 원자적(Atomic) 으로 실행됩니다.
2.1 인프라 레이어 (Pulumi)
- Hetzner API를 통해 cx21 서버 인스턴스를 즉시 프로비저닝합니다.
- 테넌트 DNS는 방식 C(내부 canonical + 사용자 CNAME) 로 등록합니다. 상세 tenant-subdomain-dns-design.md 참조.
| 단계 | 내용 | 담당 |
|---|---|---|
| 1 | Hetzner API로 cx21 서버 인스턴스 즉시 프로비저닝 | Pulumi Hetzner Provider |
| 2 | 테넌트별 DNS: 내부 tenant-{short_id}.pregoi.com (short_id = tenant_id UUID 앞 8자) → 서버/Tunnel; 사용자 {subdomain_slug}.pregoi.com CNAME → canonical | 프로비저닝 단계·Cloudflare API |
| 3 | Cloudflare에서 SSL 활성화(Full/Full Strict 등) | Pulumi Cloudflare |
- 채택 방식: 내부
tenant-a1b2c3d4.pregoi.com만 서버·Frappe site와 1:1 고정. 사용자는 acme.pregoi.com 등으로 접속하며, 실제 라우팅은 canonical로만 처리. - 참고:
prego-pulumi/는 Cloudflare·Hetzner 통합; Hetzner 리소스는 리전 스택(sg/us/eu)에 포함. 테넌트별 DNS 레코드는 Pulumi가 아닌 프로비저닝 워크플로/Worker에서 Cloudflare API로 동적 생성.
2.2 설정 레이어 (Ansible)
- Docker 위에 Frappe HR 환경을 구성하고,
bench new-site명령을 통해 데이터베이스 격리 환경을 구축합니다. - 설치 완료 후 Administrator API Key를 생성합니다.
| 단계 | 내용 | 담당 |
|---|---|---|
| 1 | 대상 서버에 Docker 엔진 설치 | Ansible role |
| 2 | Docker 위에 Frappe + HR 환경 구성 | Ansible role |
| 3 | bench new-site 로 테넌트별 DB 격리 환경 구축 | playbook |
| 4 | 설치 완료 후 Administrator API Key 생성 | playbook / script |
- 트리거: Pulumi 성공 후(또는 AI 배치로 기존 서버 지정 시에는 Pulumi 생략) Ansible Playbook 실행 — GitHub Actions 또는 Provision Worker에서 자동 연속 실행 (§9.2).
2.3 게이트웨이 레이어 (Zuplo)
- 생성된 API Key를 Zuplo 환경변수에 등록하여 엣지에서 테넌트별 Rate Limiting과 CORS를 제어합니다.
| 단계 | 내용 | 담당 |
|---|---|---|
| 1 | 생성된 API Key를 Zuplo 환경변수(또는 Secrets)에 등록 | 자동: Ansible 완료 후 동일 파이프라인에서 zuplo_sync 실행 (§9.2) |
| 2 | 엣지에서 테넌트별 Rate Limiting·CORS 정책 적용 | Zuplo Dashboard / Config as Code |
- 자동화: 파이프라인 내에서
zuplo_sync(또는 동등 스크립트)로 신규 테넌트 API Key를 Zuplo에 반영. 수동 단계 없음.
2.4 워크플로우 순서 요약
[신규 테넌트 생성 클릭 / 결제 완료] → Control Plane: D1 큐 적재 + AI 배치 결정(기존 서버 vs 신규 서버) (§9.4) → Pulumi: (신규 서버 필요 시) Hetzner 서버 + Cloudflare DNS/SSL → Ansible: Docker + Frappe new-site + API Key 생성 → Zuplo: API Key 등록 + 테넌트 정책 적용 → Control Plane: provision_jobs·tenants_master 갱신 (전 구간 자동화, §9) → (선택) 대시보드 또는 알림으로 완료 표시3. 직원 온보딩 & 데이터 보안 (싱가포르 PDPA 준수)
기존 6단계 온보딩 로직이 IaC·게이트웨이와 어떻게 맞물리는지 정의합니다.
3.1 다국어 이름 처리 (Step 1)
| 항목 | 설계 |
|---|---|
| Mother Tongue 선택 | Step 1에서 선택 시 로컬 성명 입력 폼을 동적 전환. |
| 검증 | Zod의 discriminatedUnion(또는 조건부 스키마)으로 모국어별 필드 규칙 적용. |
| 위치 | apps/client-web/lib/schemas/onboarding 및 Step 1 UI. |
3.2 민감 정보: NRIC/FIN
| 항목 | 설계 |
|---|---|
| 법적 주의 문구 | 입력 필드 위쪽에 PDPA·MOM 관련 고지 배치. |
| 마스킹 | 입력/표시 시 Masking 처리하여 2차 유출 방지. |
| 저장 | 전송 구간 암호화, 저장소 접근 최소화. |
3.3 보관 제한(Retention) — D1과 Frappe 이관
| 항목 | 설계 |
|---|---|
| 임시 저장 | Cloudflare D1으로 온보딩 세션·메타데이터 임시 관리. |
| 이관 후 삭제 | Frappe 이관 성공 시점에 D1 해당 레코드 즉시 삭제, 데이터 방치 원천 차단. |
| 실패 시 | §4.3 Fail Sanely 전략에 따라 D1 유지 + Alert. |
3.4 6단계 온보딩과 현재 구현
- 현재 단계: Profile → Contact → Address+Banking → Work+Leave+Approvers → Review → Affirmation (6단계).
- IaC와의 관계: 테넌트 생성(Pulumi+Ansible+Zuplo)이 완료된 테넌트에 대해서만 온보딩 URL·회사 상태 API가 노출되도록 연동.
4. Principal Architect의 운영 제언
4.1 KISS 원칙
| 제언 | 내용 |
|---|---|
| 통합 배포 단위 | 독립적인 Workers를 과도하게 늘리지 말고, Pages Functions를 활용해 프론트엔드와 백엔드 로직을 하나의 프로젝트로 통합 관리하여 Toil을 줄인다. |
| 적용 | 온보딩 API는 Pages Functions 또는 Zuplo 백엔드 중 일관된 한 축을 선택해 유지보수 비용을 낮춘다. |
4.2 Magic Link 보안
| 제언 | 내용 |
|---|---|
| TTL | 개인 이메일로 발송되는 수정 링크에 48시간 TTL(유효 기간) 설정. |
| 추가 인증 | 접속 시 생년월일 확인 등 추가 인증을 권장하여 토큰 유출 시 피해를 제한. |
4.3 에러 핸들링: Fail Sanely
| 제언 | 내용 |
|---|---|
| Frappe 동기화 실패 시 | D1 데이터를 즉시 삭제하지 않고 유지. |
| 알림 | iPrego 기술 지원팀에 즉시 Alert를 보내 수동 조치·재시도 가능하도록 한다. |
| 목적 | 데이터 유실 방지 및 장애 대응 시간 확보. |
5. 다음 단계 (구현 우선순위 제안)
아래는 기획서 기준 다음에 진행할 수 있는 작업 예시입니다. 코드는 본 문서에서 생성하지 않으며, 필요 시 별도 요청으로 진행합니다.
| 순서 | 모듈 | 설명 |
|---|---|---|
| 1 | Pulumi 서버 생성 스크립트 | prego-pulumi/ 내 Hetzner cx21 서버 생성 및 Cloudflare DNS/SSL 연동. |
| 2 | D1 테이블 정의서 | 온보딩 세션·메타데이터용 D1 스키마 문서화 및 마이그레이션 명세. server_metrics, servers 테이블 포함(§9.3). |
| 3 | Ansible playbook 정리 | Docker + Frappe Bench + new-site + API Key 생성까지 한 번에 실행되는 playbook·롤 정리. |
| 4 | Zuplo 동기화 스크립트 | 신규 테넌트 API Key를 Zuplo에 등록하는 zuplo_sync (또는 동등) 스크립트 명세. |
| 5 | 전 구간 자동화 오케스트레이션 | GitHub Actions 또는 Provision Worker에서 Pulumi → Ansible → Zuplo → provision_jobs 갱신까지 전 구간 자동 실행 (§9.2). |
| 6 | Control Plane 리소스·사용량 모니터링 | 서버별 CPU/메모리/디스크/테넌트 수·서비스 사용량 수집·저장(D1/server_metrics 등). §9.3. |
| 7 | AI 기반 서버 배치 결정 | 모니터링 데이터 기반으로 기존 서버 랜딩 vs 신규 서버 생성 판단. 규칙 엔진 또는 모델 연동. §9.4. |
구체적으로 어떤 모듈부터 코딩을 시작할지 정해 주시면, 해당 모듈에 맞춰 구현 계획 또는 코드를 이어서 제안하겠습니다.
6. 참조 문서
| 문서 | 용도 |
|---|---|
docs/planning/tenant-provisioning-flow.md | 가입·결제 → 큐(provision_jobs) → AI 배치·전 구간 자동화 흐름(§A·§B), Pulumi→Ansible→Zuplo→Cloudflare 단계, Control Plane 모니터링·AI 서버 배치(§B). |
docs/planning/intelligent-automation-implementation-plan.md | 지능형 자동화 구현 순서·산출물·의존 관계·검증 체크리스트(§9 구현 시 참조). |
docs/planning/provision-tenant-workflow-design.md | 5단계 전 구간 자동화 워크플로(provision-tenant.yml / Provision Worker) 설계·입출력·콜백 스펙(구현 전 기획). |
docs/planning/rag-ai-edge-architecture-plan.md | RAG AI Full Edge-Computing 아키텍처, Zuplo·Workers·Vectorize·D1·R2, 엔터프라이즈 기능·Phase 1–4 로드맵. |
docs/planning/tenant-subdomain-dns-design.md | 테넌트 DNS 채택 방식 C: 내부 canonical tenant-{short_id}.pregoi.com + 사용자 acme.pregoi.com CNAME. short_id = tenant_id UUID 앞 8자. 라우팅은 canonical로만 처리. |
docs/planning/cloudflare-orchestration-and-gtape-backup-plan.md | Cloudflare Workers 동적 테넌트 라우팅(KV)·Tunnel Gatekeeper(Zero Trust)·GTape 4단계 백업·복구(Soft Delete·R2·DiRT). 현재 시스템 적용 방안. |
docs/planning/onboarding-multitenant-d1-zuplo-plan.md | 온보딩·D1·Zuplo API 설계. |
docs/planning/onboarding-step2-step7-defaults-and-affirmation-plan.md | Step 기본값·동의서 본문. |
docs/deployment/pulumi-setup-and-placement.md | Pulumi 실행 위치·프로젝트 배치. |
docs/deployment/cloudflare-pages-integration.md | Cloudflare Pages 배포. |
infra/README.md | 인프라 디렉터리 정책. |
7. Prego SaaS 통합 관리 모듈 (Enterprise-Grade Edition)
아래 설계는 기존 구조의 강점을 유지하면서, D1 병목 제거 · 비동기 Provisioning · 정밀 Metering · PDPA 준수 강화를 반영한 확장형 모델입니다.
7.1 설계 원칙
- Control Plane과 Data Plane 분리
- Billing State와 Usage Raw Data 분리
- Provisioning 완전 비동기화
- Immutable Audit Trail 도입
- D1은 「상태 저장용」 으로만 사용
7.2 개선된 전체 아키텍처
Control Plane
- Cloudflare Pages Functions (Brain)
- D1 (Billing / Tenant State DB)
- Stripe Webhook Handler
- Zuplo Gateway
Data Plane
- R2 (Usage Raw Logs 저장)
- Scheduled Aggregator (Cron Trigger)
- Provision Worker (비동기 실행)
7.3 D1 역할 재정의 (병목 제거)
기존 문제
- 모든 Usage를 D1에 직접 기록
- Stripe Webhook 처리와 경쟁
- Quota 계산과 상태 변경이 동시에 발생
개선 방향
D1은 다음만 담당:
- Tenant 상태
- Billing 상태
- Quota 설정값
- Aggregated Usage 값
- Lifecycle 상태
Raw Usage 기록은 D1에 저장하지 않음.
7.4 Usage Metering 개선 설계
Step 1: Gateway 레벨 Soft Metering
- Zuplo Policy: Tenant ID 추출, 요청 카운트 증가(In-memory 또는 Edge Analytics), Soft Quota 초과 시 Warning
Step 2: Raw Usage 저장
- 각 요청별 로그를 R2에 JSON 로그 저장, 시간 단위 파일 분리 (예:
tenantA/2026-03-21-10.json) → D1 write 없음
Step 3: Scheduled Aggregation
- Cron Trigger(5분 또는 1시간 단위): R2에서 해당 기간 로그 읽기 → Tenant별 집계 → D1에 집계값만 Update (
UPDATE tenant_usage SET api_calls = api_calls + ? WHERE tenant_id = ?)
장점: D1 write 횟수 대폭 감소, Usage 기반 Billing 확장 가능, 정확도·스케일 대응
7.5 Billing 구조 개선
- Stripe Webhook: Event 검증, D1 상태 변경, Provisioning Queue 등록만 수행. Provisioning 직접 실행하지 않음.
- Idempotency: D1에
stripe_events (event_id TEXT PRIMARY KEY, processed_at DATETIME)등으로 중복 Webhook 방지.
7.6 Provisioning 비동기화
기존 문제: Webhook → GitHub Actions → Pulumi 실행이 동기적 연결, 실패 시 복구 어려움.
개선 구조
- Webhook 처리 시
provision_jobs (job_id, tenant_id, status ENUM('Pending','Running','Failed','Completed'), created_at)등록. - Cron Worker가 job 실행: Pulumi 실행 → Ansible 실행 → 성공 시 status = Completed, 실패 시 retry 정책 적용.
장점: Webhook 안정성, Retry 가능, 장애 격리, 인프라 중복 생성 방지.
7.7 Lifecycle 개선 (PDPA 강화)
- Soft Delete: status = Pending_Deletion → 접근 차단 → Stripe Subscription Cancel.
- Hard Purge (30일 후): Cron으로 Hetzner DB 삭제, R2 파일 삭제, Backup Snapshot 파기, D1 tenant row 삭제. Audit Log만 유지.
7.8 Immutable Audit Log 도입
- 새 테이블:
audit_logs (id, tenant_id, action_type, performed_by, reason, created_at). - Append-only 정책. Delete 금지.
7.9 멀티테넌시 전략 명확화
- SaaS 기본 모델: Shared Infra + Frappe Multi-site → 비용·운영 최적화.
- Enterprise Tier: Dedicated Hetzner Node → Plan Tier = Enterprise 시 전용 서버 생성.
7.10 Fail Sanely 정책 정교화
- Fail Open: Stripe 일시 장애·기존 Active 고객 → status = Active 유지, 7일 grace.
- Fail Closed: Fraud 의심·Subscription Deleted → 즉시 Suspend.
7.11 최종 데이터 흐름 요약
| 시나리오 | 흐름 |
|---|---|
| 신규 구독 | Stripe Checkout → Webhook → D1 status=Active → provision_jobs 등록 → Worker 실행 → Site 생성 → Zuplo Secret 등록 → Ready |
| 사용량 초과 | Zuplo Soft Throttle → Warning → Aggregator 반영 → Billing cycle 시 청구 |
| 해지 | Webhook → status=Suspended → 30일 타이머 → Hard Purge |
7.12 확장성 평가 (개선 후)
| 항목 | 평가 |
|---|---|
| D1 병목 | 해결 |
| Usage 확장성 | High |
| Billing 정확성 | High |
| Provision 안정성 | High |
| PDPA 준수 | Strong |
| Observability | 개선 가능 |
7.13 최종 평가
- 기존 설계: 8/10
- 개선 설계: 9.5/10 — Enterprise SaaS로 확장 가능.
7.14 다음 단계 제안 (Enterprise-Grade)
- D1 상세 SQL 스키마 전체 설계
- R2 기반 Usage Aggregator 코드
- Provision Worker 구조 코드
- Pulumi + Ansible 자동화 흐름 설계
- Zuplo Policy 예시 코드
8. LogPath 기반 이벤트 트랙(Event Tracking) 통합 설계
기존 개선 아키텍처(Control/Data Plane 분리, 비동기 Provisioning, R2 기반 Usage 집계)에 LogPath(Event Sourcing 기반 경로 추적 모델) 를 도입합니다.
8.1 LogPath 개념 정의
LogPath는 단순 로그가 아니라,
「하나의 비즈니스 이벤트가 시스템을 통과하는 경로(Path)를 시간 순으로 연결한 추적 체계」
입니다.
예: Stripe → Webhook → D1 → Provision Queue → Pulumi → Frappe 전체 흐름을 하나의 Trace 로 묶습니다.
8.2 설계 목표
- Event 단위 추적 가능
- Billing ↔ Provision ↔ Lifecycle 상관관계 확보
- 장애 발생 시 Root Cause 추적
- PDPA 감사 대응 가능
- SEV1 분석 시간 단축
8.3 LogPath 구조
Trace 모델: 각 SaaS 이벤트는 trace_id, tenant_id, event_type, event_source, current_stage, status, created_at, updated_at 등을 가짐.
Stage 모델 예시 (신규 구독)
- STRIPE_WEBHOOK_RECEIVED → 2. BILLING_STATUS_UPDATED → 3. PROVISION_JOB_CREATED → 4. INFRA_RUNNING → 5. SITE_CREATED → 6. GATEWAY_REGISTERED → 7. COMPLETED
각 단계가 LogPath에 기록됨.
8.4 D1 LogPath 테이블 설계
① Trace Master
CREATE TABLE traces ( trace_id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, event_type TEXT NOT NULL, status TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME);② Trace Events (Path) — Append-only
CREATE TABLE trace_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, trace_id TEXT NOT NULL, stage TEXT NOT NULL, source TEXT NOT NULL, payload_json TEXT, status TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);8.5 이벤트별 LogPath 적용
| 단계 | stage | 비고 |
|---|---|---|
| Stripe Webhook 수신 | STRIPE_WEBHOOK_RECEIVED | trace_id 생성, traces row + trace_events 첫 단계 |
| Billing 상태 변경 | BILLING_STATUS_UPDATED | payload: stripe_customer_id, subscription_id, amount, plan |
| Provision Job 등록 | PROVISION_JOB_CREATED | |
| Provision Worker 실행 | INFRA_RUNNING → INFRA_COMPLETED / INFRA_FAILED | 실패 시 status = Failed |
| Frappe Site 생성 | SITE_CREATED | |
| Zuplo Secret 등록 | GATEWAY_REGISTERED |
8.6 Usage Metering LogPath
- Usage는 Aggregation 단위 로 Trace 생성 (예:
event_type = USAGE_AGGREGATION,stage = AGGREGATION_COMPLETED). - Raw log는 R2 저장, Trace는 D1 저장.
8.7 Lifecycle 이벤트 LogPath
- Suspend:
event_type = TENANT_SUSPENDED - Soft Delete:
event_type = SOFT_DELETE_INITIATED - Hard Purge:
event_type = HARD_PURGE_EXECUTED
8.8 LogPath의 전략적 가치
| 영역 | 개선 효과 |
|---|---|
| SEV1 분석 | 이벤트 경로 1초 내 조회 |
| PDPA 감사 | 삭제 이력 추적 가능 |
| Billing 오류 | 결제→Provision 흐름 추적 |
| Retry 제어 | 실패 단계만 재실행 가능 |
| SLA 측정 | 단계별 소요 시간 측정 가능 |
8.9 Observability 통합
LogPath는 Cloudflare Logs, Stripe Event ID, Pulumi Execution ID, Ansible Job ID, Zuplo Request ID 등과 연결 가능. trace_id 를 모든 시스템에 전달.
8.10 최종 아키텍처 요약 (LogPath 포함)
- Control Plane: Pages, D1 (Tenant State + LogPath)
- Data Plane: R2 (Raw Usage), Aggregator Worker, Provision Worker
- Event Layer: LogPath (Trace + Stage)
- Gateway: Zuplo (Soft Quota)
- Billing: Stripe Webhook + Idempotency
8.11 성숙도 평가 (LogPath 포함)
| 항목 | 평가 |
|---|---|
| Reliability | High |
| Scalability | High |
| Auditability | Enterprise Grade |
| PDPA Compliance | Strong |
| Root Cause Analysis | Excellent |
종합: 9.8 / 10
8.12 다음 단계 제안 (LogPath)
- LogPath 포함 전체 D1 SQL 스키마
- Stripe Webhook + LogPath 샘플 코드
- Provision Worker + LogPath 샘플 코드
- LogPath 기반 Admin Dashboard 설계
9. 지능형 자동화 (Intelligent Automation)
Control Plane이 서버 리소스·서비스 사용량을 모니터링하고, AI 판단으로 어느 서버에 어느 사용자(테넌트)를 배치할지 동적 결정하며, Pulumi Up → Ansible → Zuplo → provision_jobs 갱신까지 전 구간을 자동화하는 설계입니다.
9.1 목표 및 범위
| 목표 | 설명 |
|---|---|
| 전 구간 자동화 | 결제/트리거 후 Pulumi, Ansible, Zuplo, D1 갱신이 사람 개입 없이 단일 파이프라인(워크플로 또는 Provision Worker)에서 순차 실행. |
| 리소스·사용량 모니터링 | Control Plane이 각 Hetzner 서버(노드)의 CPU·메모리·디스크·테넌트 수, 및 서비스 사용량(API 호출, R2 등)을 수집·저장. |
| AI 기반 배치 결정 | 신규 테넌트 프로비저닝 시 기존 서버에 랜딩(landing) 할지, 신규 서버를 생성할지 AI가 판단. 리전·플랜·현재 부하·정책을 입력으로 사용. |
9.2 전 구간 자동화 (Pulumi → Ansible → Zuplo → provision_jobs)
실행 주체: GitHub Actions 단일 워크플로, 또는 Provision Worker(Cron/Queue 기반)가 job을 소비하며 전체 파이프라인 실행.
| 단계 | 작업 | 자동화 요건 |
|---|---|---|
| 1 | provision_jobs에서 Pending job 조회 (또는 workflow_dispatch 입력으로 job_id·tenant_id 수신) | job_id, tenant_id, region, (선택) target_server_id 전달 |
| 2 | Pulumi Up | target_server_id가 없고 AI가 “신규 서버 필요”로 판단한 경우에만 실행. 실행 후 server_ip 출력을 다음 단계로 전달. |
| 3 | Ansible Playbook | server_ip(또는 target_server_id로 조회한 기존 서버 IP)로 Docker+Frappe+new-site+API Key 생성. API Key를 표준 출력/아티팩트로 수집. |
| 4 | Zuplo Sync | 수집한 tenant_id, api_key로 Zuplo Developer API 호출 — Consumer·API Key 등록. |
| 5 | Control Plane 콜백 | provision_jobs.status → Completed, tenants_master.status → Active, tenant_runtime(server_id, site_name, api_key_meta) 갱신. 실패 시 status → Failed, trace_events·알림 기록. |
데이터 전달: 각 단계의 출력(server_ip, api_key 등)을 워크플로 아티팩트·환경 변수·또는 Worker 내부 상태로 다음 단계에 전달. 수동 복사 없음.
9.3 Control Plane 리소스·사용량 모니터링
Control Plane이 어디에 어떤 부하가 있는지 파악해야 AI 배치 결정이 가능합니다.
| 구분 | 수집 항목 | 저장 위치·주기 | 용도 |
|---|---|---|---|
| 서버(노드) 리소스 | server_id, region, CPU 사용률, 메모리 사용률, 디스크 사용률, 테넌트 수, 상한 정책(최대 테넌트 수 등) | D1 server_metrics(또는 시계열 DB)·주기적 스크래핑(예: 1~5분) | AI 배치 시 “여유 있는 서버” 선정 |
| 서비스 사용량 | tenant_id별 API 호출 수, R2 요청/용량, (선택) DB 연결 수 등 | R2 Raw + D1 집계값(§7.4), Zuplo/KV 메터링 | 플랜별 Quota·과금 및 배치 시 “무거운 테넌트” 회피 |
| 가용 서버 목록 | region별 server_id, IP, 관리 FQDN, 상태(active/draining/maintenance) | D1 servers(또는 Pulumi state 연동) | AI가 “이 서버에 랜딩” 선택 시 Ansible 타깃 지정 |
수집 방식 예:
- Hetzner API 또는 노드 내 에이전트(agent)/Cron 스크립트가 메트릭을 Control Plane API로 전송.
- 또는 GitHub Actions/Worker가 주기적으로 Pulumi output·Hetzner API를 조회해 D1에 반영.
9.4 AI 기반 서버 배치 결정
입력: tenant_id, plan_tier, requested_region, (선택) 기타 메타데이터.
출력:
- land_on_existing: target_server_id (기존 서버에 새 테넌트만 추가 → Ansible만 실행).
- create_new_server: region, (선택) 스펙 힌트 → Pulumi Up 후 Ansible 실행.
AI(또는 규칙 엔진) 판단 기준 예시:
| 요소 | 설명 |
|---|---|
| 리전·플랜 | Enterprise + requested_region=eu → 해당 리전 전용 서버 또는 신규 생성. Basic/Professional → 공유 풀에서 여유 서버 우선. |
| 현재 부하 | server_metrics의 CPU/메모리/테넌트 수가 임계치 미만인 서버만 후보. |
| 정책 | “서버당 최대 N 테넌트”, “동일 플랜 우선 배치” 등. |
| 비용·균형 | 새 서버 생성 비용 vs 기존 서버 활용. 과도한 서버 증설 방지. |
구현 옵션:
- 규칙 기반: D1·server_metrics 조회 후 임계치·우선순위 규칙으로 결정.
- 모델 기반: 소규모 ML/LLM 호출로 “최적 서버” 또는 “create_new” 추천(선택).
결정 결과는 workflow_dispatch 입력 또는 provision_jobs.target_server_id, provision_jobs.create_new_server 플래그 등으로 전달.
9.5 데이터 흐름 및 오케스트레이션 요약
[Stripe Webhook / 신규 테넌트 트리거] → Control Plane: D1 tenants_master·provision_jobs 적재 (status=Pending) → Control Plane: 모니터링 데이터(D1/server_metrics) 기반 AI 배치 결정 → 결정 결과: target_server_id 또는 create_new_server=true → workflow_dispatch 또는 Provision Worker 호출 (job_id, tenant_id, region, target_server_id 등) → [자동 파이프라인] - Pulumi Up (필요 시) → server_ip - Ansible(server_ip 또는 target_server_id→ip) → api_key - Zuplo Sync(tenant_id, api_key) - Control Plane 콜백: provision_jobs=Completed, tenants_master=Active → (선택) 모니터링에 새 테넌트·서버 반영9.6 구현 시 고려사항
| 항목 | 내용 |
|---|---|
| 멱등·재시도 | Ansible/Zuplo 단계 실패 시 job은 Failed로 두고, 재시도 시 Pulumi는 스킵(이미 리소스 있음), Ansible부터 재실행 등 단계별 멱등 설계. |
| 보안 | API Key·Hetzner 토큰 등은 GitHub Secrets 또는 Worker Secret으로만 주입. D1에 API Key 원문 저장 금지. |
| 감사 | LogPath(§8)에 AI 배치 결정 결과(stage=PLACEMENT_DECIDED, target_server_id 등) 기록. |
| 폴백 | AI/규칙 엔진 장애 시 기본 정책(예: 해당 리전에서 “테넌트 수 최소” 서버 선택, 없으면 신규 생성) 적용. |