yakcloud-starter: 배포 기반(스타터 앱+CI+매니페스트) + Claude Code 스킬 yakcloud-deploy
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,53 @@
|
||||
# 지원 데이터 소스 & 바인딩 env 주입
|
||||
|
||||
앱은 접속정보를 **하드코딩하지 말고**, 바인딩된 소스가 주입하는 아래 env로 읽는다.
|
||||
매니페스트에서 `binds: [{ alias, source }]` 로 연결하면, `alias`(대문자화)가 env 프리픽스가 된다.
|
||||
예: `alias: db` → `DB_URL`, `DB_HOST`, `DB_PORT`, `DB_USERNAME`, `DB_PASSWORD`, `DB_DB`.
|
||||
|
||||
## 지원 소스 (`requires[].type`)
|
||||
| type | 소스 | 비고 |
|
||||
|---|---|---|
|
||||
| `postgresql` | PostgreSQL | 관계형 |
|
||||
| `mysql` | MySQL | 관계형 |
|
||||
| `mariadb` | MariaDB | 관계형(MySQL 와이어 호환) |
|
||||
| `mongodb` | MongoDB | 도큐먼트 |
|
||||
| `redis` | Redis | 인메모리 KV/캐시 |
|
||||
| `minio` | MinIO | S3 호환 오브젝트 스토리지 |
|
||||
| `rabbitmq` | RabbitMQ | 메시지 브로커(AMQP) |
|
||||
| `solr` | Solr | 검색 엔진 |
|
||||
| `oracle` | Oracle | 관계형(외부 연결) |
|
||||
|
||||
`plan` ∈ `{small, medium, large}`.
|
||||
|
||||
## 소스별 주입 env (예시 alias=`db`)
|
||||
모든 타입 공통: **`<ALIAS>_HOST`**, **`<ALIAS>_PORT`**.
|
||||
|
||||
| type | 추가 env 키 | `<ALIAS>_URL` 형식 |
|
||||
|---|---|---|
|
||||
| `postgresql` | `_USERNAME` `_PASSWORD` `_DB` | `postgresql://user:pw@host:port/db` |
|
||||
| `mysql` | `_USERNAME` `_PASSWORD` `_DB` | `mysql://user:pw@host:port/db` |
|
||||
| `mariadb` | `_USERNAME` `_PASSWORD` `_DB` | `mysql://user:pw@host:port/db` |
|
||||
| `mongodb` | `_USERNAME` `_PASSWORD` `_DB` | `mongodb://user:pw@host:port/db?authSource=db` |
|
||||
| `redis` | `_USERNAME`(빈) `_PASSWORD` `_DB`(번호) | `redis://:pw@host:port/db` |
|
||||
| `minio` | `_ACCESS_KEY` `_SECRET_KEY` `_ENDPOINT` `_BUCKET` `_REGION` `_USE_SSL` | `_URL`=`_ENDPOINT` |
|
||||
| `rabbitmq` | `_USERNAME` `_PASSWORD` `_VHOST` `_MGMT_URL` | `amqp://user:pw@host:port/vhost` |
|
||||
| `solr` | `_CORE` `_ENDPOINT` | `http://host:port/solr/core` |
|
||||
| `oracle` | `_USERNAME` `_PASSWORD` `_SERVICE` `_JDBC_URL` `_DSN` | `oracle://user:pw@host:port/service` |
|
||||
|
||||
> 비밀번호·키는 인클러스터 Secret으로 주입되고 콘솔/브라우저에 노출되지 않는다. URL의 user/pw는 URL-인코딩됨.
|
||||
|
||||
## 앱 코드 예시
|
||||
```python
|
||||
# Python — alias=db (postgres)
|
||||
import os, psycopg2
|
||||
conn = psycopg2.connect(os.environ["DB_URL"])
|
||||
```
|
||||
```js
|
||||
// Node — alias=mongo
|
||||
const client = new MongoClient(process.env.MONGO_URL);
|
||||
```
|
||||
```js
|
||||
// MinIO(S3) — alias=files
|
||||
const s3 = new S3Client({ endpoint: process.env.FILES_ENDPOINT, region: process.env.FILES_REGION,
|
||||
credentials: { accessKeyId: process.env.FILES_ACCESS_KEY, secretAccessKey: process.env.FILES_SECRET_KEY } });
|
||||
```
|
||||
168
.claude/skills/yakcloud-deploy/assets/scripts/yakcloud_deploy.py
Normal file
168
.claude/skills/yakcloud-deploy/assets/scripts/yakcloud_deploy.py
Normal file
@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
"""yakcloud deploy — 매니페스트(yakcloud.yaml) 기반 리컨실 배포 CLI (콘솔 API).
|
||||
|
||||
흐름(선언적 idempotent):
|
||||
1) requires 리컨실 — 논리 이름의 소스가 READY 면 스킵, 없으면 type/plan 으로 프로비저닝 → READY 대기.
|
||||
2) workloads 배포 — 각 워크로드 배포(POST .../deployments). 기존이면 PATCH(무중단 롤링).
|
||||
3) 바인딩 — 각 bind(alias→source) 를 POST /services/{serviceId}/bindings {deploymentId, alias}.
|
||||
|
||||
인증: 콘솔 API 에 개인 배포 토큰(PAT) Bearer. (환경변수)
|
||||
YAKCLOUD_URL 예) https://console.yakenator.io
|
||||
YAKCLOUD_TOKEN 배포 토큰(PAT) — 콘솔 설정에서 발급
|
||||
YAKCLOUD_CLUSTER 대상 클러스터 이름 또는 콘솔 id (없으면 매니페스트 cluster:)
|
||||
TAG 이미지 태그 치환용(${TAG}); 없으면 latest
|
||||
사용: yakcloud_deploy.py [manifest.yaml] [--dry-run]
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
import yaml
|
||||
|
||||
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) 미지정 시 여기로 노출
|
||||
TAG = os.environ.get("TAG", "latest")
|
||||
DRY = "--dry-run" in sys.argv[1:]
|
||||
|
||||
|
||||
def api(method: str, path: str, body: dict | None = None, _retry: int = 5) -> dict:
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
req = urllib.request.Request(f"{URL}/api/v1{path}", data=data, method=method,
|
||||
headers={"Authorization": f"Bearer {TOKEN}", "content-type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
raw = r.read().decode()
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
payload = e.read().decode()
|
||||
if e.code == 429 and _retry > 0: # 쓰기 레이트리밋 — retryAfterSec 백오프 후 재시도
|
||||
try:
|
||||
wait = json.loads(payload).get("error", {}).get("detail", {}).get("retryAfterSec", 2)
|
||||
except Exception: # noqa: BLE001
|
||||
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]}")
|
||||
|
||||
|
||||
def unwrap(r):
|
||||
return r.get("data", r) if isinstance(r, dict) else r
|
||||
|
||||
|
||||
def log(m: str) -> None:
|
||||
print(("\033[33m[dry]\033[0m " if DRY else "\033[36m▸\033[0m ") + m, flush=True)
|
||||
|
||||
|
||||
def resolve_cluster(ref: str) -> tuple[str, str]:
|
||||
clusters = unwrap(api("GET", "/clusters")) or []
|
||||
for c in clusters:
|
||||
if ref in (c.get("id"), c.get("name")):
|
||||
return c["id"], c.get("name") or c["id"]
|
||||
names = ", ".join(c.get("name", "?") for c in clusters) or "(없음)"
|
||||
raise SystemExit(f"클러스터 '{ref}' 를 찾을 수 없습니다. 계정 클러스터: {names}")
|
||||
|
||||
|
||||
def cluster_services() -> list[dict]:
|
||||
return unwrap(api("GET", f"/clusters/{CLUSTER}/services")) or []
|
||||
|
||||
|
||||
def cluster_deployments() -> list[dict]:
|
||||
return unwrap(api("GET", f"/clusters/{CLUSTER}/deployments")) 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)
|
||||
if match and match.get("status") == "READY":
|
||||
log(f"source '{name}' ({stype}) 이미 READY → 스킵 (id={match['id']})")
|
||||
return match["id"]
|
||||
if DRY:
|
||||
log(f"source '{name}' ({stype}, {plan}) 없음 → 프로비저닝 예정")
|
||||
return None
|
||||
if not match:
|
||||
log(f"source '{name}' ({stype}, {plan}) 프로비저닝…")
|
||||
api("POST", f"/clusters/{CLUSTER}/services",
|
||||
{"clusterId": CLUSTER, "type": stype, "name": name, "mode": "local", "size": plan})
|
||||
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":
|
||||
log(f"source '{name}' READY (id={s['id']})")
|
||||
return s["id"]
|
||||
if s and s.get("status") == "ERROR":
|
||||
raise SystemExit(f"source '{name}' 프로비저닝 ERROR")
|
||||
time.sleep(5)
|
||||
raise SystemExit(f"source '{name}' READY 대기 초과")
|
||||
|
||||
|
||||
def deploy_workload(w: dict) -> tuple[str | None, bool]:
|
||||
image = w["image"].replace("${TAG}", TAG)
|
||||
ex = w.get("expose", {}) or {}
|
||||
res = w.get("resources", {}) or {}
|
||||
body = {
|
||||
"name": w["name"], "image": image,
|
||||
"port": w.get("port"),
|
||||
"replicasDesired": w.get("replicas", 1),
|
||||
"cpuRequest": res.get("cpu", "25m"), "memRequest": res.get("mem", "96Mi"),
|
||||
"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
|
||||
else:
|
||||
body["exposeHost"] = ex.get("host") or (CLUSTER_HOST or None)
|
||||
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')}")
|
||||
return None, False
|
||||
# 멱등: 기존 배포면 PATCH(이미지 갱신 → kubectl apply = 무중단 롤링). 없으면 신규 생성.
|
||||
existing = next((d for d in cluster_deployments() if d.get("name") == w["name"]), None)
|
||||
if existing:
|
||||
log(f"deploy '{w['name']}' 기존 존재 → PATCH 롤링 업데이트 image={image}")
|
||||
api("PATCH", f"/deployments/{existing['id']}", body)
|
||||
return existing["id"], False
|
||||
log(f"deploy '{w['name']}' 신규 생성 image={image}")
|
||||
dep = unwrap(api("POST", f"/clusters/{CLUSTER}/deployments", body))
|
||||
return dep.get("id"), True
|
||||
|
||||
|
||||
def bind(dep_id: str | None, alias: str, service_id: str | None, source: str) -> None:
|
||||
if DRY or not dep_id or not service_id:
|
||||
log(f"bind '{alias}' → source '{source}' (serviceId={service_id}) 예정")
|
||||
return
|
||||
api("POST", f"/services/{service_id}/bindings", {"deploymentId": dep_id, "alias": alias})
|
||||
log(f"bind '{alias}' → '{source}' 완료")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
global CLUSTER, CLUSTER_HOST
|
||||
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")
|
||||
if not ref:
|
||||
raise SystemExit("대상 클러스터 미지정 — YAKCLOUD_CLUSTER 환경변수 또는 매니페스트 cluster: 필드")
|
||||
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 ''}")
|
||||
src_ids: dict[str, str | None] = {}
|
||||
for req in m.get("requires", []):
|
||||
src_ids[req["name"]] = reconcile_source(req)
|
||||
for w in m.get("workloads", []):
|
||||
dep_id, created = deploy_workload(w)
|
||||
if DRY or created: # 기존 배포는 PATCH(롤링)로 바인딩 유지 → 신규일 때만 바인딩
|
||||
for b in w.get("binds", []):
|
||||
bind(dep_id, b["alias"], src_ids.get(b["source"]), b["source"])
|
||||
log("완료 — 콘솔 앱 탭에서 배포/바인딩 확인" if not DRY else "완료(dry-run) — 실제 변경 없음")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
7
.claude/skills/yakcloud-deploy/assets/starter/Dockerfile
Normal file
7
.claude/skills/yakcloud-deploy/assets/starter/Dockerfile
Normal file
@ -0,0 +1,7 @@
|
||||
# 프레임워크 중립 스타터 — 의존성 없음(표준 라이브러리). 베이스는 Gitea 미러 이미지.
|
||||
FROM gitea.yakenator.io/yakenator/python:3.12-slim
|
||||
WORKDIR /app
|
||||
COPY app.py .
|
||||
ENV PORT=8080
|
||||
EXPOSE 8080
|
||||
CMD ["python", "app.py"]
|
||||
29
.claude/skills/yakcloud-deploy/assets/starter/README.md
Normal file
29
.claude/skills/yakcloud-deploy/assets/starter/README.md
Normal file
@ -0,0 +1,29 @@
|
||||
# yakcloud 스타터 (프레임워크 중립)
|
||||
|
||||
의존성 0의 최소 HTTP 서버. **배포 계약**만 지키면 어떤 스택으로 바꿔도 된다:
|
||||
- `PORT` 환경변수로 리슨(기본 8080)
|
||||
- `GET /healthz` → 200 (매니페스트 `health` 경로)
|
||||
- (선택) 바인딩된 소스는 `<ALIAS>_URL` 등 env로 접속
|
||||
|
||||
## 로컬 실행
|
||||
```sh
|
||||
PORT=8080 python3 app.py
|
||||
curl localhost:8080/healthz # {"ok":true}
|
||||
curl localhost:8080/ # 앱 정보 + 연결된 소스 키
|
||||
```
|
||||
|
||||
## 매니페스트 매핑 (`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"] 로 접속
|
||||
```
|
||||
|
||||
## 다른 스택으로 교체
|
||||
`app.py`(+`Dockerfile`)를 원하는 언어/프레임워크로 교체하되 위 3계약 유지.
|
||||
소스별 주입 env 전체 목록은 `../reference/DATA-SOURCES.md` 참고.
|
||||
55
.claude/skills/yakcloud-deploy/assets/starter/app.py
Normal file
55
.claude/skills/yakcloud-deploy/assets/starter/app.py
Normal file
@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""프레임워크 중립 최소 스타터 — Python 표준 라이브러리만(의존성 0).
|
||||
|
||||
배포 계약만 보여준다:
|
||||
· PORT 환경변수로 리슨(기본 8080)
|
||||
· GET /healthz → 200 {"ok": true} (매니페스트 health 경로)
|
||||
· GET / → 앱 정보 + 바인딩된 소스의 <PREFIX>_URL '키'만(값은 노출 안 함)
|
||||
새 프로젝트는 이 파일을 원하는 스택으로 교체하되, 위 3가지 계약만 지키면 된다.
|
||||
"""
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
import socketserver
|
||||
|
||||
PORT = int(os.environ.get("PORT", "8080"))
|
||||
|
||||
|
||||
def bound_source_keys() -> list[str]:
|
||||
"""바인딩된 데이터 소스가 주입한 <PREFIX>_URL 키만(값 미노출) — 무엇이 연결됐는지 확인용."""
|
||||
return sorted(
|
||||
k for k, v in os.environ.items()
|
||||
if k.endswith("_URL") and isinstance(v, str) and "://" in v
|
||||
)
|
||||
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def _send(self, code: int, obj: dict) -> None:
|
||||
body = json.dumps(obj, ensure_ascii=False).encode()
|
||||
self.send_response(code)
|
||||
self.send_header("content-type", "application/json; charset=utf-8")
|
||||
self.send_header("content-length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
path = self.path.split("?", 1)[0].rstrip("/") or "/"
|
||||
if path in ("/healthz", "/health"):
|
||||
self._send(200, {"ok": True})
|
||||
return
|
||||
self._send(200, {
|
||||
"app": os.environ.get("APP_NAME", "yakcloud-starter"),
|
||||
"host": os.environ.get("HOSTNAME", "?"),
|
||||
"bound_sources": bound_source_keys(), # 연결된 소스 <PREFIX>_URL 키(값 미노출)
|
||||
"path": self.path,
|
||||
})
|
||||
|
||||
def log_message(self, *_args) -> None: # 로그 조용히
|
||||
return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
socketserver.ThreadingTCPServer.allow_reuse_address = True
|
||||
with socketserver.ThreadingTCPServer(("", PORT), Handler) as httpd:
|
||||
print(f"yakcloud-starter listening on :{PORT}", flush=True)
|
||||
httpd.serve_forever()
|
||||
58
.claude/skills/yakcloud-deploy/assets/workflows/deploy.yml
Normal file
58
.claude/skills/yakcloud-deploy/assets/workflows/deploy.yml
Normal file
@ -0,0 +1,58 @@
|
||||
# Gitea Actions — 릴리스 태그(v*) push 시 이미지 빌드/푸시 후 YakCloud 에 리컨실 배포.
|
||||
# 매니페스트 구동형: yakcloud.yaml 의 각 workload.build 를 빌드해 image 로 push → yakcloud_deploy.py.
|
||||
# 프로젝트마다 수정 불필요(레포명은 GITHUB_REPOSITORY 로 자동).
|
||||
# 필요 secrets: REGISTRY_TOKEN, YAKCLOUD_URL, YAKCLOUD_TOKEN, YAKCLOUD_CLUSTER
|
||||
# 러너: 전용 host act_runner, 라벨 ci-polyglot(잡 컨테이너에 host docker.sock 마운트).
|
||||
name: deploy
|
||||
on:
|
||||
push:
|
||||
tags: ["v*"]
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ci-polyglot
|
||||
env:
|
||||
REGISTRY: gitea.yakenator.io
|
||||
steps:
|
||||
- name: Build, push images & deploy
|
||||
env:
|
||||
REG_USER: ${{ github.actor }}
|
||||
REG_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
YAKCLOUD_URL: ${{ secrets.YAKCLOUD_URL }}
|
||||
YAKCLOUD_TOKEN: ${{ secrets.YAKCLOUD_TOKEN }}
|
||||
YAKCLOUD_CLUSTER: ${{ secrets.YAKCLOUD_CLUSTER }}
|
||||
run: |
|
||||
set -eo pipefail
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
echo "▸ tag=$TAG repo=$GITHUB_REPOSITORY registry=$REGISTRY"
|
||||
|
||||
# 1) 소스 체크아웃(git clone — JS 액션 미사용, 견고)
|
||||
git clone --depth 1 -b "$TAG" \
|
||||
"https://${REG_USER}:${REG_TOKEN}@${REGISTRY}/${GITHUB_REPOSITORY}.git" src
|
||||
cd src
|
||||
|
||||
# 2) 레지스트리 로그인
|
||||
echo "$REG_TOKEN" | docker login "$REGISTRY" -u "$REG_USER" --password-stdin
|
||||
|
||||
# 3) PyYAML 준비(yakcloud-runner 이미지엔 이미 있음)
|
||||
python3 -c "import yaml" 2>/dev/null \
|
||||
|| python3 -m pip install --quiet --break-system-packages pyyaml 2>/dev/null \
|
||||
|| pip3 install --quiet pyyaml
|
||||
|
||||
# 4) 매니페스트의 각 workload.build → image 빌드/푸시(${TAG} 치환)
|
||||
python3 - "$TAG" <<'PY'
|
||||
import subprocess, sys, yaml
|
||||
tag = sys.argv[1]
|
||||
m = yaml.safe_load(open("yakcloud.yaml"))
|
||||
for w in m.get("workloads", []):
|
||||
ctx = w.get("build")
|
||||
if not ctx:
|
||||
continue # build 없으면 외부 이미지로 간주(빌드 스킵)
|
||||
img = w["image"].replace("${TAG}", tag)
|
||||
print(f"▸ build {w['name']}: {ctx} -> {img}", flush=True)
|
||||
subprocess.check_call(["docker", "build", "-t", img, ctx])
|
||||
subprocess.check_call(["docker", "push", img])
|
||||
PY
|
||||
|
||||
# 5) YakCloud 리컨실 배포(콘솔 API, PAT 인증)
|
||||
TAG="$TAG" python3 scripts/yakcloud_deploy.py yakcloud.yaml
|
||||
30
.claude/skills/yakcloud-deploy/assets/yakcloud.yaml
Normal file
30
.claude/skills/yakcloud-deploy/assets/yakcloud.yaml
Normal file
@ -0,0 +1,30 @@
|
||||
# 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: my-app # 프로젝트 이름(이미지 경로/표시에 사용) — 소문자·숫자·하이픈
|
||||
|
||||
# 배포 대상 클러스터 — 이름 또는 콘솔 id. (우선순위: YAKCLOUD_CLUSTER 시크릿 > 이 필드)
|
||||
cluster: my-service-19
|
||||
|
||||
# 필요한 데이터 소스(논리 이름). 없으면 [] 로 둬도 됨.
|
||||
# type ∈ postgresql·mysql·mariadb·mongodb·redis·minio·rabbitmq·solr·oracle, plan ∈ small·medium·large
|
||||
requires:
|
||||
- { name: appdb, type: postgresql, plan: small }
|
||||
|
||||
# 워크로드(앱 컨테이너). CI 는 각 workload.build 를 도커 빌드해 image 로 push 한다(${TAG}=릴리스 태그).
|
||||
workloads:
|
||||
- name: web
|
||||
build: ./ # 도커 빌드 컨텍스트(Dockerfile 위치). 예: ./ 또는 ./api
|
||||
image: gitea.yakenator.io/CHANGE_ME_GITEA_USER/my-app-web:${TAG}
|
||||
port: 8080
|
||||
replicas: 2 # ≥2 → 무중단 롤링(readiness 게이트)
|
||||
health: /healthz
|
||||
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