diff --git a/.yakcloud/deploy.py b/.yakcloud/deploy.py index 65b3ce1..d6c7bd9 100644 --- a/.yakcloud/deploy.py +++ b/.yakcloud/deploy.py @@ -220,6 +220,48 @@ def bind(dep_id: str | None, alias: str, service_id: str | None, source: str) -> log(f"bind '{alias}' → '{source}' 완료") +# ── 환경 오버레이: base(requires/workloads) + environments. 이름 매칭 병합 ───────────────── +# 앱 binds 는 논리이름 고정 → 환경별로 소스 정체(type/mode/plan)·워크로드 필드만 덮어쓴다. promote 무편집. +# ※ dev.py 의 동일 함수와 규약 일치(파리티) — 한쪽 바꾸면 다른 쪽도. +def _merge_env_vars(base, ov): + def norm(e): + if isinstance(e, dict): + return [{"key": k, "value": v} for k, v in e.items()] + return [dict(x) for x in (e or [])] + out = {x["key"]: x for x in norm(base)} + for x in norm(ov): + out[x["key"]] = x + return list(out.values()) + + +def resolve_env(m: dict, env: str) -> tuple[list, list]: + ec = (m.get("environments") or {}).get(env) or {} + by: dict = {} + order: list = [] + for r in (m.get("requires") or []): + by[r["name"]] = dict(r); order.append(r["name"]) + for r in (ec.get("requires") or []): + if r["name"] in by: + by[r["name"]].update(r) # 필드 병합(type/mode/plan 덮어쓰기) + else: + by[r["name"]] = dict(r); order.append(r["name"]) + requires = [by[n] for n in order] + wov = ec.get("workloads") or {} # {워크로드명: 패치} + workloads = [] + for w in (m.get("workloads") or []): + w2 = dict(w) + ov = wov.get(w["name"]) or {} + for k, v in ov.items(): + if k == "env": + w2["env"] = _merge_env_vars(w.get("env"), v) + elif k in ("resources", "expose") and isinstance(v, dict): + merged = dict(w.get(k) or {}); merged.update(v); w2[k] = merged + else: + w2[k] = v # replicas/image/port/binds 등 교체 + workloads.append(w2) + return requires, workloads + + def main() -> None: global CLUSTER, CLUSTER_HOST, ENV_DOMAINS path = next((a for a in sys.argv[1:] if not a.startswith("--")), "yakcloud.yaml") @@ -238,10 +280,11 @@ def main() -> None: CLUSTER_HOST = (unwrap(api("GET", f"/clusters/{CLUSTER}")) or {}).get("defaultHostname") or "" 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 ''}") + requires, workloads = resolve_env(m, ENV) # base + environments. 오버레이 병합 src_ids: dict[str, str | None] = {} - for req in m.get("requires", []): + for req in requires: src_ids[req["name"]] = reconcile_source(req) - for w in m.get("workloads", []): + for w in workloads: dep_id, created = deploy_workload(w) if DRY or created: # 기존 배포는 PATCH(롤링)로 바인딩 유지 → 신규일 때만 바인딩 for b in w.get("binds", []): diff --git a/.yakcloud/dev.py b/.yakcloud/dev.py index bcdb11f..571c526 100644 --- a/.yakcloud/dev.py +++ b/.yakcloud/dev.py @@ -629,12 +629,51 @@ def _ensure_agent() -> None: log(f"릴레이 에이전트 기동(pid {p.pid}) — 콘솔에서 노드·데이터·앱 라이브(자격은 노트북 밖으로 안 나감)") +# ── 환경 오버레이: base(requires/workloads) + environments. 이름 매칭 병합 ───────────────── +# 앱 binds 는 논리이름 고정 → 환경별로 소스 정체(type/mode/plan)·워크로드 필드만. 로컬은 항상 env="dev". +# ※ deploy.py 의 동일 함수와 규약 일치(파리티). +def _merge_env_vars(base, ov): + def norm(e): + if isinstance(e, dict): + return [{"key": k, "value": v} for k, v in e.items()] + return [dict(x) for x in (e or [])] + out = {x["key"]: x for x in norm(base)} + for x in norm(ov): + out[x["key"]] = x + return list(out.values()) + + +def resolve_env(m: dict, env: str): + ec = (m.get("environments") or {}).get(env) or {} + by, order = {}, [] + for r in (m.get("requires") or []): + by[r["name"]] = dict(r); order.append(r["name"]) + for r in (ec.get("requires") or []): + if r["name"] in by: + by[r["name"]].update(r) + else: + by[r["name"]] = dict(r); order.append(r["name"]) + requires = [by[n] for n in order] + wov = ec.get("workloads") or {} + workloads = [] + for w in (m.get("workloads") or []): + w2 = dict(w) + for k, v in (wov.get(w["name"]) or {}).items(): + if k == "env": + w2["env"] = _merge_env_vars(w.get("env"), v) + elif k in ("resources", "expose") and isinstance(v, dict): + merged = dict(w.get(k) or {}); merged.update(v); w2[k] = merged + else: + w2[k] = v + workloads.append(w2) + return requires, workloads + + def cmd_deploy(m: dict) -> None: context_ok() project = sanitize(m.get("project", "app")) ns = project - reqs = m.get("requires", []) or [] - wls = m.get("workloads", []) or [] + reqs, wls = resolve_env(m, "dev") # base + environments.dev 오버레이(로컬은 항상 dev 환경) # 충돌 가드 — 서로 다른 소스명/워크로드명이 같은 서비스명으로 뭉개지면 자격 오배선 → 거부. svc_owner: dict[str, str] = {} diff --git a/bin/yakcloud b/bin/yakcloud index 1c413c9..deb9e98 100755 --- a/bin/yakcloud +++ b/bin/yakcloud @@ -16,7 +16,7 @@ set -uo pipefail # -e 미사용: 'test && action' 관용구가 값 없을 때 REPO="${YAKCLOUD_STARTER_REPO:-https://gitea.yakenator.io/yakenator/yakcloud-starter}" BRANCH="${YAKCLOUD_STARTER_BRANCH:-main}" -VERSION="0.23.0" +VERSION="0.24.0" # 로컬 개발 클러스터(kind) — 개발 후 운영까지 '직선 배포'의 dev 구간. DEV_CTX="${YAK_DEV_CONTEXT:-kind-yak-dev}" DEV_CLUSTER="${YAK_DEV_CLUSTER:-yak-dev}" diff --git a/yakcloud.yaml b/yakcloud.yaml index 94be62c..e55a3a8 100644 --- a/yakcloud.yaml +++ b/yakcloud.yaml @@ -11,10 +11,21 @@ project: my-app # 프로젝트 이름(이미지 경로/표 # 로컬에서 requires 소스+workloads 가 관리형과 '동일한 바인딩 env'로 뜬다(직선 파리티). cluster=로컬 이름, port=인그레스 포트. # val/prod = **관리형**. yakcloud project promote --to val|prod → 이미지 build+push(이미 있으면 재빌드 없이) 후 리컨실 + 도메인. # (기본 도메인 c-xxxx.yakenator.io 은 자동. 운영 커스텀 도메인은 prod.domains 에.) +# +# ★ 환경별 오버라이드(오버레이 병합): 아래 base 의 requires/workloads 를 environments. 에서 '이름 매칭'으로 +# 덮어쓰거나 추가할 수 있다 → dev↔prod 소스/리소스가 달라도 promote 때 매니페스트 수정 불필요(앱 binds 는 논리이름 고정). +# 예) prod 는 관리형 공유 mongo + 큰 리소스: +# prod: +# cluster: my-prod-cluster +# domains: [app.example.com] +# requires: # 이름(appdb) 매칭 → base 의 정체를 덮어씀 +# - { name: appdb, type: mongodb, mode: shared, plan: medium } +# workloads: # 워크로드명(web) 키로 필드 패치(resources·replicas·env 는 병합, 그 외 교체) +# web: { replicas: 3, resources: { cpu: 500m, mem: 512Mi } } environments: dev: { cluster: dev, port: 80, workers: 1 } # 로컬 kind (project deploy). cluster=로컬 이름, port=인그레스 포트(이름!=dev면 비80), workers=워커 노드 수(0=CP만) 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]) — 필요 시 위 예처럼 requires/workloads 오버라이드 # 필요한 데이터 소스(논리 이름). 없으면 [] 로. 기본 = **공유(shared)** — 이미 있는 공유 소스에 연결 + 격리 DB 민팅. # type ∈ postgresql·mysql·mariadb·mongodb·redis·minio·rabbitmq·solr·oracle