English {#english}
End-to-End Provisioning Workflow Design (5 Steps)
Purpose: Plan the design and implementation of the execution vehicle (GitHub Actions workflow or Provision Worker) that runs Pulumi (if needed) → Ansible → Zuplo Sync → Control Plane callback in one automated sequence.
No code generation — design requirements, I/O, option comparison, and callback spec only.
References: intelligent-automation-implementation-plan.md §3.3, saas-unified-architecture-hetzner-cloudflare-zuplo-plan.md §9.2, tenant-provisioning-flow.md §A.5, tenant-subdomain-dns-design.md (tenant DNS approach C), provision-tenant-pipeline.md. Unified view: api-control-plane-implementation-plan.md §5, §19, §21. Ansible vars (plan_tier, plan_limits, resource isolation): resource-optimization-safe-adoption-plan.md §2.4, §4.4.
1. Purpose and scope
- Goal: One provisioning request (job_id, tenant_id, region, placement result) → infra, config, gateway, state update without human steps.
- Scope: ① Receive job input (or poll Pending job) ② Pulumi Up (conditional) ③ Ansible ④ Zuplo Sync ⑤ Control Plane callback. AI placement, monitoring, Webhook trigger are outside this design.
- Out of scope: Pulumi/Ansible/Zuplo internal logic, D1 schema changes, Stripe Webhook or placement engine implementation.
2. Execution vehicle comparison
- GitHub Actions workflow:
.github/workflows/provision-tenant.yml. Trigger: workflow_dispatch (job_id, tenant_id, region, target_server_id, create_new_server). Can run Pulumi and Ansible on same runner; callback via Control Plane API. - Provision Worker: HTTP POST /provision/run or Cron polling D1. Cannot run Pulumi/Ansible inside Worker; orchestrates by calling external workflow or service. Can update D1 directly for callback.
- Guidance: Use GitHub Actions when Pulumi and Ansible run in one pipeline. Use Provision Worker (hybrid) when callback is simplified to direct D1 update and Pulumi/Ansible are invoked externally. This plan defines steps workflow-centric; Worker option summarized in §6.
3. I/O spec (summary)
Pipeline inputs: job_id (required), tenant_id (required), region (sg|us|eu), target_server_id (optional), create_new_server (optional). Delivered via workflow_dispatch inputs or Worker body/Cron from D1.
Intermediate outputs: Pulumi → server_ip; node lookup → server_ip; Ansible → api_key (artifact); Zuplo → none; Callback → none.
Callback payload (success): job_id, tenant_id, status=Completed, server_id/node_id, host, site_name (= canonical_hostname), api_key_ref (optional). On failure: status=Failed, error_message or stage. Control Plane sets provision_jobs.status, trace_events.
4. Step design (summary)
- Step 1: Get job input (workflow inputs or D1 Pending row).
- Step 2: Resolve server_ip — if target_server_id: read host from D1 nodes; if create_new_server: Pulumi Up, then register new node (POST /internal/nodes or equivalent).
- Step 3: Ansible — dynamic inventory (server_ip, tenant_id), playbook; output api_key in artifact. Playbook: DB create → user → site dir → site_config → bench init·install-app. Inject tenant_id, site_name (canonical_hostname), db_root_password, plan_limits.
- Step 4: Zuplo Sync — tenant_id, api_key (file or string). zuplo_sync.ts or CLI. On failure, report Failed; support step-wise retry from Zuplo.
- Step 5: Callback — POST /internal/provision-complete (success) or provision-failed (failure). Control Plane updates provision_jobs, tenants_master, tenant_runtime. Or Worker updates D1 directly.
5–10. Workflow detail, Worker summary, Security, Prereqs, Checklist, Refs
- §5 GitHub Actions: name provision-tenant, trigger workflow_dispatch, jobs (resolve-server / pulumi-up → Ansible → Zuplo Sync → callback), Secrets, failure handling.
- §6 Provision Worker: HTTP or Cron; orchestrator only; calls Pulumi workflow and Ansible runner; Zuplo via fetch; D1 direct update.
- §7 Security: secrets only, no API key in logs/artifacts. Idempotency: same job_id re-run skips or handles existing resources. Retry: re-dispatch same job_id to resume from failed step. Scalability: DB placement, Docker resource limits, Logpush.
- §8 Prerequisites: D1 migration 0007, Ansible artifact and zuplo_sync, Control Plane callback or Worker D1 write, Pulumi stacks and tokens.
- §9 Verification: Pulumi→Ansible→Zuplo→callback success; target_server_id path; failure→Failed callback; idempotent re-run; no API key in logs.
- §10 Reference docs table.
Full tables (§1 scope, §2 option comparison, §3.1–§3.3, §4.1–§4.5, §5 job layout, §7.1, §10) and exact wording are in the Korean section below.
한국어 {#korean}
5단계 전 구간 자동화 워크플로 설계 기획서
목적: Pulumi(필요 시) → Ansible → Zuplo Sync → Control Plane 콜백까지 한 번에 자동 실행하는 실행 주체(GitHub Actions 워크플로 또는 Provision Worker)의 설계·구현을 위한 기획.
코드 생성 없음 — 설계 요건·입출력·옵션 비교·콜백 스펙만 정의.
참조: intelligent-automation-implementation-plan.md §3.3, saas-unified-architecture-hetzner-cloudflare-zuplo-plan.md §9.2, tenant-provisioning-flow.md §A.5, tenant-subdomain-dns-design.md (테넌트 DNS 방식 C), provision-tenant-pipeline.md. 통합 관점(공개 API·KV·Observability·보안·Runbook·Phase)은 api-control-plane-implementation-plan.md §5·§19·§21 참조. Ansible 입력 변수(plan_tier·plan_limits·자원 격리): resource-optimization-safe-adoption-plan.md §2.4·§4.4.
1. 목적 및 범위
| 항목 | 내용 |
|---|---|
| 목표 | Control Plane이 트리거한 한 건의 프로비저닝 요청(job_id, tenant_id, region, 배치 결과)을 받아, 인프라·설정·게이트웨이·상태 갱신까지 사람 개입 없이 순차 실행. |
| 범위 | ① job 입력 수신(또는 Pending job 조회) ② Pulumi Up(조건부) ③ Ansible 실행 ④ Zuplo Sync ⑤ Control Plane 콜백. AI 배치 결정·모니터링 수집·Webhook 트리거는 본 설계 밖(선행 단계). |
| 비범위 | Pulumi/Ansible/Zuplo 내부 로직 변경, D1 스키마 변경, Stripe Webhook·배치 엔진 구현. |
2. 실행 주체 옵션 비교
| 구분 | GitHub Actions 워크플로 | Provision Worker |
|---|---|---|
| 파일/위치 | .github/workflows/provision-tenant.yml | workers/provision-tenant/ (또는 기존 control-plane 확장) |
| 트리거 | workflow_dispatch(Control Plane이 GitHub API로 호출), 입력으로 job_id·tenant_id·region·target_server_id·create_new_server 전달 | ① HTTP: Control Plane이 POST /provision/run 등 호출 시 body로 동일 입력 전달 ② Cron: D1 provision_jobs에서 status=Pending인 행 폴링 후 처리 |
| Pulumi 실행 | 별도 repo(prego-pulumi) checkout 또는 pulumi up 스크립트 호출. 리전별 Secret(HCLOUD_TOKEN_SG 등) 사용. | Worker에서 Pulumi CLI 실행 불가 일반적. 외부 호출: GitHub Actions workflow_dispatch(pulumi-up만) 또는 별도 Pulumi Automation API/서비스 호출. |
| Ansible 실행 | Self-hosted runner 또는 호스트에서 ansible-playbook 실행. 인벤토리·artifact 경로를 워크플로에서 구성. | Worker에서 Ansible 직접 실행 어려움. 외부 호출: GitHub Actions(Ansible만 담당 워크플로) 재사용 또는 별도 실행 환경(예: Self-hosted runner) HTTP로 트리거. |
| Zuplo Sync | npx tsx infra/zuplo_sync.ts 실행. Env에서 ZUPLO_* 주입. | Worker에서 syncTenantApiKey() 호출 또는 fetch로 Zuplo API 직접 호출. |
| 콜백 | 워크플로 step에서 Control Plane API POST /internal/provision-complete(가정) 호출. | Worker가 D1 바인딩으로 직접 provision_jobs·tenants_master·tenant_runtime 갱신 가능. |
| 장점 | Pulumi·Ansible을 같은 runner에서 순차 실행하기 용이. 기존 Pulumi 워크플로와 패턴 통일. | D1 직접 접근으로 콜백 단순. Cron 폴링 시 Webhook 실패 시에도 job 소비 가능. |
| 단점 | Pulumi/Ansible repo·runner·Secret 관리 필요. 콜백 시 Control Plane API 구현 필요. | Pulumi·Ansible을 Worker만으로 실행하기 어려워 외부 호출 또는 하이브리드 구조 필요. |
선택 가이드
- Pulumi·Ansible을 한 파이프라인에서 순차 실행하려면 GitHub Actions 워크플로가 적합.
- 콜백을 D1 직접 갱신으로 단순화하고, Pulumi/Ansible은 기존 워크플로 또는 별도 서비스를 호출만 하려면 Provision Worker(하이브리드) 설계 가능.
- 본 기획서는 워크플로 중심으로 단계를 정의하고, Worker 옵션은 §6에서 별도 요약.
2.1 이벤트 기반 API 흐름 (Zero-Touch 전략)
가입·결제 완료 후 수동 개입 없이 한 트랜잭션처럼 프로비저닝을 끝내기 위한 흐름 요약. (구현 시 GitHub Actions·Provision Worker·Task Queue 중 채택한 실행 주체에 맞춰 적용.)
| 단계 | 역할 | 내용 |
|---|---|---|
| Request Receiver | 가입 요청 수신 | Tenant 테이블에 상태 **‘Provisioning’**으로 저장 후, 무거운 인프라 작업을 비동기로 넘김. 웹 요청 블로킹 방지. |
| Task Queue | 비동기 실행 | 무거운 작업을 큐에 넣고 즉시 응답. (예: Celery/Redis, 또는 GitHub Actions workflow_dispatch, Provision Worker Cron 폴링.) |
| Worker Step A | 인프라·앱 구축 | Ansible 실행: 테넌트 전용 DB·DB 사용자·사이트 디렉터리·site_config·bench install-app. 워크플로/트리거에서 plan_tier·plan_limits(CPU·메모리 Hard/Soft) 전달 시 컨테이너 자원 격리 적용. §4.3 Playbook 태스크 구성 참조. |
| Worker Step B | 에지 라우팅 | 프로비저닝 완료 후 Cloudflare API로 **TENANT_MAP(KV)**에 호스트명→오리진 매핑 등록. cloudflare-orchestration-and-gtape-backup-plan.md §2.1. |
| Worker Step C | 완료 처리 | 성공 시 “서비스 준비 완료” 이메일 발송(선택), Tenant 상태 **‘Active’**로 변경, 콜백(§4.5) 수행. |
3. 입출력 스펙
3.1 파이프라인 입력(트리거 시 전달)
| 이름 | 필수 | 타입 | 설명 |
|---|---|---|---|
| job_id | 예 | string | provision_jobs.job_id (UUID 등). 콜백·로그·trace 연동 시 사용. |
| tenant_id | 예 | string | 테넌트 ID. canonical_hostname = tenant-{short_id}.pregoi.com (short_id = tenant_id UUID 앞 8자). Frappe site·Ansible·Zuplo에는 canonical 사용. 사용자 노출 URL은 {subdomain_slug}.pregoi.com (CNAME → canonical). tenant-subdomain-dns-design.md 참조. |
| region | 예 | string | sg | us | eu. Pulumi 스택 선택, 노드 조회 시 사용. |
| target_server_id | 아니오 | string | 기존 노드 배치 시 nodes.node_id. 있으면 Pulumi 단계 스킵, D1에서 해당 노드의 host(IP/FQDN) 조회. |
| create_new_server | 아니오 | boolean/int | true 또는 1이면 신규 서버 필요. target_server_id 없을 때 Pulumi Up 실행. |
전달 경로
- workflow_dispatch:
inputs.job_id,inputs.tenant_id,inputs.region,inputs.target_server_id,inputs.create_new_server. - Provision Worker HTTP: JSON body 동일 키.
- Provision Worker Cron: D1에서 Pending job 1건 조회 시 해당 행의
job_id,tenant_id,region,target_server_id,create_new_server사용.
3.2 단계별 중간 출력(다음 단계로 전달)
| 단계 | 출력 이름 | 설명 |
|---|---|---|
| 1. Pulumi(조건부) | server_ip | Pulumi stack output 또는 export. Ansible 인벤토리 ansible_host에 사용. |
| 2. 노드 조회(조건부) | server_ip | target_server_id 있을 때 D1 nodes 테이블에서 해당 node_id의 host 값. |
| 3. Ansible | api_key | prego-ansible 레포에서 playbook 실행 → artifacts/tenant_api_key.txt. 워크플로는 해당 파일을 artifact로 업로드하고, Zuplo Sync 단계에서 Prego의 config/artifacts/에 둔 뒤 전달. |
| 4. Zuplo Sync | (없음) | 성공 시 다음 단계로. 실패 시 콜백에 실패 보고. |
| 5. 콜백 | (없음) | 성공 시 job_id·tenant_id·server_id·site_name·api_key_ref(또는 key_id) 등 전달. |
3.3 콜백 페이로드(성공 시 Control Plane에 전달할 내용)
| 키 | 타입 | 설명 |
|---|---|---|
| job_id | string | 갱신할 provision_jobs 행. |
| tenant_id | string | 갱신할 tenants_master·tenant_runtime. |
| status | string | Completed. |
| server_id / node_id | string | 배치된 노드 ID(nodes.node_id). |
| host | string | Ansible이 실행된 호스트(IP 또는 FQDN). |
| site_name | string | canonical_hostname (예: tenant-a1b2c3d4.pregoi.com). Frappe site·라우팅은 canonical만 사용. |
| api_key_ref | string | (선택) Zuplo key id 또는 secret_refs용 참조. API Key 원문은 전달하지 않음. |
실패 시: status: Failed, error_message(또는 stage where failed) 전달. Control Plane은 provision_jobs.status=Failed, trace_events에 실패 단계 기록.
4. 단계별 설계 요건
4.1 1단계: job 입력 확보
- 워크플로:
workflow_dispatchinputs에서 job_id, tenant_id, region, target_server_id, create_new_server 읽기. - Worker(Cron): D1
SELECT * FROM provision_jobs WHERE status = 'Pending' ORDER BY created_at LIMIT 1. 처리 시작 시 해당 행을status='Running'으로 갱신(선택, 동시 실행 방지). - 선행: Control Plane이 이미 provision_jobs에 행을 넣고, 배치 결정 결과(target_server_id 또는 create_new_server)를 입력으로 넘겼다는 가정.
4.2 2단계: 서버 IP 결정 — Pulumi 또는 기존 노드 조회
| 조건 | 동작 |
|---|---|
| target_server_id 있음 | Pulumi 실행 없음. D1 nodes에서 node_id = target_server_id인 행의 host를 server_ip로 사용. (nodes가 다른 DB/서비스에 있으면 해당 API 조회.) |
| target_server_id 없음, create_new_server = true | Pulumi Up 실행. region에 맞는 스택 선택, pulumi up 후 pulumi stack output server_ip(또는 동일한 출력 이름)를 server_ip로 사용. |
| 둘 다 없음 | 정책에 따라: 기본값으로 create_new_server=true 처리하거나, 에러로 빠져 콜백에 Failed 전달. |
Pulumi 실행 환경
- 워크플로:
prego-pulumicheckout,pulumi stack select <region>,pulumi up -y,pulumi stack output server_ip. - Worker: Pulumi를 Worker 내에서 실행하지 않으므로, “Pulumi 단계”는 별도 워크플로 호출 또는 외부 서비스 호출로 대체. 호출 시 region·tenant_id(또는 job_id) 전달, 응답으로 server_ip 수신.
신규 서버 생성 후 D1 반영
- Pulumi로 서버가 생성되면, 해당 server_ip·region을 D1
nodes에 삽입(또는 별도 “노드 등록” API 호출)하는 단계를 설계에 포함. 워크플로에서 “Pulumi up 후 노드 등록 API 호출” 또는 Worker에서 “Pulumi 워크플로 완료 이벤트 수신 후 D1 삽입” 등.
4.3 3단계: Ansible 실행
- 입력: server_ip, tenant_id.
- 동작: prego-ansible 레포를 checkout한 뒤 인벤토리를 동적으로 생성(server_ip·tenant_id 치환),
ansible-playbook -i inventory/inventory.yml playbook.yml -e tenant_id=...실행. - 출력: prego-ansible의
artifacts/tenant_api_key.txt에 API 키 기록. 워크플로에서는 해당 파일을 artifact로 업로드하고, Zuplo Sync 단계에서 다운로드해 Prego의config/artifacts/에 둔 뒤 zuplo_sync에 전달. - 실행 위치: GitHub Actions면 Self-hosted runner 또는 Ansible이 설치된 컨테이너/호스트. Worker면 Ansible을 실행할 수 없으므로 “Ansible 전용 워크플로” 또는 외부 실행기를 HTTP로 트리거하고 결과(api_key)를 돌려받는 설계.
Playbook 태스크 구성 전략 (한 트랜잭션처럼)
Ansible playbook은 아래 순서로 테넌트 전용 리소스를 만들고, 단계 실패 시 전체를 실패로 처리하는 구조를 권장한다. (코드 없이 단계만 정의.)
① 테넌트 전용 MariaDB DB 생성 → ② 해당 DB 전용 사용자·권한 부여 → ③ Frappe 사이트 디렉터리 생성 및 소유권 설정 → ④ site_config.json 템플릿 배포 → ⑤ bench(또는 동일 도구)로 사이트 초기화·install-app(예: erpnext, hrms).
변수는 파이프라인에서 주입: tenant_id, site_name(또는 canonical_hostname), db_root_password(Vault 등 시크릿).
4.4 4단계: Zuplo Sync
- 입력: tenant_id, api_key(파일 내용 또는 문자열).
- 동작:
infra/zuplo_sync.ts의syncTenantApiKey()호출 또는 CLInpx tsx infra/zuplo_sync.ts <tenant_id> --api-key-file <path>. ZUPLO_ACCOUNT_NAME, ZUPLO_BUCKET_NAME, ZUPLO_API_KEY는 Secrets/환경 변수에서 주입. - 실패 시: 전체 파이프라인 실패로 간주, 콜백에 Failed + 단계 정보 전달. Ansible은 이미 완료되었으므로 재시도 시 Zuplo Sync부터 실행하는 단계별 재시도 설계 권장.
4.5 5단계: Control Plane 콜백
- 성공 시: 위 §3.3 콜백 페이로드로 Control Plane API
POST /internal/provision-complete(가정) 호출. Control Plane은 provision_jobs.status=Completed, tenants_master.status=Active, tenant_runtime 삽입/갱신. - 실패 시:
POST /internal/provision-failed(가정) 또는 동일 엔드포인트에 status=Failed, job_id, error_message, stage 전달. Control Plane은 provision_jobs.status=Failed, trace_events에 기록. - Worker가 D1 직접 갱신하는 경우: 콜백 API 없이 Worker가 D1 바인딩으로 직접 UPDATE provision_jobs, UPDATE tenants_master, INSERT/UPDATE tenant_runtime 수행. 이때 Worker에 D1 쓰기 권한과 SQL(또는 ORM) 구현 필요.
5. GitHub Actions 워크플로 설계 상세
- 이름:
provision-tenant(또는Provision Tenant). - 트리거:
workflow_dispatchwith inputs: job_id, tenant_id, region, target_server_id(optional), create_new_server(optional). - 환경: 리전별로 다른 Secret 사용 시
environment: production-sg등 선택 입력과 매핑. - Job 구성
- Job 1: 조건 분기 — target_server_id 있으면 “resolve-server” step에서 D1(또는 API) 조회로 server_ip 설정; 없고 create_new_server면 “pulumi-up” step 실행 후 output에서 server_ip 설정.
- Job 2: Ansible — 인벤토리 생성(또는 기존 파일에 server_ip 치환), playbook 실행, artifact
tenant_api_key.txt업로드. - Job 3: Zuplo Sync — artifact 다운로드,
npx tsx infra/zuplo_sync.ts ... --api-key-file ...실행. - Job 4: 콜백 — job_id, tenant_id, status=Completed, server_id, host, site_name, api_key_ref 등 페이로드로 Control Plane API 호출.
- Secrets: HCLOUD_TOKEN_SG/US/EU, ZUPLO_ACCOUNT_NAME, ZUPLO_BUCKET_NAME, ZUPLO_API_KEY, CONTROL_PLANE_CALLBACK_URL, CONTROL_PLANE_API_KEY(또는 내부 전용 토큰), Ansible SSH private key(또는 runner에 이미 배치).
- 실패 처리: 단계별로 실패 시 즉시 콜백(Failed) 호출 후 job 실패. 재시도는 워크플로 수준이 아닌 “동일 입력으로 workflow_dispatch 재호출”로 처리 가능.
6. Provision Worker 설계 요약
- 트리거: ① HTTP
POST /provision/run(body: job_id, tenant_id, region, target_server_id?, create_new_server?). ② Cron(예: 1분마다)으로 D1 provision_jobs에서 Pending 1건 조회 후 처리. - 한계: Worker 내부에서 Pulumi·Ansible 실행이 사실상 불가하므로, “오케스트레이터” 역할만 담당.
- Pulumi: GitHub Actions
workflow_dispatch(pulumi-up 전용) 호출 후 완료 대기(폴링 또는 webhook). - Ansible: 동일하게 Ansible 전용 워크플로 또는 외부 실행기 호출 후 api_key 반환 수신.
- Zuplo: Worker에서
fetch로 Zuplo API 호출. - 콜백: Worker가 D1에 직접 UPDATE/INSERT.
- Pulumi: GitHub Actions
- 데이터 흐름: Worker가 job 입력 확보 → (필요 시) 외부 Pulumi 실행 호출 → server_ip 수신 → Ansible 실행 호출 → api_key 수신 → Zuplo API 호출 → D1 직접 갱신.
7. 보안·멱등·재시도
| 항목 | 요건 |
|---|---|
| 보안 | API Key·Hetzner 토큰·SSH 키·콜백 API 키는 Secrets/환경 변수로만 주입. 로그·아티팩트에 API Key 원문 출력 금지(no_log, mask). |
| 멱등 | 동일 job_id로 재실행 시: Pulumi는 이미 리소스 존재하면 변경 없음; Ansible은 new-site·api-key가 이미 있으면 creates/조건으로 스킵 또는 에러 처리; Zuplo는 동일 tenant_id로 재등록 시 정책(덮어쓰기/에러) 정의. |
| 재시도 | 실패 시 job은 Failed로 두고, “같은 job_id로 다시 트리거”하면 Pulumi 스킵 후 Ansible(또는 실패한 단계)부터 재실행 가능하도록 단계를 나누어 설계. |
7.1 확장성 관리 전략 (온보딩·리소스·로그)
| 항목 | 전략 요약 |
|---|---|
| DB 부하 분산 | 테넌트 증가 시 단일 DB 서버 한계를 줄이기 위해, 온보딩 파이프라인에 “현재 가장 한가한 DB 서버” 선택 로직을 둔다. server_metrics·tenant_count 기반으로 할당. saas-db-separation-and-scaling-plan.md Phase 2/3와 연동. |
| 리소스 격리 | 특정 테넌트의 무거운 작업이 다른 테넌트에 영향을 주지 않도록, Docker Compose 레벨에서 CPU·Memory 제한(deploy.resources.limits)을 적용한다. Cgroups 기반 격리로 노이즈 감소. |
| 로그 통합 | 테넌트가 여러 노드에 분산되므로, Cloudflare Logpush 또는 ELK Stack 등으로 로그를 한곳에 모아 조회·알람·감사 체계를 갖춘다. 상세: cloudflare-logpush-observability-plan.md. |
8. 선행 조건·의존 관계
- D1 마이그레이션 0007 적용됨(provision_jobs.target_server_id, create_new_server, nodes, server_metrics).
- Ansible playbook이 artifact에 API 키를 쓰고, zuplo_sync가
--api-key-file지원함(이미 구현됨). - Control Plane(또는 동일 권한)에 “콜백” 수신용 엔드포인트 또는 Worker의 D1 쓰기 권한이 있음.
- Pulumi 스택(region별) 및 HCLOUD_TOKEN_*·Cloudflare 설정이 준비되어 있음.
9. 검증·체크리스트(구현 후)
- workflow_dispatch(job_id, tenant_id, region, create_new_server=1) 시 Pulumi Up → Ansible → Zuplo → 콜백까지 성공, provision_jobs=Completed, tenants_master=Active, tenant_runtime 반영.
- workflow_dispatch(…, target_server_id=기존 node_id) 시 Pulumi 스킵, D1에서 host 조회 후 Ansible만 해당 서버로 실행, Zuplo·콜백 성공.
- Ansible 또는 Zuplo 단계 실패 시 콜백에 Failed 전달, provision_jobs.status=Failed, trace_events 기록.
- 동일 job_id로 재실행 시 의도한 대로 멱등 동작(또는 명시적 “재시도” 정책).
- 로그·아티팩트에 API Key가 노출되지 않음.
10. 참조 문서
| 문서 | 용도 |
|---|---|
| intelligent-automation-implementation-plan.md | 구현 순서·산출물 §3.3 |
| saas-unified-architecture-hetzner-cloudflare-zuplo-plan.md | §9.2 전 구간 자동화, §9.6 고려사항 |
| tenant-provisioning-flow.md | §A.5 워크플로/Worker 동작 순서 |
| provision-tenant-pipeline.md | 수동 실행 순서·워크플로 입력 스펙 |