cli v0.5.0: deploy가 도메인 리컨실 + 소스 405 액션형 에러 + --local 폴백/러너 프리플라이트 + 에러 힌트/pyyaml 가드; init에서 bin/install 제거; 문서(토큰권한·healthz·러너) 보강
This commit is contained in:
@ -111,3 +111,7 @@ git tag v0.1.0 && git push origin v0.1.0 # v* 태그만 배포 트리거(일
|
||||
| `docker create` 실패(잡 시작 안 됨) | 러너 config에서 docker.sock **중복 마운트** → act 기본 마운트만(수동 제거) |
|
||||
| 배포 중 `429 RATE_LIMITED` | CLI가 `retryAfterSec` 백오프로 재시도(내장) |
|
||||
| 다른 러너가 잡 가져감 | 라벨 충돌 → 전용 라벨(`runs-on`) 사용 |
|
||||
| 소스 프로비저닝 `405` | 배포 토큰엔 소스 **생성 권한 없음** → 콘솔에서 소스 만들고 `requires[].name`·`binds[].source`를 그 소스명으로. `yakcloud project check`가 미리 경고(READY 소스 목록 출력) |
|
||||
| 태그 push 했는데 아무 일도 안 남 | 레포에 **러너 0개**(잡이 조용히 큐잉) → `yakcloud project deploy`가 배포 전 러너 프리플라이트 경고. 러너 없으면 `yakcloud project deploy --local`(로컬 빌드·push 후 API 배포) |
|
||||
| 헬스체크 `/healthz` 404/이상 | 엣지가 `/healthz`를 **가로챔** → 배포 검증은 앱 실제 경로(`/`)로. 컨테이너 readiness엔 여전히 `health:` 사용 OK |
|
||||
| `ModuleNotFoundError: yaml`(직접 배포) | `pip install pyyaml`(또는 `pip3 install --break-system-packages pyyaml`). CI 이미지엔 포함됨 |
|
||||
|
||||
69
README.md
69
README.md
@ -1,54 +1,29 @@
|
||||
# yakcloud-starter — YakCloud 프로젝트 기반 + CLI + Claude Code 스킬
|
||||
# yakcloud 스타터 (프레임워크 중립)
|
||||
|
||||
빈 폴더에서 **한 명령으로 프로젝트를 초기화**하고, **태그 push로 자동 배포**하며, **배포 환경을 CLI로 상세 설정**한다.
|
||||
프레임워크 중립 스타터 앱 + CI + 매니페스트 + **`yakcloud` CLI** + **Claude Code 스킬**(`.claude/skills/yakcloud-deploy/`) 포함.
|
||||
의존성 0의 최소 HTTP 서버. **배포 계약**만 지키면 어떤 스택으로 바꿔도 된다:
|
||||
- `PORT` 환경변수로 리슨(기본 8080)
|
||||
- `GET /healthz` → 200 (매니페스트 `health` 경로)
|
||||
- (선택) 바인딩된 소스는 `<ALIAS>_URL` 등 env로 접속
|
||||
|
||||
## CLI 설치 (1회)
|
||||
## 로컬 실행
|
||||
```sh
|
||||
curl -fsSL https://gitea.yakenator.io/yakenator/yakcloud-starter/raw/branch/main/install.sh | sh
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
PORT=8080 python3 app.py
|
||||
curl localhost:8080/healthz # {"ok":true}
|
||||
curl localhost:8080/ # 앱 정보 + 연결된 소스 키
|
||||
```
|
||||
|
||||
## 빈 폴더 → 배포까지
|
||||
```sh
|
||||
yakcloud project init my-app # 스타터+CI+매니페스트+Claude 스킬 스캐폴딩
|
||||
cd my-app
|
||||
# (앱 개발 — Claude Code: "yakcloud 로 배포 붙여줘")
|
||||
yakcloud project info # 프로젝트 개괄(매니페스트 + 라이브 상태)
|
||||
yakcloud project deploy # 커밋 + 태그 push (버전 미지정 시 자동 버전업) → 자동 배포
|
||||
## 매니페스트 매핑 (`yakcloud.yaml`)
|
||||
```yaml
|
||||
workloads:
|
||||
- name: web
|
||||
build: ./ # 이 디렉터리(Dockerfile)를 CI 가 빌드
|
||||
image: gitea.yakenator.io/<GITEA_USER>/<project>-web:${TAG}
|
||||
port: 8080
|
||||
health: /healthz
|
||||
binds:
|
||||
- { alias: db, source: appdb } # → 앱에서 os.environ["DB_URL"] 로 접속
|
||||
```
|
||||
> **Claude Code 흐름**: 빈 폴더에서 "프로젝트 초기화해줘" → `yakcloud project init` → 개발 → "yakcloud에 배포해줘" → `yakcloud project deploy`.
|
||||
|
||||
## 명령
|
||||
```
|
||||
project init [name] 빈 폴더 스캐폴딩
|
||||
project deploy [vX.Y.Z] 커밋+태그 push → 자동 배포 (버전 생략 시 최신 태그 patch+1 자동 버전업)
|
||||
project info 프로젝트 개괄(매니페스트 + 라이브 상태)
|
||||
project check dry-run(직접 API 계획만)
|
||||
─ 배포환경 설정(앱별): 매니페스트 수정 + 배포중이면 재빌드 없이 라이브 반영 ─
|
||||
domain <fqdn> [wl] 도메인 등록 + 워크로드 할당(관리형=즉시 활성)
|
||||
scale <wl> <n> replicas
|
||||
set <wl> --image/--port/--health/--cpu/--mem/--path/--rewrite
|
||||
env <wl> KEY=VAL … [--secret KEY] [--unset KEY]
|
||||
source add <name> <type> [plan] | source rm <name>
|
||||
bind <wl> <source> <alias> | unbind <wl> <alias>
|
||||
```
|
||||
필요 env: `YAKCLOUD_URL`, `YAKCLOUD_TOKEN`(배포토큰 yakd_…), (선택) `YAKCLOUD_CLUSTER`
|
||||
|
||||
## 배포 전 1회 준비
|
||||
1. 콘솔 → 설정 → **배포 토큰** 발급(`yakd_…`).
|
||||
2. Gitea 레포 만들고 `git remote add origin …`. **Secrets**: `YAKCLOUD_URL`, `YAKCLOUD_TOKEN`, `YAKCLOUD_CLUSTER`, `REGISTRY_TOKEN`.
|
||||
3. `yakcloud.yaml` 의 `cluster`·`image`(`<GITEA_USER>`) 수정.
|
||||
|
||||
## 구성
|
||||
```
|
||||
bin/yakcloud # CLI 디스패처
|
||||
install.sh # CLI 설치
|
||||
scripts/yakcloud_deploy.py # 리컨실 배포 CLI(매니페스트→콘솔 API)
|
||||
scripts/yakcloud_ctl.py # info/설정 엔진(매니페스트 편집 + 라이브 PATCH)
|
||||
.claude/skills/yakcloud-deploy/ # Claude Code 스킬
|
||||
app.py Dockerfile # 프레임워크 중립 스타터
|
||||
yakcloud.yaml # 배포 매니페스트
|
||||
.gitea/workflows/deploy.yml # 태그 push CI(매니페스트 구동)
|
||||
DATA-SOURCES.md # 소스별 주입 env 표
|
||||
```
|
||||
## 다른 스택으로 교체
|
||||
`app.py`(+`Dockerfile`)를 원하는 언어/프레임워크로 교체하되 위 3계약 유지.
|
||||
소스별 주입 env 전체 목록은 `../reference/DATA-SOURCES.md` 참고.
|
||||
|
||||
77
bin/yakcloud
77
bin/yakcloud
@ -18,7 +18,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.4.0"
|
||||
VERSION="0.5.0"
|
||||
CONFIG_DIR="${YAKCLOUD_CONFIG_DIR:-$HOME/.config/yakcloud}"
|
||||
CONFIG_FILE="$CONFIG_DIR/config"
|
||||
|
||||
@ -147,6 +147,8 @@ cmd_project_init() {
|
||||
rm -rf "$tmp/s/.git"
|
||||
cp -R "$tmp/s/." .
|
||||
rm -rf "$tmp"
|
||||
# CLI 배포 파일(전역 yakcloud 사용)은 사용자 프로젝트에 불필요 → 스캐폴드에서 제거.
|
||||
rm -rf bin install.sh
|
||||
[ -n "$name" ] && [ -f yakcloud.yaml ] && { sed -i.bak "s/^project: .*/project: $name/" yakcloud.yaml && rm -f yakcloud.yaml.bak; }
|
||||
git rev-parse --git-dir >/dev/null 2>&1 || git init -q
|
||||
echo "✓ 스캐폴딩 완료 ($(pwd))"
|
||||
@ -192,28 +194,77 @@ _maybe_login_prompt() {
|
||||
}
|
||||
|
||||
cmd_project_deploy() {
|
||||
local local_mode=0 ver=""
|
||||
for a in "$@"; do case "$a" in --local) local_mode=1 ;; *) [ -z "$ver" ] && ver="$a" ;; esac; done
|
||||
[ -f yakcloud.yaml ] || die "yakcloud.yaml 없음 — 'yakcloud project init' 먼저"
|
||||
if [ "$local_mode" = 1 ]; then cmd_project_deploy_local "$ver"; return; fi
|
||||
git rev-parse --git-dir >/dev/null 2>&1 || git init -q
|
||||
git remote get-url origin >/dev/null 2>&1 \
|
||||
|| die "git 원격(origin) 없음 — Gitea 레포 만들어 'git remote add origin <url>' + Secrets 설정 후 다시"
|
||||
|| die "git 원격(origin) 없음 — Gitea 레포 만들어 'git remote add origin <url>' + Secrets 설정 후 다시 (또는 'yakcloud project deploy --local')"
|
||||
git fetch -q --tags origin 2>/dev/null || true
|
||||
# 버전: 인자 있으면 그것, 없으면 최신 v* 태그에서 패치 +1(없으면 v0.1.0) — 배포마다 자동 버전업.
|
||||
local ver="${1:-}"
|
||||
if [ -z "$ver" ]; then
|
||||
local last; last="$(git tag -l 'v*' --sort=-v:refname 2>/dev/null | head -1)"
|
||||
if [ -n "$last" ]; then
|
||||
ver="$(printf '%s' "$last" | awk -F. 'BEGIN{OFS="."} {$NF=$NF+1; print}')"
|
||||
else
|
||||
ver="v0.1.0"
|
||||
fi
|
||||
if [ -n "$last" ]; then ver="$(printf '%s' "$last" | awk -F. 'BEGIN{OFS="."} {$NF=$NF+1; print}')"; else ver="v0.1.0"; fi
|
||||
fi
|
||||
case "$ver" in v*) : ;; *) ver="v$ver" ;; esac
|
||||
_runner_preflight # 러너 없으면 경고(비차단)
|
||||
info "배포 버전: $ver (커밋 + 태그 push)"
|
||||
git add -A; git commit -q -m "deploy $ver" 2>/dev/null || true
|
||||
git push -q origin HEAD 2>/dev/null || true
|
||||
git tag "$ver" 2>/dev/null || die "태그 $ver 이미 존재 — 'yakcloud project deploy <다음버전>'"
|
||||
git push -q origin "$ver"
|
||||
echo "✓ $ver push — Gitea Actions 에서 빌드·배포. 콘솔 앱 탭/도메인 확인."
|
||||
echo "✓ $ver push — Gitea Actions 에서 빌드·배포. Actions 탭에서 결과 확인."
|
||||
echo " 실패/러너 없음이면: yakcloud project deploy --local (러너 없이 로컬 빌드/푸시/리컨실)"
|
||||
}
|
||||
|
||||
# 러너 프리플라이트(best-effort) — origin 이 Gitea 면 레포 러너 유무 확인. 없으면 경고(비차단).
|
||||
_runner_preflight() {
|
||||
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() {
|
||||
: "${YAKCLOUD_URL:?YAKCLOUD_URL 필요 — 'yakcloud login'}"; : "${YAKCLOUD_TOKEN:?YAKCLOUD_TOKEN 필요 — 'yakcloud login'}"
|
||||
command -v docker >/dev/null 2>&1 || die "docker 필요(--local 로컬 빌드/푸시)"
|
||||
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 scripts/yakcloud_deploy.py yakcloud.yaml
|
||||
echo "✓ 로컬 배포 완료(tag=$tag) — CI 없이 직접 리컨실."
|
||||
}
|
||||
|
||||
cmd_project_check() {
|
||||
@ -258,14 +309,16 @@ cmd_project_update() {
|
||||
rm -rf .claude/skills/yakcloud-deploy
|
||||
cp -R "$tmp/s/.claude/skills/yakcloud-deploy" .claude/skills/ 2>/dev/null || true
|
||||
rm -rf "$tmp"
|
||||
rm -rf bin install.sh # 옛 스캐폴드 잔재(CLI 배포 파일) 정리
|
||||
echo "✓ 최신화 완료 — scripts/·.gitea/·.claude 스킬 갱신(app.py·yakcloud.yaml·Dockerfile 그대로)"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
yakcloud $VERSION — YakCloud 프로젝트 CLI
|
||||
project init [name] 빈 폴더에 스타터 스캐폴딩(앱+CI+매니페스트+Claude 스킬)
|
||||
project deploy [vX.Y.Z] 커밋 + 태그 push → Gitea Actions 자동 배포(버전 생략=자동 버전업)
|
||||
project init [name] 빈 폴더에 스타터 스캐폴딩(대화형 마법사) + 자격 프롬프트
|
||||
project deploy [vX.Y.Z] 커밋 + 태그 push → Gitea Actions(버전 생략=자동 버전업). 도메인도 리컨실.
|
||||
project deploy --local 러너 없이 로컬 build/push/리컨실(CI 대안)
|
||||
project info 프로젝트 개괄(매니페스트 + 라이브 상태)
|
||||
project check dry-run(직접 API 계획만)
|
||||
project update 이 프로젝트의 엔진·CI·스킬만 최신화(앱·매니페스트 보존)
|
||||
|
||||
@ -22,7 +22,10 @@ import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ModuleNotFoundError:
|
||||
raise SystemExit("PyYAML 필요 — 'pip install pyyaml'(또는 'pip3 install --break-system-packages pyyaml') 후 다시 실행하세요.")
|
||||
|
||||
URL = os.environ["YAKCLOUD_URL"].rstrip("/")
|
||||
TOKEN = os.environ["YAKCLOUD_TOKEN"]
|
||||
@ -50,7 +53,13 @@ def api(method: str, path: str, body: dict | None = None, _retry: int = 5) -> di
|
||||
wait = 2
|
||||
time.sleep(max(1, int(wait)) + 1)
|
||||
return api(method, path, body, _retry - 1)
|
||||
raise SystemExit(f"[api] {method} {path} -> {e.code}: {payload[:400]}")
|
||||
hint = {
|
||||
401: " — 배포 토큰(YAKCLOUD_TOKEN)이 없거나 만료/오류",
|
||||
403: " — 이 토큰 권한으로는 불가(엔드포인트 권한 확인)",
|
||||
404: " — 경로의 id/이름 확인",
|
||||
405: " — 이 배포 토큰/엔드포인트로 허용되지 않는 작업(콘솔에서 처리 필요)",
|
||||
}.get(e.code, "")
|
||||
raise SystemExit(f"[api] {method} {path} -> {e.code}{hint}: {payload[:300]}")
|
||||
|
||||
|
||||
def unwrap(r):
|
||||
@ -78,6 +87,10 @@ def cluster_deployments() -> list[dict]:
|
||||
return unwrap(api("GET", f"/clusters/{CLUSTER}/deployments")) or []
|
||||
|
||||
|
||||
def _ready_sources() -> str:
|
||||
return ", ".join(s.get("name", "?") for s in cluster_services() if s.get("status") == "READY") or "(없음)"
|
||||
|
||||
|
||||
def reconcile_source(req: dict) -> str | None:
|
||||
name, stype, plan = req["name"], req["type"].upper(), req.get("plan", "small")
|
||||
match = next((s for s in cluster_services() if s.get("name") == name), None)
|
||||
@ -85,12 +98,25 @@ def reconcile_source(req: dict) -> str | None:
|
||||
log(f"source '{name}' ({stype}) 이미 READY → 스킵 (id={match['id']})")
|
||||
return match["id"]
|
||||
if DRY:
|
||||
log(f"source '{name}' ({stype}, {plan}) 없음 → 프로비저닝 예정")
|
||||
# 배포 토큰으로는 소스 생성이 막힐 수 있으므로, 없으면 미리 경고(실패 예측).
|
||||
if match:
|
||||
log(f"source '{name}' 상태={match.get('status')} — READY 대기 필요")
|
||||
else:
|
||||
log(f"⚠ source '{name}' 없음 — 배포 토큰으론 생성 불가할 수 있음. "
|
||||
f"콘솔에서 만들거나 requires[].name/binds[].source 를 기존 READY 소스로 지정. 현재 READY: {_ready_sources()}")
|
||||
return None
|
||||
if not match:
|
||||
log(f"source '{name}' ({stype}, {plan}) 프로비저닝…")
|
||||
log(f"source '{name}' ({stype}, {plan}) 프로비저닝 시도…")
|
||||
try:
|
||||
api("POST", f"/clusters/{CLUSTER}/services",
|
||||
{"clusterId": CLUSTER, "type": stype, "name": name, "mode": "local", "size": plan})
|
||||
except SystemExit as e:
|
||||
# 405/403 등 — 이 토큰으로 생성 불가. 행동 가능한 안내로 전환.
|
||||
raise SystemExit(
|
||||
f"소스 '{name}' 자동 생성 불가(배포 토큰 권한). 콘솔에서 소스를 만든 뒤 "
|
||||
f"requires[].name·binds[].source 를 기존 소스명으로 지정하세요.\n"
|
||||
f" 현재 READY 소스: {_ready_sources()}\n"
|
||||
f" (원인: {e})")
|
||||
for _ in range(120): # ~10분
|
||||
s = next((s for s in cluster_services() if s.get("name") == name), None)
|
||||
if s and s.get("status") == "READY":
|
||||
@ -102,6 +128,29 @@ def reconcile_source(req: dict) -> str | None:
|
||||
raise SystemExit(f"source '{name}' READY 대기 초과")
|
||||
|
||||
|
||||
def reconcile_domain(fqdn: str) -> None:
|
||||
"""워크로드 expose host 를 도메인 레지스트리에 리컨실 — 없으면 등록.
|
||||
관리형 도메인(yakenator.io/openrepublic.club/sapiens.inc 등)은 즉시 ACTIVE, 외부는 TXT 검증 안내."""
|
||||
if not fqdn or fqdn == CLUSTER_HOST:
|
||||
return # 클러스터 기본 도메인은 이미 등록·라우팅됨
|
||||
doms = unwrap(api("GET", f"/clusters/{CLUSTER}/domains")) or []
|
||||
if any(d.get("fqdn") == fqdn for d in doms):
|
||||
log(f"domain '{fqdn}' 이미 등록됨")
|
||||
return
|
||||
if DRY:
|
||||
log(f"domain '{fqdn}' 없음 → 등록 예정(관리형=즉시 ACTIVE, 외부=TXT 검증)")
|
||||
return
|
||||
try:
|
||||
d = unwrap(api("POST", f"/clusters/{CLUSTER}/domains", {"fqdn": fqdn}))
|
||||
except SystemExit as e:
|
||||
log(f"⚠ domain '{fqdn}' 등록 실패 — {e}")
|
||||
return
|
||||
log(f"domain '{fqdn}' 등록: status={d.get('status')} cert={d.get('certStatus')}")
|
||||
v = d.get("verify")
|
||||
if v:
|
||||
log(f' 외부 도메인 — DNS 에 TXT 추가 후 검증: {v["host"]} TXT "{v["value"]}"')
|
||||
|
||||
|
||||
def deploy_workload(w: dict) -> tuple[str | None, bool]:
|
||||
image = w["image"].replace("${TAG}", TAG)
|
||||
ex = w.get("expose", {}) or {}
|
||||
@ -126,6 +175,9 @@ def deploy_workload(w: dict) -> tuple[str | None, bool]:
|
||||
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 [])):
|
||||
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')}")
|
||||
|
||||
@ -1,24 +1,33 @@
|
||||
# YakCloud 선언적 배포 매니페스트 — 스타터(최소, 데이터소스 없이 단독 배포 가능)
|
||||
# 태그(v*) push → Gitea Actions → build/push → 리컨실 배포. 자세한 절차: .claude/skills/yakcloud-deploy/SKILL.md
|
||||
# YakCloud 선언적 배포 매니페스트 (범용 템플릿)
|
||||
# 태그(v*) push → Gitea Actions → 이 매니페스트로 배포:
|
||||
# 1) requires 리컨실 — 논리 이름의 소스가 READY 면 스킵, 없으면 type/plan 대로 프로비저닝 후 Ready 대기
|
||||
# 2) workloads 빌드/배포 — build 컨텍스트를 image 로 빌드·push, 각 워크로드가 소스를 alias 로 바인딩
|
||||
# 3) 앱 탭 등록 + 데이터소스 연결(<ALIAS>_URL 등 env 자동 주입)
|
||||
apiVersion: yakcloud/v1
|
||||
project: yakcloud-starter
|
||||
project: my-app # 프로젝트 이름(이미지 경로/표시에 사용) — 소문자·숫자·하이픈
|
||||
|
||||
# 배포 대상 클러스터 — 이름 또는 콘솔 id 로 변경. (YAKCLOUD_CLUSTER 시크릿이 있으면 그게 우선)
|
||||
# 배포 대상 클러스터 — 이름 또는 콘솔 id. (우선순위: YAKCLOUD_CLUSTER 시크릿 > 이 필드)
|
||||
cluster: my-service-19
|
||||
|
||||
# 데이터 소스 필요하면 추가(없으면 빈 목록):
|
||||
# - { name: appdb, type: postgresql, plan: small }
|
||||
requires: []
|
||||
# 필요한 데이터 소스(논리 이름). 없으면 [] 로 둬도 됨.
|
||||
# type ∈ postgresql·mysql·mariadb·mongodb·redis·minio·rabbitmq·solr·oracle, plan ∈ small·medium·large
|
||||
# ⚠ 이름이 클러스터의 기존 READY 소스와 일치하면 스킵·바인딩. 없을 때 자동 프로비저닝은
|
||||
# "배포 토큰에 소스 생성 권한이 있을 때"만 됩니다(없으면 405). 권한이 없으면 콘솔에서 소스를
|
||||
# 먼저 만들고 이 name 을 그 소스명으로 지정하세요. ('yakcloud project check' 로 미리 확인)
|
||||
requires:
|
||||
- { name: appdb, type: postgresql, plan: small }
|
||||
|
||||
# 워크로드(앱 컨테이너). CI 는 각 workload.build 를 도커 빌드해 image 로 push 한다(${TAG}=릴리스 태그).
|
||||
workloads:
|
||||
- name: web
|
||||
build: ./ # 루트 Dockerfile 을 CI 가 빌드
|
||||
image: gitea.yakenator.io/yakenator/yakcloud-starter-web:${TAG} # <GITEA_USER>/<project>-web 로 변경
|
||||
build: ./ # 도커 빌드 컨텍스트(Dockerfile 위치). 예: ./ 또는 ./api
|
||||
image: gitea.yakenator.io/CHANGE_ME_GITEA_USER/my-app-web:${TAG}
|
||||
port: 8080
|
||||
replicas: 2 # ≥2 → 무중단 롤링
|
||||
replicas: 2 # ≥2 → 무중단 롤링(readiness 게이트)
|
||||
health: /healthz
|
||||
resources: { cpu: 25m, mem: 64Mi }
|
||||
expose: { path: /, rewrite: false } # host: <등록도메인> 지정 시 그 도메인으로 노출. 여러 개는 hosts: [a, b]
|
||||
# 데이터 소스를 쓰면 requires 에 추가하고 아래처럼 바인딩(앱은 DB_URL 등으로 접속):
|
||||
# binds:
|
||||
# - { alias: db, source: appdb }
|
||||
resources: { cpu: 25m, mem: 96Mi }
|
||||
# host 지정 시 등록 도메인으로 노출(미지정=클러스터 기본 도메인). 여러 개는 hosts: [a, b]
|
||||
expose: { path: /, rewrite: false }
|
||||
binds:
|
||||
# source=위 requires 이름, alias=env 프리픽스(대문자화). 예 alias=db → DB_URL/DB_HOST/DB_PORT/…
|
||||
- { alias: db, source: appdb }
|
||||
|
||||
Reference in New Issue
Block a user