feat(dev): 릴레이 에이전트를 클러스터 단위로 — dev up 에서 기동(배포 전 노드 모니터링·연결)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 20:41:59 +09:00
parent da1911a036
commit a0352d02c0
3 changed files with 66 additions and 38 deletions

View File

@ -405,18 +405,28 @@ def _agent_dir() -> str:
return d
def _agent_pidfile(ns: str) -> str:
# 클러스터별 구분 — 서로 다른 dev 클러스터가 같은 ns(프로젝트)를 가져도 pidfile 이 안 겹치게.
return os.path.join(_agent_dir(), f"{DEV_NAME}-{ns}.pid")
def _agent_pidfile() -> str:
# 클러스터 단위 에이전트(프로젝트 무관) — 여러 dev 클러스터는 DEV_NAME 으로 구분.
return os.path.join(_agent_dir(), f"{DEV_NAME}.pid")
def _stop_agent(ns: str) -> None:
pf = _agent_pidfile(ns)
def _agent_alive() -> bool:
pf = _agent_pidfile()
if not os.path.exists(pf):
return False
try:
os.kill(int(open(pf).read().strip()), 0) # 신호 0 = 존재 확인
return True
except (OSError, ValueError):
return False
def _stop_agent() -> None:
pf = _agent_pidfile()
if not os.path.exists(pf):
return
try:
pid = int(open(pf).read().strip())
os.kill(pid, signal.SIGTERM) # 그룹장(start_new_session) → 자식까지
os.kill(int(open(pf).read().strip()), signal.SIGTERM)
except (OSError, ValueError):
pass
try:
@ -425,15 +435,18 @@ def _stop_agent(ns: str) -> None:
pass
def _ensure_agent(project: str, ns: str, reqs: list) -> None:
"""콘솔에서 ds-token 취득 → 릴레이 에이전트 데몬 기동(콘솔에서 로컬 소스 라이브 관리). best-effort.
로그인(URL+TOKEN)+등록명(YAK_REG_NAME) 없거나 소스 없으면 스킵(오프라인 dev 불변식 — 스냅샷만)."""
def _ensure_agent() -> None:
"""콘솔에서 ds-token 취득 → **클러스터 단위** 릴레이 에이전트 데몬 기동. dev up/deploy 공통.
노드 모니터링·데이터·앱을 배포 전에도 라이브로. 이미 살아있으면 스킵(멱등).
로그인(URL+TOKEN)+등록명(YAK_REG_NAME) 없으면 조용히 스킵(오프라인 dev 불변식)."""
if _agent_alive():
return
url = os.environ.get("YAKCLOUD_URL")
tok = os.environ.get("YAKCLOUD_TOKEN")
name = os.environ.get("YAK_REG_NAME")
if not (url and tok and name and reqs):
if not (url and tok and name):
return
_stop_agent(ns) # 재기동(토큰/키 갱신) — 멱등
_stop_agent() # 죽은 pidfile 정리
try:
req = urllib.request.Request(url.rstrip("/") + f"/api/v1/clusters/local/{quote(name)}/agent-token",
data=b"{}", method="POST",
@ -442,7 +455,7 @@ def _ensure_agent(project: str, ns: str, reqs: list) -> None:
info = json.loads(r.read() or b"{}")
data = info.get("data") or info
except Exception: # noqa: BLE001
warn("릴레이 토큰 취득 실패 — 콘솔의 로컬 소스 라이브 관리 비활성(스냅샷만).")
warn("릴레이 토큰 취득 실패 — 콘솔 라이브 모니터링/관리 비활성(로그인/등록 확인).")
return
relay_key, ds_token, api_base = data.get("relayKey"), data.get("dsToken"), data.get("apiBase")
if not (relay_key and ds_token and api_base):
@ -451,12 +464,12 @@ def _ensure_agent(project: str, ns: str, reqs: list) -> None:
if not os.path.exists(agent_py):
return
env = dict(os.environ, YAK_RELAY_KEY=relay_key, YAK_DS_TOKEN=ds_token, YAK_API_BASE=api_base,
YAK_DEV_NS=ns, YAK_DEV_CONTEXT=CTX, YAK_DEV_RUNTIME=RUNTIME, YAK_DEV_NET=SHARED_NET)
logf = open(os.path.join(_agent_dir(), f"{ns}.log"), "a")
YAK_DEV_CONTEXT=CTX, YAK_DEV_RUNTIME=RUNTIME, YAK_DEV_NET=SHARED_NET)
logf = open(os.path.join(_agent_dir(), f"{DEV_NAME}.log"), "a")
p = subprocess.Popen([sys.executable, agent_py], env=env, stdout=logf, stderr=logf,
stdin=subprocess.DEVNULL, start_new_session=True)
open(_agent_pidfile(ns), "w").write(str(p.pid))
log(f"릴레이 에이전트 기동(pid {p.pid}) — 콘솔에서 로컬 소스를 원격처럼 관리(자격은 노트북 밖으로 안 나감)")
open(_agent_pidfile(), "w").write(str(p.pid))
log(f"릴레이 에이전트 기동(pid {p.pid}) — 콘솔에서 노드·데이터·앱 라이브(자격은 노트북 밖으로 안 나감)")
def cmd_deploy(m: dict) -> None:
@ -492,7 +505,7 @@ def cmd_deploy(m: dict) -> None:
_prune(ns, "workload", "app", {sanitize(w["name"]) for w in wls})
_report_to_console(project, reqs, wls) # 콘솔 LOCAL 클러스터 탭(데이터소스·앱) 리포트
_ensure_agent(project, ns, reqs) # 릴레이 에이전트 기동 — 콘솔에서 로컬 소스 라이브 관리(원격 파리티)
_ensure_agent() # 클러스터 단위 릴레이 에이전트 보장(dev up 에서 이미 떴으면 스킵)
failed = [h for h, ok in results if not ok]
print()
@ -523,15 +536,18 @@ def cmd_down(m: dict) -> None:
"""네임스페이스(이 프로젝트의 dev 리소스)만 정리 — kind 클러스터 자체는 'yakcloud dev down' 이 지운다."""
context_ok()
ns = sanitize(m.get("project", "app"))
_stop_agent(ns) # 릴레이 에이전트 데몬 중지
_stop_agent() # 클러스터 릴레이 에이전트 데몬 중지
log(f"네임스페이스 '{ns}' 삭제(로컬 dev 리소스 정리)")
sh(["kubectl", "--context", CTX, "delete", "namespace", ns, "--ignore-not-found"], capture=False, check=False)
def main() -> None:
if len(sys.argv) < 2 or sys.argv[1] not in ("deploy", "status", "down"):
die("사용: yakcloud_dev.py <deploy|status|down> [manifest.yaml]")
if len(sys.argv) < 2 or sys.argv[1] not in ("up", "deploy", "status", "down"):
die("사용: yakcloud_dev.py <up|deploy|status|down> [manifest.yaml]")
cmd = sys.argv[1]
if cmd == "up": # 클러스터 단위 릴레이 에이전트만 기동(매니페스트 불필요) — dev up 에서 호출
_ensure_agent()
return
path = next((a for a in sys.argv[2:] if not a.startswith("--")), "yakcloud.yaml")
if not os.path.exists(path):
die(f"매니페스트 없음: {path}'yakcloud project init' 먼저")