앱 연동/API (JSON)
API (JSON)
스크립트·CLI·자동화 도구에서 YWS를 다루는 JSON API입니다. 주소는 https://jimmy-home.com/api/v1이고, 응답과 요청 본문은 모두 JSON입니다. 앞으로 만들 MCP 서버, IaC(plan/apply), CLI도 이 API를 씁니다.
콘솔과 같은 규칙: API와 콘솔 버튼은 같은 코드(app/services/)를 부릅니다. 이름 규칙, 한도, 소유자·공유 권한이 콘솔과 똑같습니다. 콘솔에서 안 되는 일은 API로도 안 되고, 그 반대도 마찬가지입니다.
토큰 만들기
콘솔 상단 연동 → API 토큰(/dashboard/api-tokens) → 이름, 만료(30일/90일/1년/없음), 권한 선택 → 토큰 만들기. 토큰(yws_pat_...)은 그때 한 번만 보여 주니 바로 복사하세요. 잃어버렸거나 유출됐으면 폐기하고 새로 만듭니다.
| 권한 |
할 수 있는 것 |
| 읽기 (항상 포함) |
목록, 상태, 설정 조회 (GET) |
| 쓰기 |
만들기, 시작·정지, 사양·코드 변경, 저장소 연결, 공유, Lambda 실행, DB 비밀번호 재발급 |
| 삭제 |
EC2·저장소·Lambda·DB 삭제 (되돌릴 수 없음) |
- 토큰은 만든 사람의 권한으로만 동작합니다. 관리자의 토큰이라도 관리자 권한은 없습니다: 남의 EC2를 켜고 끄는 일 등은 콘솔에서만 할 수 있습니다.
- 포털에는 토큰의 해시만 저장되고, 목록에는 앞 몇 글자만 표시됩니다.
- 사용자당 토큰은 최대 20개입니다.
export YWS_TOKEN=yws_pat_...
curl -H "Authorization: Bearer $YWS_TOKEN" https://jimmy-home.com/api/v1/ec2
curl -H "Authorization: Bearer $YWS_TOKEN" -H "Content-Type: application/json" \
-d '{"name": "my-vol", "size_gb": 10}' https://jimmy-home.com/api/v1/storage
import requests
api = requests.Session()
api.headers["Authorization"] = "Bearer yws_pat_..."
for e in api.get("https://jimmy-home.com/api/v1/ec2").json()["items"]:
print(e["name"], e["status"], e["state"] and e["state"]["label"])
/api/ 경로는 Cloudflare 브라우저 무결성 검사에서 예외라서 어떤 HTTP 클라이언트(Python 표준 urllib 포함)로도 부를 수 있습니다. 콘솔 페이지는 여전히 검사 대상입니다.
- 브라우저에서 로그인한 상태라면 토큰 없이 세션으로도 부를 수 있습니다(권한 전체). 단, 변경 요청은
Content-Type: application/json이어야 합니다(다른 사이트가 사용자 몰래 요청을 보내는 CSRF 방지).
엔드포인트
{id}는 목록에 나오는 id입니다. 목록(/ec2, /storage, /lambdas, /databases, /networks, /loadbalancers)은 ?name=이름으로 하나만 찾을 수 있습니다.
EC2
| 메서드 |
경로 |
권한 |
설명 |
| GET |
/ec2 |
읽기 |
내 EC2 + 공유받은 EC2 (access: owner/shared) |
| GET |
/ec2/{id} |
읽기 |
하나. state는 실시간 상태(running, stopped, error:...) |
| POST |
/ec2 |
쓰기 |
{name, cpu, memory_gb, disk_gb, ssh_key_id?, gpu?, node_id?}. ssh_key_id가 없으면 새 키를 만들고 개인키를 private_key로 한 번만 돌려줌 |
| PATCH |
/ec2/{id} |
쓰기 |
{cpu, memory_gb} 사양 변경 (주인만, 켜져 있으면 재시작) |
| POST |
/ec2/{id}/start, /ec2/{id}/stop |
쓰기 |
시작·정지 (주인, 공유받은 사람) |
| GET |
/ec2/{id}/history |
읽기 |
누가 언제 켜고 껐는지 |
| POST |
/ec2/{id}/shares |
쓰기 |
{username} 공유 (주인만) |
| DELETE |
/ec2/{id}/shares/{share_id} |
쓰기 |
공유 해제 |
| DELETE |
/ec2/{id} |
삭제 |
삭제 (주인만) |
| POST |
/ec2/{id}/inbound |
쓰기 |
{domain_id, port} 내 도메인 → 이 EC2의 포트 (22 불가). 응답에 url |
| DELETE |
/ec2/{id}/inbound/{rule_id} |
쓰기 |
인바운드 규칙 삭제 |
| PUT |
/ec2/{id}/internet-outbound |
쓰기 |
{allowed: true|false} 인터넷으로 나가는 연결 허용 |
POST /ec2는 생성을 시작만 하고 돌려줍니다. GET /ec2/{id}의 state.state가 running이 될 때까지 몇 초 간격으로 조회하세요.
저장소
| 메서드 |
경로 |
권한 |
설명 |
| GET |
/storage, /storage/{id} |
읽기 |
내 것 + 공유받은 것. attachments(공유받은 경우 내 연결만) |
| POST |
/storage |
쓰기 |
{name, size_gb} (size_gb: 10, 20, 50, 100, 200, 500) |
| POST |
/storage/{id}/attachments |
쓰기 |
{target_id} 내 EC2/Lambda에 연결 → /mnt/저장소이름 (EC2는 재시작) |
| DELETE |
/storage/{id}/attachments/{attachment_id} |
쓰기 |
연결 해제 |
| POST / DELETE |
/storage/{id}/shares[/{share_id}] |
쓰기 |
공유 / 해제 (해제하면 그 사람의 연결도 끊김) |
| DELETE |
/storage/{id} |
삭제 |
연결이 남아 있으면 409 |
Lambda
| 메서드 |
경로 |
권한 |
설명 |
| GET |
/lambdas, /lambdas/{id} |
읽기 |
하나 조회 시 code 포함 |
| POST |
/lambdas |
쓰기 |
{name, runtime: python|node|cpp, memory_mi?, cpu_milli?, code?} |
| PATCH |
/lambdas/{id} |
쓰기 |
{code?, runtime?} (실행 중인 인스턴스는 재시작) |
| POST |
/lambdas/{id}/invoke |
쓰기 |
{event?} → 함수 응답 {statusCode, headers, body} (콜드 스타트면 몇 초 걸림) |
| DELETE |
/lambdas/{id} |
삭제 |
|
| POST / DELETE |
/lambdas/{id}/inbound[/{rule_id}] |
쓰기 |
URL 트리거: {domain_id, port} → https://도메인으로 오는 요청이 이 함수를 실행 |
| PUT |
/lambdas/{id}/internet-outbound |
쓰기 |
{allowed} |
| PUT |
/lambdas/{id}/schedule |
쓰기 |
예약 실행: {mode: "cron", cron: "0 9 * * *"} / {mode: "once", run_once_at: "2026-10-06T09:00"} / {mode: "off"} (한국 시간) |
| GET / POST |
/lambdas/{id}/mail-triggers |
읽기 / 쓰기 |
메일 트리거 목록 / {tag, keep_in_mailbox?} → address(나+태그@...)로 오는 메일이 실행 |
| PATCH / DELETE |
/lambdas/{id}/mail-triggers/{trigger_id} |
쓰기 |
{enabled} 켜기·끄기 / 삭제 |
DB
| 메서드 |
경로 |
권한 |
설명 |
| GET |
/databases, /databases/{id} |
읽기 |
접속 주소(host, port, user, url_template). 비밀번호는 없음 |
| POST |
/databases |
쓰기 |
{name, engine: postgres|mysql|redis} → 응답에 password 한 번만 |
| POST |
/databases/{id}/reset-password |
쓰기 |
새 password |
| DELETE |
/databases/{id} |
삭제 |
|
네트워크 캔버스 · 로드밸런서
| 메서드 |
경로 |
권한 |
설명 |
| GET |
/networks, /networks/{id} |
읽기 |
노드(kind: ec2/lambda/database/loadbalancer, 인바운드 규칙, LB 멤버)와 연결(from/to 이름, 포트, 방향) |
| POST |
/networks |
쓰기 |
{name} 빈 캔버스 |
| DELETE |
/networks/{id} |
삭제 |
캔버스와 연결 정책, 그 위의 로드밸런서 삭제 (EC2·Lambda·DB는 그대로) |
| POST |
/networks/{id}/nodes |
쓰기 |
{resource_id, x?, y?} 내 EC2·Lambda·DB 올리기 |
| PATCH / DELETE |
/networks/{id}/nodes/{node_id} |
쓰기 |
{x, y} 위치 / 캔버스에서 내리기 (연결도 같이 삭제, LB 노드면 LB 삭제) |
| POST |
/networks/{id}/edges |
쓰기 |
{from_node_id, to_node_id, port_mode?: all_except_22|custom, port_ranges?: "80,443,8000-8100", direction?: uni|bi}. 같은 쌍에 다시 보내면 수정. DB로의 연결은 EC2에서만, 그 DB 서버 포트만 → 응답 message에 접속 정보 파일 위치 |
| DELETE |
/networks/{id}/edges/{edge_id} |
쓰기 |
연결 삭제 |
| POST |
/networks/{id}/edges/{edge_id}/env |
쓰기 |
EC2 → DB 연결의 접속 정보 파일 다시 쓰기 |
| POST |
/networks/{id}/loadbalancers |
쓰기 |
{name, x?, y?} 로드밸런서 만들기 (응답의 resource_id가 LB id) |
| POST / DELETE |
/networks/{id}/loadbalancers/{lb_id}/members[/{ec2_id}] |
쓰기 |
{ec2_id} 멤버 추가 / 제거 |
| GET |
/loadbalancers |
읽기 |
멤버와 인바운드 규칙 |
| POST / DELETE |
/loadbalancers/{id}/inbound[/{rule_id}] |
쓰기 |
{domain_id, port} → 멤버들로 분산 |
| PUT |
/loadbalancers/{id}/internet-outbound |
쓰기 |
{allowed} 모든 멤버에 적용 |
SSH 키 · 도메인
| 메서드 |
경로 |
권한 |
설명 |
| GET |
/ssh-keys |
읽기 |
EC2 만들 때 쓸 ssh_key_id, 사용 중인 EC2 |
| POST |
/ssh-keys |
쓰기 |
{name, public_key} 공개키 등록 (ssh-ed25519 ...) |
| DELETE |
/ssh-keys/{id} |
삭제 |
사용 중이면 409 |
| GET |
/domains |
읽기 |
내 도메인과 연결된 EC2·Lambda·LB와 포트 |
| POST |
/domains |
쓰기 |
{subdomain} → subdomain.jimmy-home.com (예약어·중복 불가) |
| DELETE |
/domains/{id} |
삭제 |
인바운드 규칙에 쓰이고 있으면 409 |
오류
항상 같은 모양입니다. 검증 오류가 여러 개면 messages에 전부 담깁니다.
{"error": "invalid", "message": "용량 값이 올바르지 않습니다."}
| HTTP |
error |
뜻 |
| 400 |
invalid |
입력값 오류 (이름 규칙, 범위, JSON 아님 등) |
| 401 |
unauthorized |
토큰 없음·만료·폐기 |
| 403 |
insufficient_scope / forbidden |
토큰에 그 권한이 없음 / 승인 안 된 사용자, 세션 요청에 JSON 아님 |
| 404 |
not_found |
없거나 내 것이 아님 (남의 리소스도 "없음"으로 응답) |
| 409 |
conflict |
이미 있는 이름, 이미 정지됨, 아직 연결돼 있음 등 |
| 502 |
upstream_error |
클러스터(k8s·DB 서버) 쪽 실패. 잠시 후 다시 시도 |
구조
app/services/: 검증·권한·k8s 호출·정리 순서 (ec2.py, storage.py, lambdas.py, databases.py, resources.py(삭제)). 콘솔 라우트와 API가 같은 함수를 부르고, 실패는 ServiceError(코드·HTTP 상태·한국어 메시지)로 올라옴
app/routers/api.py: /api/v1 라우트, 인증(Bearer 토큰 또는 세션), 응답 모양
app/routers/api_tokens.py + 모델 ApiToken: 토큰 발급·폐기 (SHA-256 해시만 저장)
- 버전 고정: 응답 필드를 없애거나 의미를 바꾸는 변경은
/api/v2로. 필드 추가는 v1에서 함
app/services/의 각 모듈: ec2, storage, lambdas(트리거 포함), databases, network(캔버스·로드밸런서·연결), inbound(인바운드 규칙·인터넷 아웃바운드), keys, domains, resources(삭제)
- 아직 없는 것: OAuth 액세스 토큰으로 API 호출(scope 동의 화면, MCP 작업 때 함께), Pages·메일, 목록 페이지 나누기(지금은 전부 한 번에)