yakcloud-starter: 배포 기반(스타터 앱+CI+매니페스트) + Claude Code 스킬 yakcloud-deploy

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 16:44:01 +09:00
commit 859e5058b8
16 changed files with 903 additions and 0 deletions

View 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"]

View 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` 참고.

View 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()