feat(dev): 다중 로컬 dev 클러스터(--name/--port) + 노드 리소스 사용현황 릴레이

`yakcloud dev up --name dev2 --port 8081` 로 머신에 여러 kind dev 클러스터(공유 소스 컨테이너 재사용).
agent 가 노드 CPU/MEM/DISK(예약·실측)를 릴레이 → 콘솔이 원격과 동일한 노드 리소스 카드로 표시.
dev up 이 metrics-server 자동 설치(실측용). 리뷰 반영(pidfile 정리·포트 범위·phase·디스크).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 19:33:45 +09:00
parent 761666aade
commit da1911a036
3 changed files with 190 additions and 22 deletions

View File

@ -411,6 +411,28 @@ _dev_runtime() {
echo ""
}
# 여러 로컬 dev 클러스터 지원 — --name/--port 를 파싱해 클러스터명·컨텍스트·등록명·인그레스 포트를 파생.
# 기본(--name 없음) = dev → kind 'yak-dev', ctx 'kind-yak-dev', 등록명 <host>, 인그레스 80/443
# --name dev2 --port N → kind 'yak-dev2', ctx 'kind-yak-dev2', 등록명 <host>-dev2, 인그레스 N (80은 기본 클러스터 몫)
# 나머지 인자(태그 등)는 DEV_ARGS 배열에 남긴다(--name 값이 태그로 오인되지 않게 여기서 소비).
_dev_resolve() {
DEV_NAME="dev"; DEV_HTTP_PORT="80"; DEV_PORT_EXPLICIT=0; DEV_ARGS=()
while [ "$#" -gt 0 ]; do
case "$1" in
--name|--cluster) DEV_NAME="${2:-}"; shift 2 || shift ;;
--name=*) DEV_NAME="${1#--name=}"; shift ;;
--cluster=*) DEV_NAME="${1#--cluster=}"; shift ;;
--port) DEV_HTTP_PORT="${2:-}"; DEV_PORT_EXPLICIT=1; shift 2 || shift ;;
--port=*) DEV_HTTP_PORT="${1#--port=}"; DEV_PORT_EXPLICIT=1; shift ;;
*) DEV_ARGS+=("$1"); shift ;;
esac
done
DEV_NAME="$(printf '%s' "$DEV_NAME" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9-' '-' | sed 's/^-*//; s/-*$//')"
[ -z "$DEV_NAME" ] && DEV_NAME="dev"
DEV_CLUSTER="${YAK_DEV_CLUSTER:-yak-$DEV_NAME}"
DEV_CTX="kind-$DEV_CLUSTER" # kind 는 항상 컨텍스트를 'kind-<클러스터명>' 으로 만든다
}
cmd_dev_up() {
command -v kind >/dev/null 2>&1 || die "kind 필요 — 'brew install kind' (https://kind.sigs.k8s.io)"
command -v kubectl >/dev/null 2>&1 || die "kubectl 필요"
@ -419,8 +441,19 @@ cmd_dev_up() {
if env $kenv kind get clusters 2>/dev/null | grep -qx "$DEV_CLUSTER"; then
info "kind 클러스터 '$DEV_CLUSTER' 이미 존재 → 재사용 ($rt)"
else
info "kind 클러스터 '$DEV_CLUSTER' 생성 ($rt) — 인그레스 80/443 host 매핑…"
env $kenv kind create cluster --name "$DEV_CLUSTER" --config - <<'EOF' || die "kind 생성 실패"
# 이름 붙은(비기본) 클러스터는 80 이 기본 클러스터 몫 → --port 필수. 기본(dev)은 80/443.
if [ "$DEV_NAME" != "dev" ] && [ "${DEV_PORT_EXPLICIT:-0}" = 0 ]; then
die "이름 붙은 dev 클러스터('$DEV_NAME')는 인그레스 포트가 필요합니다 — 예: yakcloud dev up --name $DEV_NAME --port 8081"
fi
case "$DEV_HTTP_PORT" in ''|*[!0-9]*) die "포트는 숫자여야 합니다: '$DEV_HTTP_PORT'";; esac
if [ "$DEV_HTTP_PORT" -lt 1 ] || [ "$DEV_HTTP_PORT" -gt 65535 ]; then
die "포트는 1-65535 범위여야 합니다: '$DEV_HTTP_PORT'"
fi
info "kind 클러스터 '$DEV_CLUSTER' 생성 ($rt) — 인그레스 http 포트 ${DEV_HTTP_PORT}…"
local ports=" - { containerPort: 80, hostPort: ${DEV_HTTP_PORT}, protocol: TCP }"
[ "$DEV_HTTP_PORT" = "80" ] && ports="$ports
- { containerPort: 443, hostPort: 443, protocol: TCP }"
env $kenv kind create cluster --name "$DEV_CLUSTER" --config - <<EOF || die "kind 생성 실패(포트 ${DEV_HTTP_PORT} 가 이미 사용 중일 수 있음)"
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
@ -432,8 +465,7 @@ nodes:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- { containerPort: 80, hostPort: 80, protocol: TCP }
- { containerPort: 443, hostPort: 443, protocol: TCP }
${ports}
EOF
fi
info "ingress-nginx 설치/확인…"
@ -443,6 +475,12 @@ EOF
sleep 3
kubectl --context "$DEV_CTX" -n ingress-nginx rollout status deploy/ingress-nginx-controller --timeout=180s >/dev/null 2>&1 \
|| warn_line "ingress 컨트롤러 준비 지연 — 잠시 후 'yakcloud dev deploy'."
# metrics-server(실측 노드/파드 CPU·메모리) — 콘솔 '노드 리소스 사용현황'의 실측 바용. kind 는 kubelet TLS 자체서명 → --kubelet-insecure-tls.
info "metrics-server 설치/확인(노드 리소스 실측)…"
kubectl --context "$DEV_CTX" apply -f "${YAK_DEV_METRICS:-https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml}" >/dev/null 2>&1 \
&& kubectl --context "$DEV_CTX" -n kube-system patch deployment metrics-server --type=json \
-p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]' >/dev/null 2>&1 \
|| warn_line "metrics-server 설치 건너뜀(무시 가능 — 실측 대신 예약값만 표시)."
echo "✓ 로컬 dev 클러스터 준비 ($DEV_CTX). 다음: yakcloud dev deploy"
_dev_register
}
@ -451,7 +489,10 @@ warn_line() { echo " ⚠ $*" >&2; }
# 콘솔 클러스터 목록에 이 로컬 클러스터를 메타 등록 — 머신 호스트명 기준(머신당 1개, 프로젝트는 네임스페이스).
_local_reg_name() {
local h; h="$(hostname -s 2>/dev/null | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9-' '-' | sed 's/^-*//; s/-*$//')"
echo "${h:-local-dev}"
h="${h:-local-dev}"
# 여러 dev 클러스터 구분 — 기본(dev)은 <host>, 이름 붙은 클러스터는 <host>-<name>(콘솔에 구분 표시).
[ -n "${DEV_NAME:-}" ] && [ "$DEV_NAME" != "dev" ] && h="$h-$DEV_NAME"
echo "$h"
}
# best-effort 등록/heartbeat — 로그인(URL+토큰) 없으면 조용히 스킵(오프라인 dev 불변식 유지).
_dev_register() {
@ -459,7 +500,7 @@ _dev_register() {
local name; name="$(_local_reg_name)"
curl -fsS -m 6 -X POST "${YAKCLOUD_URL%/}/api/v1/clusters/local" \
-H "Authorization: Bearer $YAKCLOUD_TOKEN" -H 'content-type: application/json' \
-d "{\"name\":\"$name\",\"displayName\":\"로컬 개발 (kind)\"}" >/dev/null 2>&1 \
-d "{\"name\":\"$name\",\"displayName\":\"$name\"}" >/dev/null 2>&1 \
&& info "콘솔 클러스터 목록에 로컬 '$name' 등록/갱신" || true
return 0
}
@ -474,16 +515,17 @@ _dev_deregister() {
cmd_dev() {
local sub="${1:-}"; [ "$#" -gt 0 ] && shift
_dev_resolve "$@" # --name/--port 파싱 → DEV_NAME/DEV_CLUSTER/DEV_CTX/DEV_HTTP_PORT/DEV_ARGS
case "$sub" in
up) cmd_dev_up "$@" ;;
up) cmd_dev_up ;;
deploy|status|clean)
[ -f yakcloud.yaml ] || die "프로젝트 폴더가 아닙니다 — 'yakcloud project init' 먼저"
[ -f yakcloud.yaml ] || die "yakcloud.yaml 없음"
ensure_pyyaml
local rt tag=""; rt="$(_dev_runtime)"
for a in "$@"; do case "$a" in -*) : ;; *) [ -z "$tag" ] && tag="$a" ;; esac; done
for a in "${DEV_ARGS[@]+"${DEV_ARGS[@]}"}"; do case "$a" in -*) : ;; *) [ -z "$tag" ] && tag="$a" ;; esac; done
local pysub="$sub"; [ "$sub" = clean ] && pysub="down"
YAK_DEV_CONTEXT="$DEV_CTX" YAK_DEV_CLUSTER="$DEV_CLUSTER" YAK_DEV_RUNTIME="${rt:-docker}" \
YAK_DEV_CONTEXT="$DEV_CTX" YAK_DEV_CLUSTER="$DEV_CLUSTER" YAK_DEV_NAME="$DEV_NAME" \
YAK_DEV_RUNTIME="${rt:-docker}" YAK_DEV_HTTP_PORT="$DEV_HTTP_PORT" \
YAK_REG_NAME="$(_local_reg_name)" TAG="${tag:-${TAG:-dev}}" python3 "$(_engine dev)" "$pysub" yakcloud.yaml
[ "$sub" = deploy ] && _dev_register || true
;;
@ -492,23 +534,35 @@ cmd_dev() {
local rt kenv=""; rt="$(_dev_runtime)"; [ "$rt" = podman ] && kenv="KIND_EXPERIMENTAL_PROVIDER=podman"
info "kind 클러스터 '$DEV_CLUSTER' 삭제…"
env $kenv kind delete cluster --name "$DEV_CLUSTER" && echo "✓ 삭제 완료"
# 공유 데이터 소스 컨테이너(yak-dev-*)도 정리(머신 레벨 전체 teardown).
local scs; scs="$("${rt:-docker}" ps -aq -f label=yakcloud.dev/shared=1 2>/dev/null)"
[ -n "$scs" ] && { info "공유 소스 컨테이너 정리…"; echo "$scs" | xargs "${rt:-docker}" rm -f >/dev/null 2>&1; }
# 릴레이 에이전트 데몬(모든 프로젝트) 정리 — kind 삭제되면 폴링해도 무의미.
# 이 클러스터의 릴레이 에이전트만 정리(pidfile <name>-<ns>.pid). 다른 dev 클러스터 것은 건드리지 않음.
local agd="${YAK_CONFIG_DIR:-$HOME/.config/yakcloud}/agent"
if [ -d "$agd" ]; then
for pf in "$agd"/*.pid; do
for pf in "$agd"/"$DEV_NAME"-*.pid; do
[ -f "$pf" ] || continue
local apid; apid="$(cat "$pf" 2>/dev/null)"
[ -n "$apid" ] && kill "$apid" 2>/dev/null
rm -f "$pf"
done
info "릴레이 에이전트 정리"
info "릴레이 에이전트 정리($DEV_NAME)"
fi
# 공유 소스 컨테이너(yak-dev-*)는 여러 클러스터가 공유 → '마지막' dev 클러스터 삭제 시에만 정리.
if env $kenv kind get clusters 2>/dev/null | grep -q '^yak-'; then
info "다른 로컬 dev 클러스터가 남아 공유 소스 컨테이너는 유지"
else
local scs; scs="$("${rt:-docker}" ps -aq -f label=yakcloud.dev/shared=1 2>/dev/null)"
[ -n "$scs" ] && { info "공유 소스 컨테이너 정리(마지막 dev 클러스터)…"; echo "$scs" | xargs "${rt:-docker}" rm -f >/dev/null 2>&1; }
# 마지막 클러스터 → 남은 모든 에이전트 pidfile 정리(구 <ns>.pid 명명 포함 — 전부 고아).
if [ -d "$agd" ]; then
for pf in "$agd"/*.pid; do
[ -f "$pf" ] || continue
local lp; lp="$(cat "$pf" 2>/dev/null)"; [ -n "$lp" ] && kill "$lp" 2>/dev/null
rm -f "$pf"
done
fi
fi
_dev_deregister
;;
*) echo "yakcloud dev <up|deploy [tag]|status|clean|down>"; exit 1 ;;
*) echo "yakcloud dev <up [--name N --port P] | deploy [--name N] [tag] | status [--name N] | clean | down [--name N]>"; exit 1 ;;
esac
}