From 89fca872c0cc98667e5c670389309b93ca3b9288 Mon Sep 17 00:00:00 2001 From: jungwoo choi Date: Fri, 28 Aug 2026 12:08:11 +0900 Subject: [PATCH] =?UTF-8?q?cli=20v0.13.0:=20=EA=B3=B5=EC=9C=A0=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EC=86=8C=EC=8A=A4=20CLI=20=EC=84=A0=EC=96=B8(sourc?= =?UTF-8?q?e=20add=20--shared,=20mode:shared)=20+=20=EB=AC=B8=EC=84=9C=203?= =?UTF-8?q?=EA=B3=84=EC=B8=B5=C2=B7cluster=20create=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit source add --shared / requires[].mode:shared 로 공유 외부 소스를 매니페스트에 선언 → 배포 시 백엔드가 클러스터별 격리 DB/계정 민팅(인클러스터 StatefulSet 없음, 소용량 클러스터용). reconcile_source 가 mode 를 POST /services 로 전달(local 하드코딩 제거). 문서(슬래시명령·매니페스트 템플릿·DATA-SOURCES·SKILL·bin help): 로컬 dev/migrate 제거·dev/val/prod 3계층 승격(promote --to)·cluster create/ls·project check --env 반영. Co-Authored-By: Claude Opus 4.8 --- .claude/commands/yakcloud.md | 8 ++++---- .claude/skills/yakcloud-deploy/SKILL.md | 6 +++--- DATA-SOURCES.md | 10 +++++++--- bin/yakcloud | 22 ++++++++++++---------- scripts/yakcloud_ctl.py | 20 +++++++++++++++----- scripts/yakcloud_deploy.py | 13 +++++++++---- yakcloud.yaml | 9 ++++++--- 7 files changed, 56 insertions(+), 32 deletions(-) diff --git a/.claude/commands/yakcloud.md b/.claude/commands/yakcloud.md index b637273..3392008 100644 --- a/.claude/commands/yakcloud.md +++ b/.claude/commands/yakcloud.md @@ -25,17 +25,17 @@ allowed-tools: Bash, Read, Write, Edit, Glob, Grep, AskUserQuestion - **그 외 전부**(`project deploy|info|check|update`, `datasource ls`, `domain`, `scale`, `set`, `env`, `source`, `bind`, `unbind`, `config`, `upgrade`) → `yakcloud $ARGUMENTS` 를 실행하고 **출력을 그대로** 보여준 뒤, 필요할 때만 한 줄로 해석. 배포/승격/삭제성(`deploy`·`promote`·`source rm`·`unbind`·`domain`)은 실행 전 무엇을 하는지 한 줄 알리고 진행. -- 개발·운영 모두 yakcloud 클러스터에서 배포·테스트한다(로컬 docker 데이터소스 없음). 매니페스트 `environments.{dev,prod}`. +- 개발·검증·운영 모두 yakcloud 클러스터에서 배포·테스트한다(로컬 docker 데이터소스 없음). 매니페스트 `environments.{dev,val,prod}`(val=검증계, 선택). ## 2) `project init` — 마법사 대체 (핵심) 터미널 마법사는 TTY가 없으면 안 뜬다. 네가 대신한다: 1. `yakcloud project init -y` 로 **비대화형 스캐폴딩**(기본 매니페스트 생성; `bin/`·`install.sh` 잔재 없음). 2. `$ARGUMENTS`/대화 맥락에 이미 있는 값은 **재질문 금지**. 부족한 핵심값만 `AskUserQuestion` 으로 모은다: - **개발 클러스터, 운영 클러스터**(같으면 하나로), 앱 포트, 노출 경로(기본 `/`), CPU/메모리, 필요한 데이터소스(있으면), 운영 도메인(있으면). - → 매니페스트 `environments.dev.cluster` / `environments.prod.cluster` / `environments.prod.domains` 에 반영. + **개발 클러스터, (검증 클러스터·선택), 운영 클러스터**(같으면 하나로), 앱 포트, 노출 경로(기본 `/`), CPU/메모리, 필요한 데이터소스(있으면), 운영 도메인(있으면). + → 매니페스트 `environments.dev.cluster` / (`environments.val.cluster`) / `environments.prod.cluster` / `environments.prod.domains` 에 반영. 3. 모은 값을 CLI로 반영: - `yakcloud set --port … --cpu … --mem … --path …` - - 소스: `yakcloud source add [plan]` + `yakcloud bind ` + - 소스: 인클러스터(local)=`yakcloud source add [plan]`, 공유 외부(shared)=`yakcloud source add mongodb --shared`(백엔드가 클러스터별 격리 DB 민팅; 콘솔/API 등록도 가능) + `yakcloud bind `. - 도메인: `yakcloud domain ` - 이용 가능한 클러스터 소스를 먼저 보고 싶으면 `yakcloud datasource ls`. 4. 자격 미설정이면(`yakcloud config` 로 확인) 터미널 `yakcloud login` 안내. diff --git a/.claude/skills/yakcloud-deploy/SKILL.md b/.claude/skills/yakcloud-deploy/SKILL.md index af5029b..73d1bbe 100644 --- a/.claude/skills/yakcloud-deploy/SKILL.md +++ b/.claude/skills/yakcloud-deploy/SKILL.md @@ -27,8 +27,8 @@ YakCloud 콘솔 API로 **데이터소스 리컨실 + 워크로드 배포 + 바 - **"프로젝트 초기화"** → 빈 폴더에서 `yakcloud project init [name]` (스타터+CI+매니페스트+이 스킬) - **"개발 배포"** → `yakcloud project deploy` (자동 버전업 → 태그 push → CI 빌드 → **개발 클러스터**). 기본 도메인으로 확인. - **"승격"**(개발→검증→운영) → `yakcloud project promote [vX.Y.Z] [--to val|prod]` (그 이미지를 **재빌드 없이 상위 환경으로**; `--to prod` 는 운영 도메인 부착). 검증계(val)는 선택 — `environments` 에 없으면 개발→운영 2단계 -- **"프로젝트 개괄"** → `yakcloud project info` · 사전 점검 → `yakcloud project check [--prod]` -- 매니페스트 `environments.{dev,prod}.cluster` (+ `prod.domains`). 로컬 개발/로컬 데이터소스는 없음 — 개발도 클러스터 배포로 확인. +- **"프로젝트 개괄"** → `yakcloud project info` · 사전 점검 → `yakcloud project check [--env dev|val|prod]` +- 매니페스트 `environments.{dev,val,prod}.cluster` (val 선택, + `prod.domains`). 로컬 개발/로컬 데이터소스는 없음 — 개발도 클러스터 배포로 확인. **배포환경 설정(앱별) — 매니페스트 수정 + 배포중이면 재빌드 없이 라이브 반영** - `yakcloud domain [wl]` 도메인 등록 + 워크로드 할당 @@ -115,7 +115,7 @@ git tag v0.1.0 && git push origin v0.1.0 # v* 태그만 배포 트리거(일 | `docker create` 실패(잡 시작 안 됨) | 러너 config에서 docker.sock **중복 마운트** → act 기본 마운트만(수동 제거) | | 배포 중 `429 RATE_LIMITED` | CLI가 `retryAfterSec` 백오프로 재시도(내장) | | 다른 러너가 잡 가져감 | 라벨 충돌 → 전용 라벨(`runs-on`) 사용 | -| 소스 프로비저닝 `405` | 배포 토큰엔 소스 **생성 권한 없음** → 콘솔에서 소스 만들고 `requires[].name`·`binds[].source`를 그 소스명으로. `yakcloud project check`가 미리 경고(READY 소스 목록 출력) | +| 소스 프로비저닝 `405` | 배포 토큰엔 소스 생성 권한 없음 → (1) **인클러스터(local)**: 콘솔에서 소스 먼저 만들고 `requires[].name`·`binds[].source`를 그 소스명으로; (2) **공유 외부(shared, 소용량 S용)**: `requires` 에 `mode: shared` 선언(`yakcloud source add mongodb --shared`; 콘솔/API 등록도 가능) → 배포 시 백엔드가 클러스터별 격리 DB/계정 민팅. `yakcloud project check`가 미리 경고(READY 소스 목록 출력) | | 태그 push 했는데 아무 일도 안 남 | 레포에 **러너 0개**(잡이 조용히 큐잉) → `yakcloud project deploy`가 배포 전 러너 프리플라이트 경고. 러너 없으면 `yakcloud project deploy --local`(로컬 빌드·push 후 API 배포) | | 헬스체크 `/healthz` 404/이상 | 엣지가 `/healthz`를 **가로챔** → 배포 검증은 앱 실제 경로(`/`)로. 컨테이너 readiness엔 여전히 `health:` 사용 OK | | `ModuleNotFoundError: yaml`(직접 배포) | `pip install pyyaml`(또는 `pip3 install --break-system-packages pyyaml`). CI 이미지엔 포함됨 | diff --git a/DATA-SOURCES.md b/DATA-SOURCES.md index 293519a..208cabb 100644 --- a/DATA-SOURCES.md +++ b/DATA-SOURCES.md @@ -36,9 +36,13 @@ > 비밀번호·키는 인클러스터 Secret으로 주입되고 콘솔/브라우저에 노출되지 않는다. URL의 user/pw는 URL-인코딩됨. -## 개발/운영 모두 클러스터에서 -데이터 소스는 **클러스터 안에서 실제로 프로비저닝**된다(로컬 docker 없음). 개발 클러스터에 배포하면 그 클러스터의 소스가 -`_*` 로 주입되고, 운영 클러스터로 승격하면 운영 클러스터의 소스가 주입된다 — 각 환경이 자체 데이터 계층을 가진다. +## 개발/검증/운영 모두 클러스터에서 +데이터 소스는 두 가지 모드: +1. **인클러스터(local)**: 클러스터 안에서 실제로 프로비저닝(StatefulSet). 개발 클러스터에 배포하면 그 클러스터의 소스가 + `_*` 로 주입되고, 상위 환경으로 승격하면 그 환경 클러스터의 소스가 주입된다 — 각 환경이 자체 데이터 계층을 가진다. +2. **공유 외부(shared)**: 소용량(S) 클러스터용. 매니페스트 `requires` 에 `{ name, type: mongodb, mode: shared }` 로 선언 + (또는 `yakcloud source add mongodb --shared`; 콘솔/API 등록도 가능). 배포 시 백엔드가 클러스터별 격리 DB/계정을 + 만들어, 모든 환경이 같은 외부 소스 이름으로 환경별 격리 접속(인클러스터 StatefulSet 불필요). 환경 간 데이터 백업·싱크는 별도(추후). ## 앱 코드 예시 diff --git a/bin/yakcloud b/bin/yakcloud index 19946fa..9807dd9 100755 --- a/bin/yakcloud +++ b/bin/yakcloud @@ -1,20 +1,22 @@ #!/usr/bin/env bash -# yakcloud — YakCloud 프로젝트 CLI. 개발·운영 모두 yakcloud 클러스터에서(로컬 dev 없음). +# yakcloud — YakCloud 프로젝트 CLI. 개발·검증·운영 모두 yakcloud 클러스터(로컬 dev 없음, val 선택). # +# yakcloud cluster create [--template S|M] [--nodes N] [--golden] 클러스터 생성(완료까지 대기) +# yakcloud cluster ls 내 클러스터 목록 # yakcloud project init [name] 빈 폴더에 스타터 스캐폴딩(앱+CI+매니페스트+Claude 스킬) # yakcloud project deploy [vX.Y.Z] 커밋 + 태그 push → CI 빌드 → 개발 클러스터 배포 # yakcloud project promote [vX.Y.Z] [--to val|prod] 그 이미지를 재빌드 없이 상위 환경으로 승격(개발→검증→운영) -# yakcloud project info 프로젝트 개괄(매니페스트 + dev/prod 라이브 상태) -# yakcloud project check [--prod] dry-run(직접 API 계획만) +# yakcloud project info 프로젝트 개괄(매니페스트 + 라이브 상태) +# yakcloud project check [--env dev|val|prod] dry-run(직접 API 계획만) # # yakcloud domain [wl] · scale · set · env · datasource ls · source · bind/unbind # -# 매니페스트: environments.{dev,prod}.cluster (+ prod.domains). env: YAKCLOUD_URL, YAKCLOUD_TOKEN(yakd_…) +# 매니페스트: environments.{dev,val,prod}.cluster (val 선택, + prod.domains). env: YAKCLOUD_URL, YAKCLOUD_TOKEN(yakd_…) set -uo pipefail # -e 미사용: 'test && action' 관용구가 값 없을 때 CLI 를 중단시키는 함정 회피. 중요 경로는 명시적 || die. REPO="${YAKCLOUD_STARTER_REPO:-https://gitea.yakenator.io/yakenator/yakcloud-starter}" BRANCH="${YAKCLOUD_STARTER_BRANCH:-main}" -VERSION="0.12.3" +VERSION="0.13.0" CONFIG_DIR="${YAKCLOUD_CONFIG_DIR:-$HOME/.config/yakcloud}" CONFIG_FILE="$CONFIG_DIR/config" @@ -437,15 +439,15 @@ cmd_project_update() { usage() { cat < [--template S|M] [--nodes N] [--golden] 클러스터 생성(프로비저닝 완료까지 대기) cluster ls 내 클러스터 목록(이름·상태·노드·기본 도메인) project init [name] 빈 폴더에 스타터 스캐폴딩(대화형 마법사) + 자격 프롬프트 project deploy [vX.Y.Z] 커밋 + 태그 push → CI 빌드 → **개발 클러스터** 배포(버전 생략=자동 버전업) project deploy --local 러너 없이 로컬 build/push/리컨실(CI 대안, 개발 클러스터) project promote [vX.Y.Z] [--to val|prod] 그 이미지를 재빌드 없이 상위 환경으로 승격(개발→검증→운영). 기본 --to prod - project info 프로젝트 개괄(매니페스트 + dev/prod 라이브 상태) - project check [--prod] dry-run(직접 API 계획만) + project info 프로젝트 개괄(매니페스트 + 라이브 상태) + project check [--env dev|val|prod] dry-run(직접 API 계획만) project update 이 프로젝트의 엔진·CI·스킬만 최신화(앱·매니페스트 보존) upgrade 전역 yakcloud CLI 자기 갱신(최신 버전으로) ── 자격(한 곳에 안전 보관; 비밀=OS 키체인 암호화) ── @@ -458,9 +460,9 @@ yakcloud $VERSION — YakCloud 프로젝트 CLI (개발·운영 모두 yakcloud set --image/--port/--health/--cpu/--mem/--path/--rewrite env KEY=VAL … [--secret KEY] [--unset KEY] datasource ls 클러스터에서 이용 가능한 소스 목록(이름·타입·내부/외부·상태) - source add [plan] | source rm + source add [plan] [--shared] | source rm (--shared=공유 외부, 백엔드가 격리 DB 민팅) bind | unbind -매니페스트: environments.{dev,prod}.cluster(+prod.domains). env: YAKCLOUD_URL, YAKCLOUD_TOKEN(배포토큰 yakd_…) +매니페스트: environments.{dev,val,prod}.cluster(val 선택, +prod.domains). env: YAKCLOUD_URL, YAKCLOUD_TOKEN(배포토큰 yakd_…) EOF } diff --git a/scripts/yakcloud_ctl.py b/scripts/yakcloud_ctl.py index 9f0eee3..da651ad 100644 --- a/scripts/yakcloud_ctl.py +++ b/scripts/yakcloud_ctl.py @@ -232,10 +232,17 @@ def cmd_source(a) -> None: if a.action == "add": if any(r.get("name") == a.name for r in reqs): sys.exit(" ✗ 이미 있음: %s" % a.name) - reqs.append({"name": a.name, "type": a.type, "plan": a.plan}) - save_manifest(m) - print(" ✓ requires += {name=%s, type=%s, plan=%s}" % (a.name, a.type, a.plan)) - print(" · 프로비저닝/바인딩은 'yakcloud bind %s ' 후 'yakcloud deploy'" % a.name) + if getattr(a, "shared", False): # 공유 외부 소스 — 배포 시 백엔드가 클러스터별 격리 DB/계정 민팅 + reqs.append({"name": a.name, "type": a.type, "mode": "shared"}) + save_manifest(m) + print(" ✓ requires += {name=%s, type=%s, mode=shared}" % (a.name, a.type)) + print(" · 공유 외부 소스 — 배포 시 백엔드가 클러스터별 격리 DB/계정 민팅(인클러스터 StatefulSet 없음).") + print(" · 바인딩: 'yakcloud bind %s ' 후 'yakcloud deploy'" % a.name) + else: + reqs.append({"name": a.name, "type": a.type, "plan": a.plan}) + save_manifest(m) + print(" ✓ requires += {name=%s, type=%s, plan=%s}" % (a.name, a.type, a.plan)) + print(" · 프로비저닝/바인딩은 'yakcloud bind %s ' 후 'yakcloud deploy'" % a.name) else: # rm m["requires"] = [r for r in reqs if r.get("name") != a.name] # 관련 바인딩도 정리 @@ -362,7 +369,10 @@ def build_parser() -> argparse.ArgumentParser: sosub = so.add_subparsers(dest="action", required=True) sol = sosub.add_parser("ls", help="클러스터에서 이용 가능한 소스 목록"); sol.set_defaults(fn=cmd_source_ls) soa = sosub.add_parser("add"); soa.add_argument("name"); soa.add_argument("type") - soa.add_argument("plan", nargs="?", default="small"); soa.set_defaults(fn=cmd_source) + soa.add_argument("plan", nargs="?", default="small") + soa.add_argument("--shared", action="store_true", + help="공유 외부 소스(mode:shared) — 백엔드가 클러스터별 격리 DB/계정 민팅(소용량 클러스터용, 현재 mongodb)") + soa.set_defaults(fn=cmd_source) sor = sosub.add_parser("rm"); sor.add_argument("name"); sor.set_defaults(fn=cmd_source) b = sub.add_parser("bind", help="워크로드에 소스 바인딩") diff --git a/scripts/yakcloud_deploy.py b/scripts/yakcloud_deploy.py index 4e65408..b8f7916 100644 --- a/scripts/yakcloud_deploy.py +++ b/scripts/yakcloud_deploy.py @@ -99,18 +99,23 @@ def reconcile_source(req: dict) -> str | None: if match and match.get("status") == "READY": log(f"source '{name}' ({stype}) 이미 READY → 스킵 (id={match['id']})") return match["id"] + mode = req.get("mode", "local") # local=인클러스터 프로비저닝 · shared=공유 외부(백엔드가 클러스터별 격리 DB 민팅) if DRY: if match: log(f"source '{name}' 상태={match.get('status')} — READY 대기 필요") else: - log(f"source '{name}' ({stype}, {plan}) 없음 → 프로비저닝 예정(POST /services). 현재 READY: {_ready_sources()}") + plan_or_mode = "shared" if mode == "shared" else plan + log(f"source '{name}' ({stype}, {plan_or_mode}) 없음 → 프로비저닝 예정(POST /services, mode={mode}). 현재 READY: {_ready_sources()}") return None if not match: - log(f"source '{name}' ({stype}, {plan}) 프로비저닝 시도…") + log(f"source '{name}' ({stype}, {'shared' if mode=='shared' else plan}) 프로비저닝 시도… (mode={mode})") try: # 소스 생성 = POST /services (clusterId 는 body). /clusters/{id}/services 는 GET 전용. - api("POST", "/services", - {"clusterId": CLUSTER, "type": stype, "name": name, "mode": "local", "size": plan}) + # local=인클러스터 프로비저닝(size 사용) · shared=공유 외부(size 무관, 백엔드가 격리 DB/계정 민팅). + body = {"clusterId": CLUSTER, "type": stype, "name": name, "mode": mode} + if mode == "local": + body["size"] = plan + api("POST", "/services", body) except SystemExit as e: raise SystemExit( f"소스 '{name}' 생성 실패 — 콘솔에서 소스를 만든 뒤 requires[].name·binds[].source 를 그 소스명으로 " diff --git a/yakcloud.yaml b/yakcloud.yaml index 9deef06..087fa19 100644 --- a/yakcloud.yaml +++ b/yakcloud.yaml @@ -17,9 +17,12 @@ environments: # 필요한 데이터 소스(논리 이름). 없으면 [] 로 둬도 됨. # type ∈ postgresql·mysql·mariadb·mongodb·redis·minio·rabbitmq·solr·oracle, plan ∈ small·medium·large -# ⚠ 이름이 클러스터의 기존 READY 소스와 일치하면 스킵·바인딩. 없을 때 자동 프로비저닝은 -# "배포 토큰에 소스 생성 권한이 있을 때"만 됩니다(없으면 405). 권한이 없으면 콘솔에서 소스를 -# 먼저 만들고 이 name 을 그 소스명으로 지정하세요. ('yakcloud project check' 로 미리 확인) +# ⚠ 이름이 클러스터의 기존 READY 소스와 일치하면 스킵·바인딩. +# - 인클러스터(local) 소스: 배포 토큰에 생성 권한이 있으면 자동 프로비저닝(없으면 405 → 콘솔에서 먼저 만들고 이 name 사용). +# - 공유 외부(shared) 소스(소용량 S 클러스터용): 아래처럼 mode: shared 로 선언(또는 'yakcloud source add mongodb --shared'). +# 배포 시 백엔드가 클러스터별 격리 DB/계정을 민팅(인클러스터 StatefulSet 불필요). 콘솔/API 등록도 가능. 예: +# - { name: appdb, type: mongodb, mode: shared } +# ('yakcloud project check' 로 미리 확인·리스팅) requires: - { name: appdb, type: postgresql, plan: small }