Skip to content

English {#english}

Plan: Step 1 Pulumi (Hetzner) → Step 2 Ansible (Docker · Frappe)

Purpose: Define phases, prerequisites, tasks, and outputs for creating a Hetzner server with Pulumi and then running Docker, Frappe, tenant site, and API Key with Ansible.
No code generation — plan, scope, and paths only.

  • Pulumi test: Lowest-cost/spec (§1.5); delete server after test.
  • Pulumi production: Service spec (§1.6), cost example (§1.7), 1,000 concurrent-user strategy (§1.8).

Base paths

LayerBase pathUse
Pulumi/Users/marco/prego-pulumiHetzner·Cloudflare IaC, stacks (sg/us/eu), run scripts
Ansible/Users/marco/prego-ansibleInventory, roles (Docker, Frappe), playbook, bench/API Key scripts

Step 1: Pulumi (Hetzner server)

Prerequisites: Hetzner API token, SSH key registered in Hetzner, Pulumi stack (sg/us/eu), Cloudflare accountId and Zone if using R2/DNS.
Order: (1) Firewall (SSH 22 only; 80/443 optional if using Tunnel). (2) Server (test: §1.5; prod: §1.6). (3) Management DNS A record. (4) Optional R2 bucket.
Outputs: Server public IP (→ Ansible ansible_host), management FQDN. Run from prego-pulumi; pulumi stack select sg, then preview/up.


Step 2: Ansible (Docker · Frappe)

Prerequisites: Step 1 server IP in inventory, SSH access.
Order: (1) Docker role. (2) Frappe(+ HR) role. (3) Tenant site (bench new-site). (4) Administrator API Key.
Outputs: Server with Docker + Frappe + tenant site; API Key for Zuplo/Control Plane. Run from prego-ansible; ansible-playbook with inventory.
Image-based provisioning (§2.5): After one full Ansible success, create Hetzner image from that server; later nodes use that image and Ansible runs only new-site + API Key (or minimal playbook). Golden image = bench init only, no tenant-specific data.


§3 Ansible scope and out-of-scope

Vectorize, tenant canonical DNS, tenant user CNAME: not in Ansible; handled by Pulumi or existing workflow/scripts (e.g. cloudflare-tenant-dns.ts). See §3.1–§3.2 in Korean for table and rationale.

Full tables (§1.1–§1.8, §2.1–§2.5, §3) and exact wording are in the Korean section below.


한국어 {#korean}

기획서: 1단계 Pulumi(Hetzner 서버) → 2단계 Ansible(Docker·Frappe) 실행 순서

목적: Pulumi로 Hetzner 서버를 생성한 뒤, Ansible로 Docker·Frappe·테넌트 사이트·API Key까지 진행하기 위한 단계·사전 조건·작업·결과를 정의한다.
코드 생성 없음 — 기획·범위·경로만 정리.

  • Pulumi 테스트 단계: 연습용 최저가·최저 사양(§1.5). 테스트 직후 삭제 권장.
  • Pulumi 상용 단계: 실제 서비스용 사양(§1.6)·비용 예시(§1.7)·1,000명 동시 접속 전략(§1.8).

Base 경로

구분Base 경로용도
Pulumi/Users/marco/prego-pulumiHetzner·Cloudflare IaC, 스택(sg/us/eu), 실행 스크립트
Ansible/Users/marco/prego-ansible인벤토리, roles(Docker, Frappe), playbook, bench/API Key 스크립트

1단계: Pulumi로 Hetzner 서버 생성

1.1 사전 조건

항목설명확인 방법
Hetzner API 토큰Hetzner Cloud 콘솔에서 발급. 프로젝트별로 사용.HCLOUD_TOKEN 환경변수 또는 Pulumi config (pulumi config set hcloud:token <secret>)
SSH 키 (Hetzner 등록)서버에 주입할 공개키. Hetzner 콘솔 Security → SSH Keys에 이름으로 등록.예: prego-admin — Pulumi 코드에서 해당 이름으로 조회
Pulumi 스택리전별 스택: sg(Phase 1), us(Phase 4), eu(Phase 5).pulumi stack select sg (또는 us/eu). 스택 설정은 /Users/marco/prego-pulumi/Pulumi.sg.yaml
Cloudflare accountIdR2·DNS 사용 시 필요. Zone이 속한 계정 ID.CLOUDFLARE_ACCOUNT_ID 또는 pulumi config set cloudflare:accountId <id>
Cloudflare Zone관리용 DNS를 넣을 도메인. 예: pregoi.com.Zone이 이미 존재하고, API 토큰으로 해당 Zone 접근 가능해야 함

1.2 작업 순서

순서작업담당(Base)비고
1방화벽 생성Pulumi (/Users/marco/prego-pulumi)SSH(22)만 허용. Cloudflare Tunnel 사용 시 80/443은 서버에 열지 않아도 됨.
2Hetzner 서버 프로비저닝Pulumi테스트 시 §1.5, 상용 시 §1.6 사양 적용. (현재 코드 기본: cx21, ubuntu-24.04, 스택별 location)
3Cloudflare 관리용 DNS A 레코드 생성Pulumi예: node-01.pregoi.com → 서버 공인 IP. 관리·헬스체크용이므로 proxy 비활성(Proxied=false)
4(선택) R2 버킷Pulumi정적 에셋·폰트 등 사용 시. accountId 있을 때만 생성.

1.3 결과물 (2단계 입력으로 전달)

출력설명2단계에서의 사용
서버 공인 IPHetzner 서버의 IPv4 주소Ansible 인벤토리 ansible_host
관리용 FQDN예: node-01.pregoi.comSSH 접속·헬스체크·문서화

Pulumi 출력 예: pulumi export("server_ip", ...), pulumi export("management_dns", ...).
IP를 2단계 Ansible 인벤토리와 SSH 접근 검증에 사용.

1.4 실행 위치·명령 (참고)

  • 작업 디렉터리: /Users/marco/prego-pulumi
  • 스택 선택: pulumi stack select sg (또는 us/eu)
  • 실행: ./run-up.sh preview, ./run-up.sh up (또는 pulumi preview / pulumi up)

1.5 Pulumi 테스트 단계 (연습용 최저가·최저 사양)

Pulumi 프로비저닝 로직만 검증할 때 사용. 테스트 직후 서버 삭제하여 비용 최소화.

항목내용비고
TypeShared Resources시간당 과금 기준 가장 저렴
LocationFalkenstein (FSN1) 또는 Helsinki (HEL1)유럽 지역이 대개 가장 저렴
ImageUbuntu 24.04표준·Pulumi 예제와 호환
ArchitectureArm64x86보다 저렴
Server PlanCPX22 (4 vCPU, 8GB RAM, x86) 또는 CAX11 (2 vCPU, 4GB RAM, Arm64)CAX11 재고 부족 시 CPX22 사용. Hetzner Shared 라인업
NetworkingIPv4 체크, IPv6 사용, 둘 다 사용
Private NetworksVPC 테스트 시 미리 생성한 네트워크 선택Pulumi로 VPC 구성 연습 시
SSH keys로컬 PC 공용키(id_rsa.pub) Hetzner에 등록 완료Pulumi에서 해당 키 이름으로 조회

1.6 Pulumi 상용 단계 (실제 서비스 사양)

실제 서비스용으로 적용할 설정. 구현 시 Pulumi/Ansible에서 아래 값으로 적용.

항목내용비고
Server PlanCPX31 (x86) 이상Frappe MariaDB·Redis 워커 메모리 사용이 크므로 최소 8GB RAM 권장
Volumes20GB ~ 50GB 추가Docker 이미지·테넌트 DB·업로드 파일 분리. OS와 별도 볼륨으로 관리
BackupsEnable (체크)서버 비용의 20% 추가, 매일 자동 스냅샷. 데이터 보호 필수
FirewallsStateful Firewall80(HTTP), 443(HTTPS), 22(SSH) 만 허용, 나머지 차단
Placement groupsSpread서버 2대 이상 시 서로 다른 물리 호스트에 배치해 가동률 극대화

설정·운영 팁

항목내용
Cloud-initPulumi user_dataapt-get update && apt-get install -y docker.io 등 스크립트 삽입 시 서버 생성 직후 Docker 자동 설치. Cloud-init 공식 문서 참고.
Labelsproject: frappe-test, env: dev 등 레이블 부여 시 Hetzner 콘솔에서 필터·비용 추적 용이.
NamePulumi에서 동적 생성 권장. 수동 시 dev-frappe-01 등 규칙 통일.

1.7 상용 비용 예시 (CPX31 기준)

항목상세월 예상 비용 (EUR)비고
Server PlanCPX31 (4 vCPU / 8GB RAM)€13.60기본 서버
Primary IPIPv4 1개€0.60필수 할당 시
VolumesSSD 50GB 추가€2.25GB당 약 €0.045
Backups서버 비용의 20%€2.72자동 스냅샷

1.8 상용: CPX31에서 1,000명 동시 접속 전략

CPX31(8GB RAM) 단일 서버에서 1,000명 동시 접속은 일반 설정으로는 거의 불가능에 가깝다. Frappe 아키텍처를 활용한 리소스 효율화로 설계할 때 참고.

1. 공유 DB 및 커넥션 풀링 (DB Efficiency)

조치내용
Single MariaDB Instance테넌트마다 DB 컨테이너를 두면 8GB로는 부족. 하나의 MariaDB 안에서 bench new-site --db-name으로 DB만 분리.
Max Connectionsmy.cnf에서 max_connections 1,500 이상, innodb_buffer_pool_size를 RAM의 약 40%(약 3GB)로 고정해 스왑 방지.
Alpine 기반 이미지DB·Redis 모두 Alpine 기반 이미지로 컨테이너 메모리 사용 최소화.

2. 스왑 및 커널 파라미터 (OS 안정성)

조치내용
Swap 4GBSSD 볼륨에 /swapfile 생성, swappiness=10으로 RAM 우선 사용. 성능보다 생존 목적.
File Descriptors1,000명 접속 시 오픈 파일 수 제한 가능. Cloud-init에 ulimit -n 65535 포함.

3. Docker Compose 리소스 격리

특정 테넌트 부하가 전체를 다운시키지 않도록 제한.

조치내용
Memory limitsdeploy.resources.limits.memory: 1.5G, reservations.memory: 512M 등으로 컨테이너별 상한·하한 설정.
CPU Sharescpu_shares: 512 등으로 유휴 시에만 CPU 우선순위 배분.

4. 1,000명 동시 접속을 위한 필수 아키텍처

조치내용
전면 캐시 계층Frappe 앞에 Cloudflare 또는 Nginx/Caddy 캐시 배치. 정적 파일(JS/CSS/이미지)이 Frappe Python 워커까지 가지 않도록 차단.
Gunicorn Worker(2 x CPU) + 1 공식을 넘어, 메모리 허용 범위에서 12~16개까지 검토. 응답 지연 시 큐잉되도록 설정.
Redis 통합캐시·큐·Socketio용 Redis를 단일 인스턴스로 두어 메모리 파편화 감소.

5. 추천 추가 옵션: 전용 CPU (Dedicated vCPU)

1,000명 접속 시 Steal time으로 서비스 정지가 우려되면 CCX11 (Dedicated) 라인업 검토. Shared 리소스는 고부하 시 인접 서버 영향 가능.


2단계: Ansible으로 Docker·Frappe 설치

2.1 사전 조건

항목설명확인 방법
1단계 결과: 서버 IPPulumi로 생성된 서버의 공인 IP1단계 pulumi export 또는 Pulumi Cloud 출력
Ansible 인벤토리위 IP를 ansible_host로 갖는 호스트/그룹 정의/Users/marco/prego-ansible/inventory/ (또는 기획에 맞는 경로)
SSH 접근 가능해당 IP로 SSH 접속(예: ssh root@<ip>) 성공1단계에서 주입한 SSH 키로 인증

2.2 작업 순서

순서작업담당(Base)비고
1Docker 설치 roleAnsible (/Users/marco/prego-ansible)role: Docker CE 설치·서비스 기동. Ubuntu 24.04 기준.
2Frappe(+ HR) 환경 roleAnsibleFrappe/ERPNext 또는 Frappe + HR 앱 설치. Docker 기반 또는 bench 기반으로 설계에 따라 결정.
3테넌트 사이트 생성Ansible (task/script)bench new-site <tenant_site> 실행. tenant_site는 테넌트 ID 또는 사이트명 규칙에 따름.
4Administrator API Key 생성Ansible (CLI/스크립트)Frappe CLI 또는 스크립트로 API Key 발급. 자동화 권장 — 수동이면 4·5단계에 키 전달이 어려움.

2.3 결과물 (4·5단계에 전달)

출력설명이후 단계에서의 사용
서버 상태Docker + Frappe + 테넌트 사이트 기동 완료Frappe API 호출 가능한 상태
API Key 문자열해당 사이트·Administrator용 API KeyZuplo 등 API 게이트웨이·Control Plane 등에서 테넌트별 인증

2.4 실행 위치·인벤토리 (참고)

  • 작업 디렉터리: /Users/marco/prego-ansible
  • 인벤토리: 1단계에서 받은 IP로 호스트 정의. 예: inventory/hosts.yml 또는 inventory/sg/hosts.yml (리전별)
  • 실행: ansible-playbook -i inventory/ playbook.yml (playbook·role 경로는 기획에 따라 결정)
  • 단계별 구현 방안: ansible-implementation-plan.md 참조.

2.5 이미지 기반 프로비저닝 (상용·설치 시간 단축)

테스트가 끝나고 상용 서버에 한 번 전체 설치(Ansible playbook)가 성공한 뒤, 그 서버에서 **이미지(스냅샷)**를 만들어 두면, 이후 새 노드를 띄울 때 설치 시간을 크게 줄일 수 있다.

구분풀 설치(매번 Ansible 전체)이미지 기반
소요 시간Docker + MariaDB 컨테이너 + bench init(수 분~수십 분) + new-site + API KeyPulumi로 이미지에서 서버 생성 → Ansible은 테넌트만 추가(new-site, API Key) 또는 최소 설정만 실행
적합첫 노드, 테스트, 이미지 없을 때두 번째 노드 이후, 스케일 아웃, 동일 스택/리전

권장 흐름

  1. 1회: 상용 사양으로 Pulumi Up → Ansible playbook 전체 실행(Docker, MariaDB, Frappe bench init, 필요 시 첫 테넌트까지). 설치가 정상 완료된 App 서버를 Golden 상태로 둠.
  2. 이미지 생성: Hetzner 콘솔에서 해당 App 서버를 선택 → Create image (또는 Snapshot). 이미지 이름·라벨 규칙 통일(예: prego-app-YYYYMMDD, prego-frappe-bench-v15). DB 서버는 별도이므로 DB 서버용 이미지를 따로 만들 수도 있음(선택).
  3. 이후 노드: Pulumi에서 새 서버 생성 시 image를 Ubuntu 기본 대신 위 커스텀 이미지 ID/이름으로 지정. 생성된 서버에는 이미 Docker·Frappe bench가 있으므로, Ansible은 frappe_site 역할만 실행(또는 new-site·API Key만 수행하는 경량 playbook)하면 됨.
  4. 이미지 갱신: Frappe/bench 버전 업그레이드·보안 패치 후 같은 방식으로 새 이미지를 만들고, 이후 Pulumi에서 새 노드는 새 이미지를 쓰도록 변경.

주의

  • 이미지에 **테넌트별 정보(사이트명·DB)**가 들어가면 안 되므로, Golden 이미지는 bench init까지만 넣고, 첫 사이트는 넣지 않거나 테스트용 한 개만 넣고 제거한 뒤 이미지화하는 편이 안전함.
  • DB 서버는 App과 분리되어 있으므로, App 이미지에는 DB 연결 정보(host/port)만 설정 가능한 상태로 두고, 실제 db_host는 Ansible/인벤토리로 주입.

이렇게 하면 설치 시간이 줄어들고, 상용에서 노드를 추가할 때는 서버 생성 + 짧은 Ansible(테넌트 추가)만 하면 된다.


3. Ansible 범위 및 비범위 — Cloudflare Vectorize·테넌트 DNS

다음 항목들은 현재 Pulumi에는 구현되어 있지 않고, 기획서 또는 Prego 워크플로/스크립트에서만 다룬다. Ansible에서 구현하는 것이 적합한지 분석한 결과를 정리한다. 코드 생성 없음 — 담당 계층·권장만 명시.

3.1 Ansible의 역할 정리

Ansible이 담당하는 것설명
서버 위 상태대상 호스트(VM) 위에서만 의미가 있는 구성: Docker CE 설치, Frappe/bench 설치·실행, bench new-site, API Key 생성.
실행 맥락SSH로 서버에 접속해 패키지·서비스·디렉터리·CLI 명령을 실행. 인벤토리는 ansible_host(server_ip) 기준.

즉, Cloudflare 계정/전역 리소스도메인·DNS 레코드는 서버 안에 존재하지 않으므로, Ansible의 “대상 호스트”가 아니다.

3.2 Cloudflare Vectorize — Ansible 부적합

항목내용
현재 상태기획서(rag-ai-edge-architecture-plan.md §4)에만 “Pulumi로 Vectorize·D1 프로비저닝” 설계가 있음. prego-pulumi에는 Vectorize 리소스 미구현.
Vectorize 성격Cloudflare 엣지의 관리형 벡터 DB. 서버(VM)에 설치하는 소프트웨어가 아니며, Cloudflare API로 인덱스 생성·설정함.
Ansible 적합성❌ Ansible에서 구현 비권장. Ansible은 “호스트 위 상태”를 다루므로, Cloudflare 계정 단위 리소스(Vectorize 인덱스) 생성·관리는 Ansible 역할이 아님.
담당 권장Pulumi(prego-pulumi) 에서 pulumi_cloudflare로 Vectorize 인덱스 리소스 추가하거나, Cloudflare API를 호출하는 별도 스크립트/워크플로 단계. Worker(rag-query, rag-ingestion)는 이미 wrangler 바인딩으로 인덱스 이름만 참조하므로, 인덱스가 “어디서 생성되느냐”만 IaC 또는 프로비저닝 스크립트로 통일하면 됨.

3.3 테넌트 canonical DNS (tenant-xxx.pregoi.com) — Ansible 비권장

항목내용
현재 상태Prego 레포 infra/cloudflare-tenant-dns.ts + 워크플로 tenant-dns 단계에서 처리. Pulumi는 관리용 node-NN.pregoi.com A 레코드만 담당.
canonical DNS 성격테넌트별 canonical FQDN(예: tenant-a1b2c3d4.pregoi.com)에 대한 A 또는 CNAME 레코드는 Cloudflare Zone에 생성되는 DNS 레코드. 서버 내부 상태가 아님.
Ansible 적합성❌ Ansible에서 구현 비권장. 기술적으로는 Ansible 제어기에서 delegate_to: localhost + uri 모듈로 Cloudflare API를 호출해 레코드를 만들 수는 있으나, (1) Ansible에 Cloudflare API 토큰·Zone ID를 넘겨야 하고, (2) “서버 구성”과 “DNS 레코드 생성”이 한 playbook에 섞여 책임이 흐려짐.
담당 권장현행 유지: 워크플로 단계에서 cloudflare-tenant-dns.ts 호출. 또는 테넌트 DNS를 인프라 코드로 통일하고 싶다면 Pulumi에서 테넌트별 리소스(또는 동적 블록)로 canonical 레코드 생성 검토. Ansible은 server_ip·site_name 등 “결과값”만 변수로 받아 서버 설정에만 사용.

3.4 테넌트 사용자 CNAME (acme.pregoi.com → canonical) — Ansible 비권장

항목내용
현재 상태동일하게 cloudflare-tenant-dns.tscreate --subdomain acme 등으로 CNAME 생성. tenant-subdomain-dns-design.md 방식 C.
성격사용자 서브도메인을 canonical로 연결하는 Cloudflare Zone 내 CNAME 레코드. 서버와 무관.
Ansible 적합성❌ Ansible에서 구현 비권장. 이유는 §3.3과 동일. DNS는 오케스트레이션/워크플로 또는 IaC(Pulumi)가 담당하는 것이 계층 분리와 보안(시크릿 관리)에 유리함.
담당 권장현행 유지: 워크플로에서 cloudflare-tenant-dns.ts로 canonical + 선택적 subdomain CNAME 생성. Ansible은 DNS 생성에 관여하지 않음.

3.5 요약 표

기능Ansible 구현권장 담당
Cloudflare Vectorize 인덱스❌ 부적합Pulumi 또는 전용 프로비저닝 스크립트/워크플로
테넌트 canonical DNS (tenant-xxx.pregoi.com)❌ 비권장현행: cloudflare-tenant-dns.ts + 워크플로. 대안: Pulumi
테넌트 사용자 CNAME (acme.pregoi.com → canonical)❌ 비권장현행: cloudflare-tenant-dns.ts + 워크플로

단계 간 데이터 전달 요약

[1단계 Pulumi]
입력: Hetzner 토큰, SSH 키 이름, Pulumi 스택(sg/us/eu), Cloudflare accountId
출력: server_ip, management_dns (선택: R2 버킷)
[2단계 Ansible]
입력: server_ip → 인벤토리 ansible_host, SSH 접근 가능
출력: Docker + Frappe + 테넌트 사이트, API Key 문자열
[4·5단계]
입력: API Key 문자열 (Zuplo 등록, Control Plane·클라이언트 연동 등)

구현 시 참고 (경로만 명시, 코드 없음)

항목경로 또는 위치
상용 미구현 항목 체크리스트pulumi-hetzner-production-todo.md — §1.6 Volumes, Backups, CPX31, Placement group, Cloud-init 구현 순서
Pulumi 프로젝트 루트/Users/marco/prego-pulumi
Pulumi 진입점·리소스 정의/Users/marco/prego-pulumi/__main__.py
Pulumi 스택 설정/Users/marco/prego-pulumi/Pulumi.sg.yaml, Pulumi.us.yaml, Pulumi.eu.yaml
Pulumi 리전 설정/Users/marco/prego-pulumi/config/region.py
Ansible 프로젝트 루트/Users/marco/prego-ansible
Ansible 인벤토리/Users/marco/prego-ansible/ 하위 (예: inventory/, inventory/sg/hosts.yml 등 — 구조는 구현 시 결정)
Ansible roles/Users/marco/prego-ansible/roles/ (예: docker, frappe 등 — 구현 시 생성)
Ansible playbook/Users/marco/prego-ansible/ 루트 또는 playbooks/ (구현 시 결정)

문서 위치: docs/planning/pulumi-ansible-step1-step2-plan.md

Help