cli v0.13.0: 공유 외부 소스 CLI 선언(source add --shared, mode:shared) + 문서 3계층·cluster create 정리
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 <noreply@anthropic.com>
This commit is contained in:
@ -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 <name> -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 <wl> --port … --cpu … --mem … --path …`
|
||||
- 소스: `yakcloud source add <name> <type> [plan]` + `yakcloud bind <wl> <name> <alias>`
|
||||
- 소스: 인클러스터(local)=`yakcloud source add <name> <type> [plan]`, 공유 외부(shared)=`yakcloud source add <name> mongodb --shared`(백엔드가 클러스터별 격리 DB 민팅; 콘솔/API 등록도 가능) + `yakcloud bind <wl> <name> <alias>`.
|
||||
- 도메인: `yakcloud domain <fqdn> <wl>`
|
||||
- 이용 가능한 클러스터 소스를 먼저 보고 싶으면 `yakcloud datasource ls`.
|
||||
4. 자격 미설정이면(`yakcloud config` 로 확인) 터미널 `yakcloud login` 안내.
|
||||
|
||||
@ -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 <fqdn> [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 <name> 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 이미지엔 포함됨 |
|
||||
|
||||
@ -36,9 +36,13 @@
|
||||
|
||||
> 비밀번호·키는 인클러스터 Secret으로 주입되고 콘솔/브라우저에 노출되지 않는다. URL의 user/pw는 URL-인코딩됨.
|
||||
|
||||
## 개발/운영 모두 클러스터에서
|
||||
데이터 소스는 **클러스터 안에서 실제로 프로비저닝**된다(로컬 docker 없음). 개발 클러스터에 배포하면 그 클러스터의 소스가
|
||||
`<ALIAS>_*` 로 주입되고, 운영 클러스터로 승격하면 운영 클러스터의 소스가 주입된다 — 각 환경이 자체 데이터 계층을 가진다.
|
||||
## 개발/검증/운영 모두 클러스터에서
|
||||
데이터 소스는 두 가지 모드:
|
||||
1. **인클러스터(local)**: 클러스터 안에서 실제로 프로비저닝(StatefulSet). 개발 클러스터에 배포하면 그 클러스터의 소스가
|
||||
`<ALIAS>_*` 로 주입되고, 상위 환경으로 승격하면 그 환경 클러스터의 소스가 주입된다 — 각 환경이 자체 데이터 계층을 가진다.
|
||||
2. **공유 외부(shared)**: 소용량(S) 클러스터용. 매니페스트 `requires` 에 `{ name, type: mongodb, mode: shared }` 로 선언
|
||||
(또는 `yakcloud source add <name> mongodb --shared`; 콘솔/API 등록도 가능). 배포 시 백엔드가 클러스터별 격리 DB/계정을
|
||||
만들어, 모든 환경이 같은 외부 소스 이름으로 환경별 격리 접속(인클러스터 StatefulSet 불필요).
|
||||
환경 간 데이터 백업·싱크는 별도(추후).
|
||||
|
||||
## 앱 코드 예시
|
||||
|
||||
22
bin/yakcloud
22
bin/yakcloud
@ -1,20 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# yakcloud — YakCloud 프로젝트 CLI. 개발·운영 모두 yakcloud 클러스터에서(로컬 dev 없음).
|
||||
# yakcloud — YakCloud 프로젝트 CLI. 개발·검증·운영 모두 yakcloud 클러스터(로컬 dev 없음, val 선택).
|
||||
#
|
||||
# yakcloud cluster create <name> [--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 <fqdn> [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 <<EOF
|
||||
yakcloud $VERSION — YakCloud 프로젝트 CLI (개발·운영 모두 yakcloud 클러스터)
|
||||
yakcloud $VERSION — YakCloud 프로젝트 CLI (개발·검증·운영 모두 yakcloud 클러스터, val 선택)
|
||||
cluster create <name> [--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 <wl> --image/--port/--health/--cpu/--mem/--path/--rewrite
|
||||
env <wl> KEY=VAL … [--secret KEY] [--unset KEY]
|
||||
datasource ls 클러스터에서 이용 가능한 소스 목록(이름·타입·내부/외부·상태)
|
||||
source add <name> <type> [plan] | source rm <name>
|
||||
source add <name> <type> [plan] [--shared] | source rm <name> (--shared=공유 외부, 백엔드가 격리 DB 민팅)
|
||||
bind <wl> <source> <alias> | unbind <wl> <alias>
|
||||
매니페스트: 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
|
||||
}
|
||||
|
||||
|
||||
@ -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 <workload> %s <alias>' 후 '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 <workload> %s <alias>' 후 '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 <workload> %s <alias>' 후 '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="워크로드에 소스 바인딩")
|
||||
|
||||
@ -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 를 그 소스명으로 "
|
||||
|
||||
@ -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 <name> mongodb --shared').
|
||||
# 배포 시 백엔드가 클러스터별 격리 DB/계정을 민팅(인클러스터 StatefulSet 불필요). 콘솔/API 등록도 가능. 예:
|
||||
# - { name: appdb, type: mongodb, mode: shared }
|
||||
# ('yakcloud project check' 로 미리 확인·리스팅)
|
||||
requires:
|
||||
- { name: appdb, type: postgresql, plan: small }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user