feat(cli): 0.20.0 — dev=로컬 kind 기본(project deploy 자동 생성), promote=관리형(build+push)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-30 15:29:41 +09:00
parent f699eda791
commit 0e83adc82a
3 changed files with 124 additions and 98 deletions

View File

@ -269,9 +269,19 @@ def bind_env(alias: str, conn: dict) -> dict:
# ── 워크로드: 빌드 + kind load + Deployment/Service/Ingress ───────────────────── # ── 워크로드: 빌드 + kind load + Deployment/Service/Ingress ─────────────────────
def _effective_image(image: str) -> str: def _effective_image(image: str) -> str:
"""podman 은 레지스트리(도메인) 미지정 이미지 'localhost/' 를 붙여 저장한다 → kind load·Deployment 이름 불일치. """로컬 dev 는 레지스트리에 push 하지 않고 kind-load 하므로 이미지명은 '유효한 로컬 태그'면 된다.
podman 이고 첫 컴포넌트가 레지스트리(도메인 '.'·포트 ':' 포함 또는 'localhost')가 아니면 localhost/ 를 붙여 일치시킨다. - repo(레지스트리/경로)는 소문자여야 빌드된다(docker/podman 공통) → 미설정 CHANGE_ME_GITEA_USER 같은
docker 는 그대로(무영향). 레지스트리 지정 이미지(gitea.yakenator.io/…)는 두 런타임 모두 그대로.""" 대문자 placeholder 도 로컬 빌드되게 repo 만 소문자화(태그는 대소문자 보존).
- podman 은 레지스트리 미지정 이미지에 'localhost/' 를 붙여 저장 → kind load·Deployment 이름 일치를 위해 맞춘다
(docker 무영향). 레지스트리 지정 이미지(gitea.yakenator.io/…)는 그대로."""
# 1) repo 소문자화(마지막 '/' 뒤에 ':' 가 있으면 그게 태그 구분자 → 태그 보존).
slash = image.rfind("/")
if ":" in image[slash + 1:]:
repo, tag = image.rsplit(":", 1)
image = repo.lower() + ":" + tag
else:
image = image.lower()
# 2) podman localhost/ 보정.
if RUNTIME != "podman": if RUNTIME != "podman":
return image return image
first = image.split("/", 1)[0] first = image.split("/", 1)[0]

View File

@ -4,19 +4,19 @@
# yakcloud cluster create <name> [--template S|M] [--nodes N] [--golden] 클러스터 생성(완료까지 대기) # yakcloud cluster create <name> [--template S|M] [--nodes N] [--golden] 클러스터 생성(완료까지 대기)
# yakcloud cluster ls 내 클러스터 목록 # yakcloud cluster ls 내 클러스터 목록
# yakcloud project init [name] 빈 폴더에 스타터 스캐폴딩(앱+CI+매니페스트+Claude 스킬) # yakcloud project init [name] 빈 폴더에 스타터 스캐폴딩(앱+CI+매니페스트+Claude 스킬)
# yakcloud project deploy [vX.Y.Z] 커밋 + 태그 push → CI 빌드 → 개발 클러스터 배포 # yakcloud project deploy [tag] 개발 배포 = 로컬 kind(없으면 자동 생성). 오프라인·무료·CI 불필요
# yakcloud project promote [vX.Y.Z] [--to val|prod] 그 이미지를 재빌드 없이 상위 환경으로 승격(개발→검증→운영) # yakcloud project promote [vX.Y.Z] --to val|prod 관리형 검증/운영으로 승격(build+push 후 리컨실)
# yakcloud project info 프로젝트 개괄(매니페스트 + 라이브 상태) # yakcloud project info 프로젝트 개괄(매니페스트 + 라이브 상태)
# yakcloud project check [--env dev|val|prod] dry-run(직접 API 계획만) # yakcloud project check [--env dev|val|prod] dry-run(직접 API 계획만)
# #
# yakcloud domain <fqdn> [wl] · scale · set · env · datasource ls · source · bind/unbind # yakcloud domain <fqdn> [wl] · scale · set · env · datasource ls · source · bind/unbind
# #
# 매니페스트: environments.{dev,val,prod}.cluster (val 선택, + prod.domains). env: YAKCLOUD_URL, YAKCLOUD_TOKEN(yakd_…) # 매니페스트: environments.dev=로컬 kind{cluster,port}, val/prod=관리형{cluster,+prod.domains}. env: YAKCLOUD_URL, YAKCLOUD_TOKEN(yakd_…)
set -uo pipefail # -e 미사용: 'test && action' 관용구가 값 없을 때 CLI 를 중단시키는 함정 회피. 중요 경로는 명시적 || die. set -uo pipefail # -e 미사용: 'test && action' 관용구가 값 없을 때 CLI 를 중단시키는 함정 회피. 중요 경로는 명시적 || die.
REPO="${YAKCLOUD_STARTER_REPO:-https://gitea.yakenator.io/yakenator/yakcloud-starter}" REPO="${YAKCLOUD_STARTER_REPO:-https://gitea.yakenator.io/yakenator/yakcloud-starter}"
BRANCH="${YAKCLOUD_STARTER_BRANCH:-main}" BRANCH="${YAKCLOUD_STARTER_BRANCH:-main}"
VERSION="0.19.1" VERSION="0.20.0"
# 로컬 개발 클러스터(kind) — 개발 후 운영까지 '직선 배포'의 dev 구간. # 로컬 개발 클러스터(kind) — 개발 후 운영까지 '직선 배포'의 dev 구간.
DEV_CTX="${YAK_DEV_CONTEXT:-kind-yak-dev}" DEV_CTX="${YAK_DEV_CONTEXT:-kind-yak-dev}"
DEV_CLUSTER="${YAK_DEV_CLUSTER:-yak-dev}" DEV_CLUSTER="${YAK_DEV_CLUSTER:-yak-dev}"
@ -192,12 +192,14 @@ cmd_project_init() {
# 매니페스트 채우기 마법사 — ctl 은 API 미접속(YAKCLOUD_TOKEN 비움)으로 매니페스트만 편집. # 매니페스트 채우기 마법사 — ctl 은 API 미접속(YAKCLOUD_TOKEN 비움)으로 매니페스트만 편집.
_init_wizard() { _init_wizard() {
local dflt_app="$1" app devc valc prodc pdom port health cpu mem reps path kv local dflt_app="$1" app devc devp valc prodc pdom port health cpu mem reps path kv
echo "── 프로젝트 설정 (엔터=기본값) · 개발 → 검증 → 운영 3단계 ──" echo "── 프로젝트 설정 (엔터=기본값) · 개발(로컬 kind) → 검증/운영(관리형) ──"
printf "앱 이름 [%s]: " "$dflt_app"; read -r app; app="$(_sanitize_name "${app:-$dflt_app}")" printf "앱 이름 [%s]: " "$dflt_app"; read -r app; app="$(_sanitize_name "${app:-$dflt_app}")"
printf "개발 클러스터(이름 또는 콘솔 id): "; read -r devc printf "개발 클러스터 = 로컬 kind, 이름 [dev]: "; read -r devc; devc="$(_sanitize_name "${devc:-dev}")"
printf "검증 클러스터(선택, 엔터=생략): "; read -r valc devp=80
printf "운영 클러스터 [%s]: " "${devc:-개발과 동일}"; read -r prodc; prodc="${prodc:-$devc}" if [ "$devc" != "dev" ]; then printf "로컬 인그레스 포트 [8080]: "; read -r devp; devp="${devp:-8080}"; fi
printf "검증 클러스터(관리형, 선택, 엔터=생략): "; read -r valc
printf "운영 클러스터(관리형, 선택, 엔터=생략): "; read -r prodc
printf "운영 도메인(선택, 예 app.example.com): "; read -r pdom printf "운영 도메인(선택, 예 app.example.com): "; read -r pdom
printf "컨테이너 포트 [8080]: "; read -r port; port="${port:-8080}" printf "컨테이너 포트 [8080]: "; read -r port; port="${port:-8080}"
printf "헬스체크 경로 [/healthz]: "; read -r health; health="${health:-/healthz}" printf "헬스체크 경로 [/healthz]: "; read -r health; health="${health:-/healthz}"
@ -207,20 +209,24 @@ _init_wizard() {
printf "노출 경로(ingress path) [/]: "; read -r path; path="${path:-/}" printf "노출 경로(ingress path) [/]: "; read -r path; path="${path:-/}"
sed -i.bak "s/my-app/$app/g" yakcloud.yaml && rm -f yakcloud.yaml.bak # project·워크로드명·image = 앱(프로젝트)명 sed -i.bak "s/my-app/$app/g" yakcloud.yaml && rm -f yakcloud.yaml.bak # project·워크로드명·image = 앱(프로젝트)명
[ -n "${GITEA_USER:-}" ] && { sed -i.bak "s#CHANGE_ME_GITEA_USER#${GITEA_USER}#" yakcloud.yaml 2>/dev/null; rm -f yakcloud.yaml.bak; } [ -n "${GITEA_USER:-}" ] && { sed -i.bak "s#CHANGE_ME_GITEA_USER#${GITEA_USER}#" yakcloud.yaml 2>/dev/null; rm -f yakcloud.yaml.bak; }
[ -n "$devc" ] && { sed -i.bak "s#my-dev-cluster#$devc#" yakcloud.yaml && rm -f yakcloud.yaml.bak; } # dev = 로컬 kind: environments.dev 줄의 cluster/port 를 치환(주석·나머지 보존).
sed -i.bak -E "s#^( dev:[[:space:]]*\{ cluster: )[^,}]*(, port: )[0-9]+#\1$devc\2$devp#" yakcloud.yaml && rm -f yakcloud.yaml.bak
[ -n "$prodc" ] && { sed -i.bak "s#my-prod-cluster#$prodc#" yakcloud.yaml && rm -f yakcloud.yaml.bak; } [ -n "$prodc" ] && { sed -i.bak "s#my-prod-cluster#$prodc#" yakcloud.yaml && rm -f yakcloud.yaml.bak; }
[ -n "$pdom" ] && { sed -i.bak "s#domains: \[\]#domains: [$pdom]#" yakcloud.yaml && rm -f yakcloud.yaml.bak; } [ -n "$pdom" ] && { sed -i.bak "s#domains: \[\]#domains: [$pdom]#" yakcloud.yaml && rm -f yakcloud.yaml.bak; }
if [ -n "$valc" ]; then sed -i.bak "s#my-val-cluster#$valc#" yakcloud.yaml && rm -f yakcloud.yaml.bak if [ -n "$valc" ]; then sed -i.bak "s#my-val-cluster#$valc#" yakcloud.yaml && rm -f yakcloud.yaml.bak
else sed -i.bak "/my-val-cluster/d" yakcloud.yaml && rm -f yakcloud.yaml.bak; fi # 검증 생략 시 val 줄 제거 else sed -i.bak "/my-val-cluster/d" yakcloud.yaml && rm -f yakcloud.yaml.bak; fi # 검증 생략 시 val 줄 제거
ensure_pyyaml ensure_pyyaml
YAKCLOUD_TOKEN= python3 "$(_engine ctl)" set "$app" --port "$port" --health "$health" --cpu "$cpu" --mem "$mem" --path "$path" >/dev/null 2>&1 || true # stderr 는 노출(침묵 유실 방지) — set/scale 이 실패하면 사용자가 알 수 있게. || 로 마법사는 계속.
YAKCLOUD_TOKEN= python3 "$(_engine ctl)" scale "$app" "$reps" >/dev/null 2>&1 || true YAKCLOUD_TOKEN= python3 "$(_engine ctl)" set "$app" --port "$port" --health "$health" --cpu "$cpu" --mem "$mem" --path "$path" >/dev/null \
|| echo " ⚠ 워크로드 설정(port/health/…) 반영 실패 — yakcloud.yaml 확인"
YAKCLOUD_TOKEN= python3 "$(_engine ctl)" scale "$app" "$reps" >/dev/null \
|| echo " ⚠ replicas 반영 실패 — yakcloud.yaml 확인"
echo "환경변수(KEY=VALUE, 빈 줄=종료):" echo "환경변수(KEY=VALUE, 빈 줄=종료):"
while :; do while :; do
printf " env> "; read -r kv || break; [ -z "$kv" ] && break printf " env> "; read -r kv || break; [ -z "$kv" ] && break
YAKCLOUD_TOKEN= python3 "$(_engine ctl)" env "$app" "$kv" >/dev/null 2>&1 || echo " (형식: KEY=VALUE)" YAKCLOUD_TOKEN= python3 "$(_engine ctl)" env "$app" "$kv" >/dev/null 2>&1 || echo " (형식: KEY=VALUE)"
done done
echo "✓ 매니페스트 반영: app=$app dev=${devc:-<미지정>} val=${valc:-<생략>} prod=${prodc:-<미지정>} port=$port path=$path cpu=$cpu mem=$mem replicas=$reps" echo "✓ 매니페스트 반영: app=$app · dev(로컬 kind)=$devc:$devp · val=${valc:-<생략>} · prod=${prodc:-<생략>} · port=$port path=$path cpu=$cpu mem=$mem replicas=$reps"
} }
_maybe_login_prompt() { _maybe_login_prompt() {
@ -229,89 +235,99 @@ _maybe_login_prompt() {
case "$a" in y|Y|yes|YES) cmd_login ;; *) echo " 나중에 'yakcloud login' 으로 설정하세요." ;; esac case "$a" in y|Y|yes|YES) cmd_login ;; *) echo " 나중에 'yakcloud login' 으로 설정하세요." ;; esac
} }
# 매니페스트 environments.<env>.<key> 값 출력(없으면 빈 문자열).
_manifest_env() {
ensure_pyyaml >/dev/null 2>&1 || true
python3 - "$1" "$2" <<'PY' 2>/dev/null
import sys, yaml
try: m = yaml.safe_load(open("yakcloud.yaml")) or {}
except Exception: sys.exit(0)
e = ((m.get("environments") or {}).get(sys.argv[1]) or {})
v = e.get(sys.argv[2])
if v is not None: print(v)
PY
}
# 로컬 kind 클러스터(yak-<name>) 존재 여부.
_dev_cluster_exists() {
local n="$1" rt kenv=""; rt="$(_dev_runtime)"; [ "$rt" = podman ] && kenv="KIND_EXPERIMENTAL_PROVIDER=podman"
env ${kenv:+$kenv} kind get clusters 2>/dev/null | grep -qx "yak-$n"
}
# project deploy = 개발(dev) 배포 = **로컬 kind**. environments.dev.cluster=로컬 이름(--name), .port=인그레스 포트.
# 로컬 클러스터가 없으면 dev up(생성+인그레스+에이전트) 후 dev deploy. 오프라인·무료·즉시(관리형/CI 미사용).
cmd_project_deploy() { cmd_project_deploy() {
local local_mode=0 ver="" local ver=""
for a in "$@"; do case "$a" in --local) local_mode=1 ;; *) [ -z "$ver" ] && ver="$a" ;; esac; done for a in "$@"; do case "$a" in --local|--ci) : ;; *) [ -z "$ver" ] && ver="$a" ;; esac; done
[ -f yakcloud.yaml ] || die "yakcloud.yaml 없음 — 'yakcloud project init' 먼저" [ -f yakcloud.yaml ] || die "yakcloud.yaml 없음 — 'yakcloud project init' 먼저"
if [ "$local_mode" = 1 ]; then cmd_project_deploy_local "$ver"; return; fi local devname devport; devname="$(_manifest_env dev cluster)"; devport="$(_manifest_env dev port)"
git rev-parse --git-dir >/dev/null 2>&1 || git init -q [ -n "$devname" ] || devname="dev"
git remote get-url origin >/dev/null 2>&1 \ # 포트 인자 — 이름이 dev 가 아니거나(반드시 비80) 명시 포트가 80이 아니면 up·deploy 양쪽에 동일하게 전달
|| die "git 원격(origin) 없음 — Gitea 레포 만들어 'git remote add origin <url>' + Secrets 설정 후 다시 (또는 'yakcloud project deploy --local')" # (deploy 에도 넘겨야 리포트 URL 이 host:port 로 맞음). 빈 배열은 bash 3.2 set -u 안전 확장.
git fetch -q --tags origin 2>/dev/null || true local portargs=()
if [ -z "$ver" ]; then if [ "$devname" != "dev" ]; then
local last; last="$(git tag -l 'v*' --sort=-v:refname 2>/dev/null | head -1)" [ -n "$devport" ] || devport=8080
if [ -n "$last" ]; then ver="$(printf '%s' "$last" | awk -F. 'BEGIN{OFS="."} {$NF=$NF+1; print}')"; else ver="v0.1.0"; fi portargs=(--port "$devport")
elif [ -n "$devport" ] && [ "$devport" != "80" ]; then
portargs=(--port "$devport")
fi fi
case "$ver" in v*) : ;; *) ver="v$ver" ;; esac info "개발(dev) = 로컬 kind '$devname'. 없으면 생성 후 배포 — 관리형/CI 미사용(콘솔엔 로컬 클러스터로 표시)."
_runner_preflight # 러너 없으면 경고(비차단) if ! _dev_cluster_exists "$devname"; then
info "배포 버전: $ver (커밋 + 태그 push)" info "로컬 클러스터 '$devname' 없음 → 생성(dev up)"
git add -A; git commit -q -m "deploy $ver" 2>/dev/null || true cmd_dev up --name "$devname" "${portargs[@]+"${portargs[@]}"}"
git push -q origin HEAD 2>/dev/null || true fi
git tag "$ver" 2>/dev/null || die "태그 $ver 이미 존재 — 'yakcloud project deploy <다음버전>'" cmd_dev deploy --name "$devname" "${portargs[@]+"${portargs[@]}"}" ${ver:+"$ver"}
git push -q origin "$ver" echo " 검증/운영으로 올리려면: yakcloud project promote --to val|prod (관리형 클러스터)"
echo "✓ $ver push — Gitea Actions 에서 빌드·배포. Actions 탭에서 결과 확인." }
echo " 실패/러너 없음이면: yakcloud project deploy --local (러너 없이 로컬 빌드/푸시/리컨실)"
# 워크로드 이미지 빌드 + 레지스트리 push(promote 용) — 이미 같은 태그가 레지스트리에 있으면 스킵(운영 재빌드 방지).
_build_and_push() {
local tag="$1" rt; rt="$(_dev_runtime)"; [ -n "$rt" ] || die "docker 또는 podman 필요(이미지 빌드/푸시)"
local reg="${GITEA_URL:-https://gitea.yakenator.io}"; reg="${reg#https://}"; reg="${reg#http://}"
if [ -n "${GITEA_USER:-}" ] && [ -n "${GITEA_TOKEN:-}" ]; then
printf '%s' "$GITEA_TOKEN" | "$rt" login "$reg" -u "$GITEA_USER" --password-stdin >/dev/null 2>&1 \
|| info "레지스트리 로그인 경고($reg) — push 실패 가능('yakcloud login')"
fi
RT="$rt" TAG="$tag" python3 - <<'PY'
import os, subprocess, yaml
rt=os.environ["RT"]; tag=os.environ["TAG"]; m=yaml.safe_load(open("yakcloud.yaml")) or {}
for w in m.get("workloads",[]) or []:
ctx=w.get("build")
if not ctx: continue
img=w["image"].replace("${TAG}",tag)
exists=subprocess.call([rt,"manifest","inspect",img],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)==0
if exists:
print("▸ skip(이미 레지스트리에 있음) %s"%img,flush=True); continue
print("▸ build %s: %s -> %s"%(w["name"],ctx,img),flush=True)
subprocess.check_call([rt,"build","-t",img,ctx]); subprocess.check_call([rt,"push",img])
PY
} }
# 러너 프리플라이트(best-effort) — origin 이 Gitea 면 레포 러너 유무 확인. 없으면 경고(비차단). # 러너 프리플라이트(best-effort) — origin 이 Gitea 면 레포 러너 유무 확인. 없으면 경고(비차단).
_runner_preflight() { # (구) --local / deploy-local — dev 는 이제 항상 로컬(kind)이라 별도 모드 불필요. 로컬 배포로 위임.
local url base owner_repo n
url="$(git remote get-url origin 2>/dev/null)" || return 0
base="${GITEA_URL:-https://gitea.yakenator.io}"
case "$url" in *"${base#https://}"*|*gitea.yakenator.io*) : ;; *) return 0 ;; esac
[ -n "${GITEA_TOKEN:-}" ] || return 0
owner_repo="$(printf '%s' "$url" | sed -E 's#^[a-z]+://[^@]*@?[^/]+/##; s#\.git$##')"
n="$(curl -fsS -H "Authorization: token $GITEA_TOKEN" "$base/api/v1/repos/$owner_repo/actions/runners" 2>/dev/null \
| python3 -c 'import sys,json
try:
d=json.load(sys.stdin); print(len(d if isinstance(d,list) else d.get("runners",[])))
except Exception: print(-1)' 2>/dev/null || echo -1)"
if [ "$n" = "0" ]; then
echo " ⚠ 이 레포에 등록된 Actions 러너가 없습니다 — 공유 러너가 라벨을 못 맞추면 태그 push 잡이 조용히 실패할 수 있습니다."
echo " 확실히 하려면: yakcloud project deploy --local"
fi
}
# CI 없이 로컬에서 build/push/리컨실 — 러너가 없거나 죽었을 때의 확실한 대안.
cmd_project_deploy_local() { cmd_project_deploy_local() {
: "${YAKCLOUD_URL:?YAKCLOUD_URL 필요 — 'yakcloud login'}"; : "${YAKCLOUD_TOKEN:?YAKCLOUD_TOKEN 필요 — 'yakcloud login'}" info "참고: 개발 배포는 이제 기본이 로컬 kind 입니다 — 'yakcloud project deploy' 와 동일(관리형은 'promote')."
command -v docker >/dev/null 2>&1 || die "docker 필요(--local 로컬 빌드/푸시)" cmd_project_deploy "$@"
ensure_pyyaml
local tag="${1:-}"; [ -z "$tag" ] && tag="dev-$(date +%Y%m%d-%H%M%S)"; case "$tag" in v*|dev-*) : ;; *) tag="v$tag" ;; esac
local reg="${GITEA_URL:-https://gitea.yakenator.io}"; reg="${reg#https://}"; reg="${reg#http://}"
if [ -n "${GITEA_USER:-}" ] && [ -n "${GITEA_TOKEN:-}" ]; then
printf '%s' "$GITEA_TOKEN" | docker login "$reg" -u "$GITEA_USER" --password-stdin >/dev/null 2>&1 \
|| info "docker login 경고($reg) — 레지스트리 push 실패 가능(yakcloud login 확인)"
else
info "Gitea 자격 없음 — 'yakcloud login' 후 --local 권장(레지스트리 push)"
fi
info "로컬 빌드/푸시 (tag=$tag)…"
TAG="$tag" python3 - <<'PY'
import os, subprocess, yaml
tag = os.environ["TAG"]; m = yaml.safe_load(open("yakcloud.yaml"))
for w in m.get("workloads", []) or []:
ctx = w.get("build")
if not ctx:
continue
img = w["image"].replace("${TAG}", tag)
print("▸ build %s: %s -> %s" % (w["name"], ctx, img), flush=True)
subprocess.check_call(["docker", "build", "-t", img, ctx])
subprocess.check_call(["docker", "push", img])
PY
info "리컨실 배포(콘솔 API)…"
TAG="$tag" python3 "$(_engine deploy)" yakcloud.yaml
echo "✓ 로컬 배포 완료(tag=$tag) — CI 없이 직접 리컨실."
} }
# dry-run — dev 는 로컬(kind)이라 안내만, val/prod 는 관리형 리컨실 계획.
cmd_project_check() { cmd_project_check() {
[ -f yakcloud.yaml ] || die "'yakcloud project init' 먼저" [ -f yakcloud.yaml ] || die "'yakcloud project init' 먼저"
: "${YAKCLOUD_URL:?YAKCLOUD_URL 필요}"; : "${YAKCLOUD_TOKEN:?YAKCLOUD_TOKEN 필요}"
ensure_pyyaml ensure_pyyaml
local env=""; for a in "$@"; do case "$a" in --env=*) env="${a#--env=}" ;; --prod) env=prod ;; esac; done local env=""; for a in "$@"; do case "$a" in --env=*) env="${a#--env=}" ;; --prod) env=prod ;; esac; done
YAKCLOUD_ENV="${env:-dev}" python3 "$(_engine deploy)" yakcloud.yaml --dry-run env="${env:-dev}"
if [ "$env" = dev ]; then
local dn; dn="$(_manifest_env dev cluster)"; [ -n "$dn" ] || dn="dev"
echo "dev = 로컬 kind '$dn' — 'yakcloud project deploy' 로 오프라인 로컬 배포(관리형/CI 미사용)."
echo " requires/workloads 계획은 로컬 리컨실이 그대로 적용합니다."
return
fi
: "${YAKCLOUD_URL:?YAKCLOUD_URL 필요}"; : "${YAKCLOUD_TOKEN:?YAKCLOUD_TOKEN 필요}"
YAKCLOUD_ENV="$env" python3 "$(_engine deploy)" yakcloud.yaml --dry-run
} }
# 운영 승격 — 개발에서 검증한 그 태그의 이미지를 **재빌드 없이** 운영 클러스터로 리컨실 + 운영 도메인 부착. # 승격 — 개발(로컬)에서 검증한 것을 **관리형** 검증/운영 클러스터로. 로컬 이미지는 레지스트리에 없으므로
# 태그 이미지를 build+push(이미 있으면 스킵=운영 재빌드 방지) 후 리컨실 + 도메인 부착.
cmd_project_promote() { cmd_project_promote() {
[ -f yakcloud.yaml ] || die "'yakcloud project init' 먼저" [ -f yakcloud.yaml ] || die "'yakcloud project init' 먼저"
: "${YAKCLOUD_URL:?YAKCLOUD_URL 필요 — 'yakcloud login'}"; : "${YAKCLOUD_TOKEN:?YAKCLOUD_TOKEN 필요 — 'yakcloud login'}" : "${YAKCLOUD_URL:?YAKCLOUD_URL 필요 — 'yakcloud login'}"; : "${YAKCLOUD_TOKEN:?YAKCLOUD_TOKEN 필요 — 'yakcloud login'}"
@ -324,13 +340,15 @@ cmd_project_promote() {
*) [ -z "$tag" ] && tag="$1"; shift ;; *) [ -z "$tag" ] && tag="$1"; shift ;;
esac esac
done done
case "$env" in val|prod) : ;; *) die "promote 대상은 val 또는 prod (관리형). dev 는 'project deploy'(로컬)." ;; esac
if [ -z "$tag" ]; then if [ -z "$tag" ]; then
git fetch -q --tags origin 2>/dev/null || true git fetch -q --tags origin 2>/dev/null || true
tag="$(git tag -l 'v*' --sort=-v:refname 2>/dev/null | head -1)" tag="$(git tag -l 'v*' --sort=-v:refname 2>/dev/null | head -1)"
[ -n "$tag" ] || tag="v0.1.0"
fi fi
[ -n "$tag" ] || die "승격할 태그가 없습니다 — 'yakcloud project promote vX.Y.Z' (개발 배포된 태그)"
case "$tag" in v*) : ;; *) tag="v$tag" ;; esac case "$tag" in v*) : ;; *) tag="v$tag" ;; esac
info "'$env' 승격: $tag → '$env' 환경(재빌드 없이 개발에서 빌드된 그 이미지를 리컨실)" info "'$env' 승격: $tag → 관리형 '$env' 클러스터(이미지 build+push 후 리컨실; 이미 있으면 재빌드 없이 그 이미지)"
_build_and_push "$tag"
YAKCLOUD_ENV="$env" TAG="$tag" python3 "$(_engine deploy)" yakcloud.yaml YAKCLOUD_ENV="$env" TAG="$tag" python3 "$(_engine deploy)" yakcloud.yaml
echo "✓ 승격 완료: $tag → '$env' 클러스터 + 도메인(기본 도메인은 자동)" echo "✓ 승격 완료: $tag → '$env' 클러스터 + 도메인(기본 도메인은 자동)"
} }
@ -669,9 +687,8 @@ yakcloud $VERSION — YakCloud 프로젝트 CLI (개발·검증·운영 모두
dev status | dev clean 로컬 배포 상태 / 이 프로젝트 리소스 삭제 dev status | dev clean 로컬 배포 상태 / 이 프로젝트 리소스 삭제
dev down 로컬 kind 클러스터 삭제 dev down 로컬 kind 클러스터 삭제
project init [name] 빈 폴더에 스타터 스캐폴딩(대화형 마법사) + 자격 프롬프트 project init [name] 빈 폴더에 스타터 스캐폴딩(대화형 마법사) + 자격 프롬프트
project deploy [vX.Y.Z] 커밋 + 태그 push → CI 빌드 → **개발 클러스터** 배포(버전 생략=자동 버전업) project deploy [tag] **개발 배포 = 로컬 kind**(environments.dev). 로컬 클러스터 없으면 자동 생성 후 배포(오프라인·무료·CI 불필요)
project deploy --local 러너 없이 로컬 build/push/리컨실(CI 대안, 개발 클러스터) project promote [vX.Y.Z] --to val|prod **관리형** 검증/운영으로 승격 — 이미지 build+push(이미 있으면 재빌드 없이) 후 리컨실. 기본 --to prod
project promote [vX.Y.Z] [--to val|prod] 그 이미지를 재빌드 없이 상위 환경으로 승격(개발→검증→운영). 기본 --to prod
project info 프로젝트 개괄(매니페스트 + 라이브 상태) project info 프로젝트 개괄(매니페스트 + 라이브 상태)
project check [--env dev|val|prod] dry-run(직접 API 계획만) project check [--env dev|val|prod] dry-run(직접 API 계획만)
project update 이 프로젝트의 엔진·CI·스킬만 최신화(앱·매니페스트 보존) project update 이 프로젝트의 엔진·CI·스킬만 최신화(앱·매니페스트 보존)

View File

@ -6,16 +6,15 @@
apiVersion: yakcloud/v1 apiVersion: yakcloud/v1
project: my-app # 프로젝트 이름(이미지 경로/표시에 사용) — 소문자·숫자·하이픈 project: my-app # 프로젝트 이름(이미지 경로/표시에 사용) — 소문자·숫자·하이픈
# 환경 — 개발 → 검증 → 운영 3단계. 각 환경 = 별도 클러스터. 검증계(val)가 필요 없으면 그 줄을 지워도 됩니다. # 환경 — 개발(로컬 kind) → 검증/운영(관리형). 검증계(val)가 필요 없으면 그 줄을 지워도 됩니다.
# 로컬 개발(kind) → yakcloud dev up (최초 1회) → yakcloud dev deploy — 콘솔/CI 불필요, 오프라인 반복. # dev = **로컬 kind**. yakcloud project deploy → 로컬 클러스터(없으면 자동 생성) + dev deploy(오프라인·무료, CI 불필요).
# 로컬에서 requires 소스+workloads 가 관리형과 '동일한 바인딩 env'로 뜬다(직선 파리티). # 로컬에서 requires 소스+workloads 가 관리형과 '동일한 바인딩 env'로 뜬다(직선 파리티). cluster=로컬 이름, port=인그레스 포트.
# deploy → dev (태그 push → CI 빌드 → 개발 클러스터, 기본 도메인으로 확인) # val/prod = **관리형**. yakcloud project promote --to val|prod → 이미지 build+push(이미 있으면 재빌드 없이) 후 리컨실 + 도메인.
# promote --to val→ 그 이미지를 재빌드 없이 검증 클러스터로 # (기본 도메인 c-xxxx.yakenator.io 은 자동. 운영 커스텀 도메인은 prod.domains 에.)
# promote --to prod→ 검증 통과분을 운영 클러스터로 + 운영 도메인(기본 도메인 c-xxxx.yakenator.io 은 자동)
environments: environments:
dev: { cluster: my-dev-cluster } # deploy 대상(기본) dev: { cluster: dev, port: 80 } # 로컬 kind (project deploy). cluster=로컬 이름, port=인그레스 포트(이름!=dev면 비80)
val: { cluster: my-val-cluster } # promote --to val val: { cluster: my-val-cluster } # 관리형 (promote --to val)
prod: { cluster: my-prod-cluster, domains: [] } # promote --to prod (domains 예: [app.example.com]) prod: { cluster: my-prod-cluster, domains: [] } # 관리형 (promote --to prod, domains 예: [app.example.com])
# 필요한 데이터 소스(논리 이름). 없으면 [] 로. 기본 = **공유(shared)** — 이미 있는 공유 소스에 연결 + 격리 DB 민팅. # 필요한 데이터 소스(논리 이름). 없으면 [] 로. 기본 = **공유(shared)** — 이미 있는 공유 소스에 연결 + 격리 DB 민팅.
# type ∈ postgresql·mysql·mariadb·mongodb·redis·minio·rabbitmq·solr·oracle # type ∈ postgresql·mysql·mariadb·mongodb·redis·minio·rabbitmq·solr·oracle