Hetzner·Pulumi 기반 서버 리소스 관리: Trial 종료·유료 전환·독립(전용) 서버 기획서
목적: (1) 모든 서버는 Hetzner를 사용하며, prego-pulumi 레포에 연결된 Pulumi 솔루션이 기존 기획서·워크플로에 어떻게 반영되어 있는지 확인하고, (2) 무료 Trial 종료 후 유료 전환 또는 서비스 종료, (3) 처음부터 유료 패키지 선택 시 독립(전용) 서버 구성(Frappe·MariaDB·Redis 각 1대 = 테넌트당 3대)을 현재 설계에서 어떻게 다루는지 정리한 뒤, 서비스에 적합한 서버 리소스 관리 모델을 추천한다. 코드 생성 없음 — 기획·분석·추천만.
참조: prego-pulumi (README, main.py, config/region.py), tenant-onboard-resource-allocation-flow-plan.md, placement-test-data-and-simulation-dashboard.md, provision-tenant.yml, api-control-plane-tenant-lifecycle-analysis.md.
1. 현재 Pulumi(Hetzner) 구현 요약 (prego-pulumi)
| 항목 | 내용 |
|---|
| 위치 | /Users/marco/prego-pulumi (별도 레포). Prego 워크플로에서 clone 후 pulumi stack select + pulumi up 호출. |
| 프로바이더 | Hetzner Cloud (pulumi_hcloud), Cloudflare (DNS, R2, D1, KV). |
| 스택 | 리전별 1스택: sg, us, eu (또는 dev = sg). Pulumi Cloud 조직/프로젝트: opsfork-org/prego-pulumi. |
| 리소스(스택당) | App 서버 1대 + DB 서버 1대 + Redis 서버 1대 (고정). 방화벽, Placement Group(spread), 선택적 Volume. |
| 출력 | server_ip(app), db_server_ip, redis_server_ip, management_dns(node-NN.pregoi.com). |
| 역할 | 멀티테넌트 공유: 동일 스택의 App 1대에 여러 테넌트(Frappe 사이트)가 적층됨. DB·Redis는 해당 리전 테넌트들이 공유. |
정리: 현재 Pulumi는 “리전당 1세트(App 1 + DB 1 + Redis 1)” 만 생성하며, 테넌트 전용 독립 3대(1 tenant = App 1 + DB 1 + Redis 1) 구성은 미지원이다.
2. 기획서·워크플로에 Pulumi가 반영된 여부
2.1 반영되어 있는 부분
| 문서/구성요소 | Pulumi·Hetzner 반영 내용 |
|---|
| tenant-onboard-resource-allocation-flow-plan.md | create_new_server 시 Pulumi로 새 서버 생성, R3 Pulumi 성공 후 노드 등록, 시나리오 A/B(메모리 포화·노드 증설)에서 Pulumi up 경로 언급. Hetzner는 문서에 “Hetzner 서버”로 명시. |
| provision-tenant.yml | create_new_server=true 시 prego-pulumi clone → pulumi stack select $stack (region=sg/us/eu) → pulumi up -y → server_ip, db_server_ip 출력 사용. Pulumi 성공 후 POST /internal/nodes 로 노드 등록. |
| placement.ts (R4) | Enterprise → dedicated 노드만, 그 외 → shared. 노드는 Control Plane D1 nodes 에서 관리; “노드”의 물리 생성은 워크플로에서 Pulumi로 수행. |
| placement-test-data-and-simulation-dashboard.md | Placement 결과(create_new_server, target_server_id)와 “서버 생성·Frappe·DB·Redis·적층” 검증을 다룸. 실제 서버 생성은 워크플로 → Pulumi 연동으로 이루어지므로, 해당 기획서는 Pulumi를 전제로 한 플로우와 호환되나, 문서 내에 “prego-pulumi”·“Hetzner” 문자열은 명시되어 있지 않음. |
| intelligent-automation-implementation-plan.md, IMPLEMENTATION_INDEX | Pulumi → Ansible → Zuplo → D1 전 구간 자동화, prego-pulumi 경로·스택(sg/us/eu) 참조. |
| saas-db-separation-and-scaling-plan.md, redis-separation-pulumi-ansible-plan.md | Pulumi로 DB·Redis 서버 프로비저닝, Phase별 로드맵. |
결론: 대부분의 기획서와 워크플로는 Pulumi(Hetzner)를 전제로 하며, create_new_server 시 prego-pulumi를 사용하는 구조가 반영되어 있다.
2.2 현재 제약(갭)
| 항목 | 내용 |
|---|
| 스택당 서버 수 | Pulumi main.py는 스택당 App 1 + DB 1 + Redis 1 고정. “앱 서버만 1대 더” 추가하려면 새 스택(예: sg-node02) 또는 Pulumi 코드 변경(앱 서버 N대) 필요. |
| create_new_server 의미 | 워크플로는 region별 기존 스택(sg/us/eu)에 대해 pulumi up 실행. 해당 스택이 이미 있으면 추가 서버가 생기지 않음(이미 리소스 존재). 즉 “첫 프로비저닝 시 3대 생성”에는 부합하나, “리전 내 2번째 앱 노드” 는 현재 구조로는 불가. |
| 독립(전용) 3대 | “1 테넌트 = Frappe 1대 + MariaDB 1대 + Redis 1대” 전용 구성은 현재 Pulumi·기획서 어디에도 정의되어 있지 않음. |
3. 시나리오 1: 무료 Trial 종료 후 유료 전환 또는 서비스 종료
3.1 요구 사항 정리
- 무료 Trial: 약 3개월 사용 후
- 유료 전환: 결제 시작 → 기존 테넌트 유지(동일 shared 노드 또는 업그레이드).
- 서비스 종료: Trial 만료·미전환 시 접근 차단 또는 리소스 정리.
3.2 현재 설계 반영 여부
| 항목 | 현재 반영 | 갭·보완 |
|---|
| Trial 기간 | tenants_master·billing에 trial_ends_at(또는 유사) 필드가 기획서·마이그레이션에 명시적으로 없음. Stripe의 trial_period_days·subscription trial_end 는 Stripe 측에 있음. | 정의 필요: D1에 trial_ends_at 또는 Stripe subscription과 매핑해 “Trial 만료일”을 Control Plane에서 조회 가능하게 할지 결정. |
| Trial → 유료 전환 | Stripe checkout.session.completed·customer.subscription.updated 로 유료 구독 시작 시 워크플로·Webhook 처리 가능. plan_tier 상향·갱신은 billing·tenants_master 반영으로 확장 가능. | 정의 필요: “Trial 만료 N일 전 알림”, “만료 시 자동 전환(결제 수단 있음) vs 미전환 시 제한” 정책을 기획서·Runbook에 명시. |
| Trial 종료·미전환(서비스 종료) | api-control-plane-tenant-lifecycle-analysis: Suspended 상태, Grace Period(만료일+7일) 후 접근 차단. purge-job은 Pending_Deletion 30일 후 Hard Purge. | 갭: Trial 만료를 “결제 실패”와 동일한 Suspended·Grace 흐름으로 둘지, Trial_Expired 전용 상태로 둘지 정책 정의. “서비스 종료” 시 리소스(동일 shared 노드 상의 해당 테넌트만 제거) 정리 순서는 purge·Runbook에 맞춰 정의 가능. |
정리: Trial 기간·만료일·전환/종료 정책은 부분적으로만 기획서에 반영되어 있다. trial_ends_at(또는 Stripe 연동), Trial 만료 시 전환 vs 종료 플로우, Suspended/Trial_Expired 상태를 명시하는 기획 보강이 필요하다.
4. 시나리오 2: 처음부터 유료 패키지 선택 — 독립(전용) 서버
4.1 요구 사항 정리
- 독립(전용) 구성: 한 테넌트당 다른 테넌트와 서버를 공유하지 않음.
- Frappe 1대 + MariaDB 1대 + Redis 1대 = 테넌트당 3대 필요.
4.2 현재 설계 반영 여부
| 항목 | 현재 반영 | 갭 |
|---|
| Placement R4 | Enterprise → dedicated 노드만 선택. nodes.role = ‘dedicated’. | 물리 의미: “dedicated”는 현재 “다른 테넌트와 앱을 공유하지 않는 노드”를 의미하나, Pulumi는 노드 1대(앱만) 개념이지, 1 tenant = App 1 + DB 1 + Redis 1 세트를 만들지 않음. |
| Pulumi | 스택당 App 1 + DB 1 + Redis 1 (공유용 1세트). 테넌트 전용 3대 세트 생성 로직 없음. | 갭: 독립 구성은 테넌트별 별도 스택 또는 전용 스택 템플릿(tenant_id당 App 1 + DB 1 + Redis 1)이 필요. |
| 워크플로 | create_new_server 시 리전 스택 1개만 사용. tenant_id·plan_tier에 따라 “전용 스택”을 선택하는 분기 없음. | 갭: plan_tier=enterprise(또는 “dedicated” 플래그)일 때 별도 Pulumi 스택(예: dedicated-{tenant_id} 또는 dedicated-{region}-{seq}) 실행 경로가 없음. |
| Ansible | 인벤토리: server_ip, db_server_ip, redis_server_ip. 1 tenant 1 app 인벤토리 구성은 가능하나, “이 테넌트만을 위한 DB·Redis 1대씩”은 해당 서버들만 인벤토리에 넣으면 됨. | 정합: Pulumi가 테넌트 전용 3대를 만들면, Ansible은 기존처럼 1 app + 1 db + 1 redis 대상으로 playbook 실행하면 됨. |
결론: 독립(전용) 서버 구성(테넌트당 3대) 은 현재 Pulumi·워크플로·기획서에 반영되어 있지 않다. R4 “dedicated”는 “dedicated 노드(앱 1대)” 수준이며, “1 tenant = 3 servers” 리소스 모델은 신규 정의가 필요하다.
5. 서버 리소스 관리 모델 추천
5.1 리소스 모델 구분
| 모델 | 설명 | 테넌트당 서버 수 | 현재 구현 |
|---|
| 공유(Shared) | 여러 테넌트가 동일 App 노드(및 리전 공용 DB·Redis)에 적층. | 0 (리전당 1세트 공유) | ✅ Pulumi 스택당 1세트, Placement에서 shared 노드 배치. |
| 독립(전용, Dedicated) | 1 테넌트가 Frappe 1대 + MariaDB 1대 + Redis 1대를 단독 사용. | 3 | ❌ 미구현. |
5.2 추천: 이원화 리소스 모델
공유 풀(Shared Pool) 과 독립 풀(Dedicated Pool) 을 나누어 관리하는 것을 추천한다.
| 구분 | 공유 풀 (Shared) | 독립 풀 (Dedicated) |
|---|
| 대상 | Free, Basic, Professional, Trial(무료) | Enterprise 또는 “독립 서버” 옵션 선택 고객 |
| 리소스 | 리전당 N개 앱 노드 + 1(또는 소수) DB·Redis 공유. | 테넌트당 1스택: App 1 + DB 1 + Redis 1 (Hetzner 3대). |
| Placement | 기존 decidePlacement: region, role=shared, server_metrics 기준. | “dedicated” 요청 시 노드 선택이 아니라 “전용 스택 프로비저닝” 플로우로 분기. |
| Pulumi | 기존과 동일 또는 리전당 앱 노드 다중화(스택 이름 확장 예: sg-node01, sg-node02). | 테넌트 전용 스택: 예) dedicated-{tenant_id_safe} 또는 dedicated-{region}-{seq}. 해당 스택에서 App 1 + DB 1 + Redis 1 생성. |
| 워크플로 | create_new_server → 기존 region 스택(sg/us/eu) 또는 노드 추가 스택. target_server_id 있으면 기존 노드 사용. | infra_mode=dedicated(또는 plan_tier=enterprise + dedicated_tenant=true) → 전용 스택 Pulumi up → 동일 스택에서 server_ip, db_server_ip, redis_server_ip 사용 → Ansible은 해당 3대만 대상. |
| 노드 등록 | 앱 노드만 nodes에 등록(host=app IP). DB·Redis는 region별 기본값 또는 R7. | 선택: 전용 세트를 “노드 1개”(app만)로 등록하거나, tenant_dedicated_resources 테이블에 tenant_id, app_host, db_host, redis_host 저장. |
5.3 Trial 종료 반영 추천
| 항목 | 추천 |
|---|
| 데이터 | tenants_master 또는 billing에 trial_ends_at (또는 Stripe subscription trial_end와 동기화). |
| 정책 | Trial 만료 X일 전 알림(이메일·앱 내). 만료 시: (A) 결제 수단 있음 → 유료 전환 플로우, (B) 없음 → Suspended 또는 Trial_Expired 전환, Grace(예: 7일) 후 접근 차단. |
| 리소스 | Trial·Free는 공유 풀만 사용. 전환 시 유료 플랜이 shared면 동일 노드 유지; dedicated 선택 시 독립 풀 프로비저닝(마이그레이션 또는 신규 3대) 기획 필요. |
5.4 Pulumi 측 추천 (개념)
- 공유 풀:
- 현행 유지: 리전당 1스택(1 App + 1 DB + 1 Redis)으로 첫 create_new_server 처리.
- 확장 시: 리전 내 앱 노드 추가가 필요하면 스택 이름 규칙 확장(예: sg → sg-node01, sg-node02)하여 스택당 App 1대만 두거나, Pulumi 코드에서 app_server_count 등 변수로 앱 서버 다대 생성.
- 독립 풀:
- 테넌트 전용 스택 1개 = App 1 + DB 1 + Redis 1.
- 스택 이름:
dedicated-{tenant_id} (Hetzner 리소스명 충돌 방지용 sanitize) 또는 ded-{region}-{short_id}.
- 워크플로에서 infra_mode=dedicated(또는 전용 플래그)일 때만 이 경로 실행; 기존 shared 경로와 분리.
6. 기획서 반영 체크리스트 (요약)
| # | 항목 | 현재 | 권장 |
|---|
| 1 | 기획서에 Pulumi·Hetzner 명시 | 대부분 반영됨. placement-test-data 문서에는 “prego-pulumi”·“Hetzner” 문구 없음. | placement-test-data-and-simulation-dashboard.md 참조에 “prego-pulumi(Hetzner)” 추가. |
| 2 | Trial 기간·만료 | 미정의. | trial_ends_at(또는 Stripe 연동), Trial 만료 시 전환/종료 정책 기획서에 추가. |
| 3 | Trial → 유료 / 미전환 종료 | lifecycle에 Suspended·Grace 있음. Trial 전용 상태·플로우는 미명시. | Trial_Expired 또는 Suspended 활용, Grace 후 제한 순서 Runbook·§13에 명시. |
| 4 | 독립(전용) 3대 구성 | 미반영. | “독립 풀” 리소스 모델·테넌트당 3대, Pulumi 전용 스택·워크플로 분기 기획서에 신규 절 추가. |
| 5 | create_new_server 확장 | 현재는 리전 1스택만. | 공유 풀 앱 노드 다대화 시 스택 명명 규칙 또는 Pulumi 변수 확장; 독립 풀은 전용 스택 경로 분리. |
7. 정리
- Pulumi(Hetzner) 반영: 대부분의 기획서와 provision-tenant 워크플로는 prego-pulumi를 사용하며, create_new_server 시 리전별 1스택(App 1 + DB 1 + Redis 1) 생성·노드 등록까지 반영되어 있다. Placement 테스트 기획서는 해당 플로우와 호환되나, 문서에 “prego-pulumi”·“Hetzner”를 한 줄 명시하면 좋다.
- Trial 종료: 무료 Trial(~3개월) 종료 후 유료 전환 또는 서비스 종료는 부분적으로만 반영됨. trial_ends_at, Trial 만료 시 전환/종료 정책, Suspended·Grace와의 관계를 기획서·Runbook에 명시하는 것이 좋다.
- 독립(전용) 서버: “1 테넌트 = Frappe 1대 + MariaDB 1대 + Redis 1대” 구성은 현재 설계에 없음. 서버 리소스 관리 모델로 공유 풀(기존) 과 독립 풀(테넌트당 3대, Pulumi 전용 스택) 을 이원화하고, Trial·유료·전용을 이 모델에 맞춰 정책과 워크플로를 확장하는 것을 추천한다.