yakcloud-starter: 배포 기반(스타터 앱+CI+매니페스트) + Claude Code 스킬 yakcloud-deploy
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
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()
|
||||
Reference in New Issue
Block a user