데이터베이스
데이터베이스 카테고리의 API 작업 13개를 다룹니다.
| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/database | 데이터베이스 사용 가능 여부 |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/database | 데이터베이스를 생성합니다(runlot deploy가 \"database\": true 선언을 읽을 때 호출됨) |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/database/connect | psql 또는 드라이버로 연결하는 데 필요한 정보 |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/database/endpoint | 자격 증명 없는 연결 정보(runlot port-forward) |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens | 유효한 단기 자격 증명 목록 |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens | 단기 자동화용 자격 증명을 발급합니다(runlot pg token) |
| DELETE | /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens/{tokenUser} | 단기 자격 증명을 폐기합니다 |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/database/export | 진단 이미지를 생성합니다(runlot pg export) |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/database/generations | 백업 세대 목록(runlot pg generations) |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/database/restore | 특정 세대로 복원합니다(runlot pg restore) |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/database/delete | 데이터베이스를 삭제합니다(runlot pg delete) |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/database/operations/{opId} | 작업 하나의 진행 상황 |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/database/operations/{opId}/abort | 진행 중인 작업을 중단합니다(runlot pg abort) |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database
응답은 단순히 {db: bool}입니다. 크기나 세대 같은 물리적 상태는 여기에 없는데, 그것은 노드가 진실의 원천이기 때문입니다.
자격 증명(비밀번호)은 이 응답에 없습니다. 이 경로는 viewer도 허용합니다 — 연결 세부 정보는 /database/connect로 분리되었습니다.
operationId getDatabase
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 200 | 데이터베이스 사용 가능 여부 | object |
| 403 | — | — |
| 404 | — | — |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database
데이터베이스 구성 레코드의 존재가 곧 사용 가능 여부를 나타냅니다. 구성이 완료되면 node-agent는 다음 수렴 시점에 프로젝트의 workerd를 env.db가 연결된 구성으로 교체합니다. 배포나 epoch 변경은 필요하지 않습니다.
이 작업은 멱등이며 비밀번호를 회전시키지 않습니다. 두 번째 호출은 행에 이미 있는 값을 다시 읽어 같은 연결 정보를 반환합니다. 비밀번호를 회전시키면 첫 응답에서 기록된 연결 문자열이 조용히 무효화되며, 그것이 죽었다는 유일한 신호는 다음 로그인 실패뿐입니다.
operationId createDatabase
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 200 | 자격 증명 및 연결 정보 | DatabaseConnect |
| 403 | — | — |
| 404 | — | — |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database/connect
/database에서 분리된 이유는 역할입니다. 데이터베이스가 존재하는지는 viewer도 알 수 있지만, 비밀번호는 그렇지 않습니다. 만약 하나의 응답이 둘 다 담고 역할에 따라 필드를 제거하는 방식이었다면, 제거를 빠뜨린 경로 하나가 곧 유출이 됩니다. member 이상만 받을 수 있습니다.
operationId getDatabaseConnect
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 200 | 연결 정보 | DatabaseConnect |
| 403 | — | — |
| 404 | 프로젝트가 존재하지 않거나, 데이터베이스가 아직 생성되지 않았습니다 | Error |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database/endpoint
host, port, database, sslmode뿐입니다. viewer 접근도 허용됩니다 — 여기에는 비밀이 없습니다. runlot port-forward는 로그인 세션을 사용해 연결하므로(front가 세션을 검증합니다, docs/pg-driver-support.md §4.4) 비밀번호가 필요 없고, 이 엔드포인트만으로 충분합니다.
operationId getDatabaseEndpoint
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 200 | 엔드포인트 | object |
| 403 | — | — |
| 404 | — | — |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens
비밀번호와 검증자는 포함되지 않습니다. member 이상이 사용할 수 있습니다.
operationId listDatabaseTokens
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 200 | 목록 조회 | object |
| 403 | — | — |
| 404 | — | — |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens
연결 문자열에 사용할 사용자/비밀번호 쌍입니다. 비밀번호는 이 응답에서만 나타납니다 — CP는 SCRAM 검증자만 보관하므로, 잃어버린 비밀번호는 재발급해야 합니다. 세션은 여전히 프로젝트의 역할 아래에서 열립니다: 토큰 사용자는 인증에 쓰이는 이름일 뿐, 엔진 역할이 아닙니다. member.
operationId createDatabaseToken
요청 본문: application/json · object
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 201 | 발급된 자격 증명 | DatabaseToken |
| 400 | — | — |
| 403 | — | — |
| 404 | — | — |
DELETE /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens/{tokenUser}
이후의 연결은 거부됩니다. 존재하지 않는 자격 증명을 삭제하려 하면 404를 반환합니다. member 이상이 사용할 수 있습니다.
operationId deleteDatabaseToken
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 204 | 폐기됨 | — |
| 403 | — | — |
| 404 | — | — |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/export
백업이 아닙니다(docs/env-db-assembly.md §5). 이것이 만드는 것은 현재 (project, epoch) 아래의 불변 진단 키이며, latest, freshness, retention 포인터 중 어느 것도 옮기지 않고, 다음 자동 백업의 이름에도 영향을 주지 않습니다.
이제 예약 백업이 실행됩니다(Phase 5, docs/phase5.md): node-agent가 스케줄러와 노드 전체 세마포어를 소유하며, 단계별 세대는 RPO 1시간 격자에 맞춰 생성됩니다. 이 세대들은 gen/<project>/<epoch>/ 아래에 저장되며, …/database/generations에서 목록을 확인하고 …/database/restore에서 하나를 선택합니다. 이 엔드포인트가 만드는 진단 이미지는 그 네임스페이스 밖에 머무릅니다 — restore는 그것을 선택할 수 없고, pruning도 이를 세지 않으며, latest를 옮길 수도 없습니다.
멱등이 아닙니다. 서버가 요청마다 새 스탬프를 만들기 때문에, 호출할 때마다 이미지가 하나씩 더 생성됩니다. 멱등하게 흡수되는 유일한 것은 스탬프가 노드에 도달한 뒤의 같은 스탬프 재시도뿐입니다.
member 이상만 요청할 수 있습니다. 이 이미지는 데이터베이스 전체이므로, viewer가 한 번의 호출로 가져와야 할 대상이 아닙니다.
느립니다. 응답은 덤프가 끝나야 도착하며 — 크기에 비례합니다 — 그동안 프로젝트의 액터가 점유됩니다.
operationId exportDatabase
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 200 | 생성된 진단 키 | DatabaseExport |
| 403 | — | — |
| 404 | 프로젝트가 존재하지 않거나, 데이터베이스가 아직 생성되지 않았습니다 | Error |
| 409 | 배치가 중단된 상태입니다(suspended). 진단 내보내기는 실행 중인 incarnation에서만 생성됩니다. 중단된 프로젝트의 데이터를 가져오려면 복원 경로를 사용해야 합니다. | Error |
| 502 | 노드가 이미지 빌드에 실패했습니다(export_failed) | Error |
| 503 | 내보낼 수 있는 상태가 아닙니다 — home 노드 주소가 없거나(no_home_node), CP에 노드 관리자 경로가 설정되어 있지 않습니다(no_node_admin). | Error |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database/generations
복원은 있는 것 중에서만 고를 수 있습니다(docs/phase5.md B1). CP의 db_generations 테이블을 직접 읽습니다. 오프사이트 스토리지를 직접 조회하지는 않습니다: 복원이 수신한 바이트를 검증하려면 bytes와 sha256이 CP에 있어야 합니다.
lastCheckedAt은 변경 없음 건너뛰기(no-change skip)의 하트비트입니다. 세대가 진행되지 않는 것과 백업이 죽은 것은 서로 다른 상황이며, 이 값이 그 둘을 구분합니다 — 비어 있다면 아직 확인이 이루어지지 않았다는 뜻입니다.
member 이상이 사용할 수 있습니다. 목록에는 오프사이트 키와 다이제스트가 포함됩니다.
데이터베이스가 활성화되어 있을 필요가 없습니다. pg delete 이후에도 마지막 안전 세대가 남아 있으므로, 삭제 후에도 이 목록에서 계속 볼 수 있습니다.
operationId listGenerations
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 200 | 세대 목록, 최신순 | DatabaseGenerations |
| 403 | — | — |
| 404 | — | — |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/restore
세대를 복원하는 것은 기존 인스턴스를 종료한 뒤 다시 활성화하는 과정입니다(docs/phase5.md B2). pg.restore가 빈 store만 받아들인다는 사실이 이 형태를 결정합니다: draining → sealing(현재 데이터를 가진 복원 전 세대를 먼저 보존합니다 — 되돌아갈 길) → committing(선택한 세대를 final_gen에 씁니다) → reactivation(epoch+1).
그 사이에 이루어진 쓰기는 유실됩니다. 그래서 admin 이상의 권한이 필요하며, CLI는 프로젝트 이름을 다시 입력하도록 요구합니다.
서버는 latest를 절대 해석하지 않습니다. 목록에서 본 정확한 (epoch, seq)를 전달해야 합니다 — 서버가 스스로 "가장 최근"을 선택한다면, 그 사이에 생성된 예약 세대 때문에 사용자가 본 것과 다른 세대로 보내질 수 있습니다.
202를 반환합니다. 돌아오는 것은 결과가 아니라 operation id입니다; 진행 상황은 …/database/operations/{opId}에서 확인하세요.
operationId restoreDatabase
요청 본문: application/json · RestoreRequest
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 202 | operation을 열었습니다 | OperationStarted |
| 400 | — | — |
| 403 | — | — |
| 404 | 프로젝트가 존재하지 않거나, 데이터베이스가 아직 생성되지 않았습니다 | Error |
| 409 | 다른 operation이 이미 진행 중이거나(operation_in_progress), 배포가 중단된 상태입니다(suspended). | Error |
| 503 | 복원할 수 있는 상태가 아닙니다(예: 사용 가능한 batch 노드가 없음) | Error |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/delete
draining → sealing(마지막 안전 세대를 final로 남깁니다) → committing(project_databases 행을 삭제합니다; 다음 convergence에서 노드가 env.db가 없는 프로세스로 교체하고 데이터 디렉터리를 tombstone으로 옮깁니다, docs/phase5.md B3). admin 이상의 권한이 필요합니다.
confirm은 프로젝트 이름과 정확히 일치해야 합니다. 일치하지 않으면 400 confirm_mismatch를 반환하고 아무 것도 시작되지 않습니다. CLI 프롬프트만으로는 충분하지 않습니다 — 잘못된 디렉터리에서 --yes로 실행된 스크립트는 프롬프트를 아예 보여주지 않습니다.
DELETE 메서드를 쓰지 않는 이유: 이 요청은 행 하나를 삭제하는 것이 아니라 202로 시작해 몇 분에 걸쳐 끝나는 operation이며, 본문에 확인 문자열이 필요합니다 — 경로상의 프록시들이 DELETE 본문을 일관되게 처리하지 않습니다.
operationId deleteDatabase
요청 본문: application/json · DeleteRequest
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 202 | operation을 열었습니다 | OperationStarted |
| 400 | 확인 문자열이 프로젝트 이름과 일치하지 않습니다(confirm_mismatch) | Error |
| 403 | — | — |
| 404 | 프로젝트가 존재하지 않거나, 데이터베이스가 아직 생성되지 않았습니다 | Error |
| 409 | 다른 operation이 이미 진행 중입니다(operation_in_progress) | Error |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database/operations/{opId}
복원 또는 삭제의 진행 상황을 지켜봅니다. CLI는 2초마다 폴링하며 phase가 바뀔 때마다 출력합니다.
operation이 프로젝트 아래에 위치하는 것은 id가 자격 증명이 아니기 때문입니다. cp-core의 /v1/operations/{opId}는 id를 아는 어떤 클라이언트에도 응답합니다(node, cp-public), 하지만 사용자 대면 표면이 같은 방식으로 동작한다면 id 하나가 다른 org의 operation 상태를 여는 키가 되어 버립니다. 이 경로는 응답하기 전에 operation의 projectId가 경로상의 프로젝트와 일치하는지 확인하며, 그렇지 않으면 존재하지 않는 것처럼 404를 반환합니다.
member 이상의 권한이 필요합니다.
operationId getDatabaseOperation
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 200 | Operation 상태 | Operation |
| 403 | — | — |
| 404 | 프로젝트에 해당 operation이 없습니다(다른 프로젝트에 속한 operation도 여기로 떨어집니다) | Error |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/operations/{opId}/abort
복구 불가능한 단계(docs/phase5.md B4) 이전에만 가능합니다: evict, restore_generation, delete는 커밋 전까지 취소할 수 있지만 restore는 취소할 수 없습니다. 취소하면 배치가 active로 돌아가고 terminalCode=aborted로 남습니다.
admin 이상입니다. 서버는 먼저 읽고 나서 취소합니다 — 순서를 뒤집으면 404를 반환하기 전에 다른 프로젝트의 작업을 실제로 취소하게 됩니다.
operationId abortDatabaseOperation
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 200 | 일시 중단된 작업의 상태 | Operation |
| 403 | — | — |
| 404 | 프로젝트에 해당 작업이 없습니다 | Error |
| 409 | 되돌릴 수 없는 지점을 지났습니다(not_abortable) — 커밋 후이거나 restore 작업인 경우입니다. | Error |