cli v0.11.0: 로컬 dev/migrate 제거, 개발/운영 클러스터 승격 모델. environments.{dev,prod} 매니페스트; project deploy=개발 클러스터, project promote=재빌드 없이 운영 클러스터+운영 도메인. CI=dev 환경. dev/prod dry-run 검증

This commit is contained in:
2026-08-28 05:07:14 +09:00
parent 9353cd7706
commit dd6cf8a880
11 changed files with 98 additions and 1441 deletions

View File

@ -31,7 +31,9 @@ URL = os.environ["YAKCLOUD_URL"].rstrip("/")
TOKEN = os.environ["YAKCLOUD_TOKEN"]
CLUSTER_REF = os.environ.get("YAKCLOUD_CLUSTER")
CLUSTER = "" # main 에서 이름→id 로 해석해 채운다
CLUSTER_HOST = "" # 클러스터 기본 도메인 — expose.host(s) 미지정 시 여기로 노출
CLUSTER_HOST = "" # 클러스터 기본 도메인 — 항상 노출(운영은 여기 + 운영 도메인)
ENV = os.environ.get("YAKCLOUD_ENV", "dev") # 대상 환경 environments.<env> (기본 dev). promote=prod
ENV_DOMAINS: list = [] # main 에서 environments.<env>.domains 로 채움(운영 도메인)
TAG = os.environ.get("TAG", "latest")
DRY = "--dry-run" in sys.argv[1:]
@ -163,24 +165,25 @@ def deploy_workload(w: dict) -> tuple[str | None, bool]:
"healthPath": w.get("health"),
"path": ex.get("path", "/"), "pathType": "Prefix", "rewritePrefix": bool(ex.get("rewrite", False)),
}
# 노출 도메인: 여러 개(hosts) > 단일(host) > 클러스터 기본 도메인.
hosts = ex.get("hosts")
if hosts:
body["exposeHosts"] = hosts
# 노출 도메인 = 워크로드 지정 host(s) + 환경 도메인(운영). 커스텀이 있으면 기본 도메인도 함께 노출.
wl_hosts = ex.get("hosts") or ([ex["host"]] if ex.get("host") else [])
custom = list(dict.fromkeys(wl_hosts + ENV_DOMAINS))
if custom:
body["exposeHosts"] = list(dict.fromkeys(custom + ([CLUSTER_HOST] if CLUSTER_HOST else [])))
else:
body["exposeHost"] = ex.get("host") or (CLUSTER_HOST or None)
body["exposeHost"] = CLUSTER_HOST or None
# 환경변수(선언적): 리스트[{key,value,secret?}] 또는 맵{KEY: VALUE}.
env = w.get("env") or []
if isinstance(env, dict):
env = [{"key": k, "value": v} for k, v in env.items()]
body["env"] = [{"key": e["key"], "value": str(e.get("value", "")),
"secret": bool(e.get("secret", False))} for e in env]
# 도메인 리컨실 — expose 에 명시한 host(들)를 레지스트리에 등록(없으면). 배포 하나로 '도달 가능한 앱'이 되게.
for h in (ex.get("hosts") or ([ex["host"]] if ex.get("host") else [])):
# 도메인 리컨실 — 워크로드 host(들) + 환경(운영) 도메인을 레지스트리에 등록(없으면).
for h in custom:
reconcile_domain(h)
if DRY:
log(f"deploy '{w['name']}' 예정: image={image} port={body['port']} path={body['path']} "
f"replicas={body['replicasDesired']} hosts={hosts or body.get('exposeHost')}")
f"replicas={body['replicasDesired']} hosts={body.get('exposeHosts') or body.get('exposeHost')}")
return None, False
# 멱등: 기존 배포면 PATCH(이미지 갱신 → kubectl apply = 무중단 롤링). 없으면 신규 생성.
existing = next((d for d in cluster_deployments() if d.get("name") == w["name"]), None)
@ -202,15 +205,22 @@ def bind(dep_id: str | None, alias: str, service_id: str | None, source: str) ->
def main() -> None:
global CLUSTER, CLUSTER_HOST
global CLUSTER, CLUSTER_HOST, ENV_DOMAINS
path = next((a for a in sys.argv[1:] if not a.startswith("--")), "yakcloud.yaml")
m = yaml.safe_load(open(path))
ref = CLUSTER_REF or m.get("cluster")
# 환경 해석: environments.<ENV>.{cluster,domains}. 하위호환: 없으면 top-level cluster:.
envs = m.get("environments") or {}
env_cfg = envs.get(ENV) or {}
ref = CLUSTER_REF or env_cfg.get("cluster") or m.get("cluster")
if not ref:
raise SystemExit("대상 클러스터 미지정 — YAKCLOUD_CLUSTER 환경변수 또는 매니페스트 cluster: 필드")
raise SystemExit(
f"환경 '{ENV}' 대상 클러스터 미지정 — 매니페스트 environments.{ENV}.cluster (또는 YAKCLOUD_CLUSTER). "
f"등록된 환경: {', '.join(envs) or '(없음)'}")
ENV_DOMAINS = env_cfg.get("domains") or []
CLUSTER, cname = resolve_cluster(ref)
CLUSTER_HOST = (unwrap(api("GET", f"/clusters/{CLUSTER}")) or {}).get("defaultHostname") or ""
log(f"project '{m.get('project')}' → cluster '{cname}' ({CLUSTER}) (tag {TAG}){' [DRY-RUN]' if DRY else ''}")
dom = f" +도메인 {ENV_DOMAINS}" if ENV_DOMAINS else ""
log(f"[{ENV}] project '{m.get('project')}' → cluster '{cname}' ({CLUSTER}){dom} (tag {TAG}){' [DRY-RUN]' if DRY else ''}")
src_ids: dict[str, str | None] = {}
for req in m.get("requires", []):
src_ids[req["name"]] = reconcile_source(req)