본문으로 건너뛰기

네임스페이스 바인딩 발급·회수 런북

:::info 도입 버전 관리자 API (/api/v1/admin/namespace-bindings) 는 v1.2+ 입니다. 그 이전 버전에서는 바인딩 발급·회수 경로가 없어 DB 를 직접 수정해야 했습니다. :::

네임스페이스 바인딩은 "이 워크스페이스가 이 물리 네임스페이스를 쓸 자격이 있나" 를 정하는 행입니다. 게이트의 판정 규칙과 모드 전환은 query-grant-enforcement.md 에 있고, 이 문서는 바인딩을 실제로 만들고 거두는 운영 절차를 다룹니다.

:::danger 왜 이 런북이 생겼나 — 2026-08-05 ~ 08-07 장애 GEND_WRITE_NAMESPACE_MODE=enforce 를 켤 때, 워크스페이스 프로비저너가 만드는 바인딩은 자기 소유 스키마 2개(iceberg.ws_<slug>, hive.ws_<slug>_staging) 뿐이었습니다. 공유 네임스페이스 iceberg.datasets 는 어떤 워크스페이스도 받지 못했고, 그 결과 학습 데이터셋 생성이 전면 403 이 됐습니다. 당시 데이터셋이 0건이라 observe 분포에도 잡히지 않아 두 달 가까이 드러나지 않았고, 결국 finance-investDB 직접 UPDATE 로 승격해서야 해소됐습니다.

발급 경로가 배치 스크립트뿐이고 회수 경로는 코드에 아예 없었던 것이 원인입니다. 이 API 가 그 두 구멍을 메웁니다.

추기(2026-08-08): 공유 datasets 발급 누락 경로는 ADR-0044 좌표 이관으로 구조적으로 사라졌습니다. 아래 발급·회수 절차 자체는 다른 네임스페이스에 그대로 유효합니다. :::

0. 전제

  • admin 전용입니다. 전 핸들러가 require_admin 입니다 (ws-admin 으로는 불가).
  • DDL·마이그레이션이 필요 없습니다. namespace_binding 테이블은 이미 존재합니다.
  • 좌표는 (catalog, schema) 가 아니라 (metastore, database_name) 입니다. hiveiceberg 가 prod 에서 같은 HMS 를 가리키기 때문입니다.
export GEND_API=https://gend.genon.ai/api/v1 # 배포 환경에 맞게
export TOKEN=<admin JWT>
alias gapi='curl -sS -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json"'
메서드경로용도
GET/admin/namespace-bindings?workspace=&namespace=&metastore=&mode=조회·필터
POST/admin/namespace-bindings발급 (있으면 모드를 맞춤)
PATCH/admin/namespace-bindings/{id}모드 변경 (read ↔ write)
DELETE/admin/namespace-bindings/{id}회수

1. 조회 — 지금 누가 무엇에 묶여 있나

gapi "$GEND_API/admin/namespace-bindings?workspace=finance-invest"
{
"items": [
{
"id": "…",
"workspace_slug": "finance-invest",
"workspace_status": "active",
"metastore": "s3a://gend-iceberg",
"database_name": "datasets",
"catalog_names": ["hive", "iceberg"],
"access_mode": "write",
"created_at": "2026-08-07T…"
}
],
"total": 1
}

:::note 오타는 빈 목록이 아니라 404 없는 slug/namespace 로 필터하면 404 입니다. 빈 목록을 돌려주면 "이 워크스페이스는 바인딩이 없다"로 읽혀, 없는 사실을 확인해 준 셈이 되기 때문입니다. :::

workspace_statusactive 가 아니면 바인딩이 있어도 접근이 열리지 않습니다 — 게이트는 active 워크스페이스만 해석합니다. "발급했는데 여전히 403" 의 흔한 원인입니다.

2. 발급 — outcome 을 반드시 읽으십시오

gapi -X POST "$GEND_API/admin/namespace-bindings" -d '{
"workspace_slug": "finance-invest",
"namespace": "datasets",
"mode": "write"
}'

(namespace, workspace) 당 1행이라 이미 있는 쌍은 INSERT 가 아니라 UPDATE 입니다. 그래서 이 API 는 409 로 끝내지 않고 무엇을 했는지 outcome 으로 알려줍니다.

outcomeHTTP
created신규 발급201
promoted기존 readwrite 승격200
demoted기존 writeread 강등 — 회수와 같은 안전장치를 통과해야 함200
unchanged이미 요청한 모드였음 (멱등)200

같은 요청을 여러 번 보내도 안전합니다(unchanged). 런북을 재실행할 수 있습니다.

:::tip 공유 네임스페이스 datasets 수동 발급 — 더 이상 필요 없습니다 (v1.2.1+) 학습 데이터셋이 워크스페이스별 스키마(iceberg.ws_<슬러그>)로 옮겨가면서(ADR-0044), 데이터셋 쓰기 권한은 프로비저너가 만드는 owner 바인딩에 이미 포함됩니다. 신규 워크스페이스 체크리스트에서 datasets 수동 발급 항목을 빼십시오 — 지금 발급해도 데이터셋이 그 네임스페이스에 있지 않으므로 아무 효과가 없고, "발급했으니 됐다" 는 잘못된 안심만 남습니다.

종전 절차 (2026-08-08 이전 — 참고용)

프로비저너는 ws_<slug> / ws_<slug>_staging 두 개만 발급했고 공유 datasets 는 어떤 워크스페이스도 받지 못해, 신규 워크스페이스마다 아래를 수동 실행해야 했습니다.

gapi -X POST "$GEND_API/admin/namespace-bindings" \
-d '{"workspace_slug":"<신규 slug>","namespace":"datasets","mode":"write"}'

프로비저너가 datasets 를 자동 발급하지 않은 것은 버그가 아니라 미결 정책이었습니다 — _register_namespace() 의 owner=NULL 분기가 공유 네임스페이스를 다음 프로비저닝 워크스페이스의 소유로 확정해버려, 그 잘못된 소유 추측이 이후 격리 시행의 근거가 되기 때문입니다 (#3071 §4). 좌표를 옮기면서 이 딜레마 자체가 사라졌습니다.

기존 datasets 바인딩이 남아 있어도 무해합니다. 옛 위치에 아직 데이터셋이 남아 있는 동안은 오히려 필요합니다 — 정리는 #3246 에서 다룹니다. :::

동명 네임스페이스가 여럿일 때

public 처럼 여러 메타스토어에 같은 이름이 있으면 409 + 후보 목록이 돌아옵니다. 임의로 하나를 고르지 않습니다 — 잘못 고르면 엉뚱한 물리 데이터에 접근이 열립니다.

{
"status": 409,
"code": "NAMESPACE_AMBIGUOUS",
"detail": "동일한 database_name 'public' 이 여러 메타스토어에 있습니다 — metastore 를 지정하세요",
"errors": [
{"namespace_id": "…", "metastore": "gendpg", "catalog_names": ["gendpg"]},
{"namespace_id": "…", "metastore": "sourcedb", "catalog_names": ["sourcedb"]}
]
}

"metastore": "sourcedb" 를 요청에 추가해 다시 호출합니다.

3. 모드 변경

gapi -X PATCH "$GEND_API/admin/namespace-bindings/$ID" -d '{"mode":"write"}'

승격(readwrite)은 읽기를 깨지 않습니다 — 읽기 게이트는 모드 무관 바인딩을 인정합니다(write ⊇ read).

강등(writeread)은 회수와 같습니다. 쓰기를 즉시 끊으므로 §4 의 데이터 확인을 그대로 거칩니다. ?force=true 없이는 데이터가 남은 강등이 거부됩니다.

4. 회수 — 사유 필수, 데이터 있으면 기본 거부

gapi -X DELETE "$GEND_API/admin/namespace-bindings/$ID" \
-d '{"reason":"오발급 정리 — 승인 #1234"}'
  • reason필수입니다(빈 문자열 불가, 422). 감사 레코드의 유일한 사유 기록처입니다.
  • 그 워크스페이스가 해당 네임스페이스에 데이터를 갖고 있으면 409 로 거부하고 건수를 돌려줍니다.
{
"status": 409,
"code": "NAMESPACE_BINDING_DATA_PRESENT",
"detail": "ws=finance-invest ns=s3a://gend-iceberg:datasets 에 남아 있는 데이터가 3건 있어 회수를 거부했습니다 — 정리 후 다시 시도하거나 ?force=true 로 강행하세요",
"errors": [
{"resource": "total", "count": 3, "force_required": true},
{"resource": "training_datasets", "count": 3}
]
}

:::danger ?force=true 는 되돌리기 어렵습니다 회수는 읽기·쓰기뿐 아니라 삭제·정리까지 막을 수 있습니다. 데이터를 남긴 채 끊으면 그 워크스페이스는 자기 데이터셋을 치울 수도 없는 상태가 됩니다. 강행은 데이터를 먼저 옮기거나 지운 뒤가 원칙입니다.

gapi -X DELETE "$GEND_API/admin/namespace-bindings/$ID?force=true" \
-d '{"reason":"ws 폐지 — 데이터 이관 완료, 승인 #1234"}'

:::

성공 응답에는 회수 직전 스냅샷이 담깁니다 — 되돌릴 때 필요한 좌표(workspace_slug, metastore, database_name, access_mode)가 그대로 남습니다.

건수는 현재 training_datasets 만 셉니다. 다른 리소스(데이터 마트·파이프라인 산출물 등)는 세지 않으므로, total: 0 이 "그 네임스페이스가 비었다"는 뜻은 아닙니다. 확장 지점은 라우터의 _DATA_REF_COUNTERS 입니다.

5. 검증 — 양방향으로 재십시오

API 가 200 을 준 것은 접근이 열렸다는 증거가 아닙니다. 발급/회수 뒤에는 게이트가 실제로 어떻게 판정하는지 확인합니다.

GET /admin/namespace-bindings 는 게이트 검증이 아닙니다. 그 조회는 바인딩 행의 존재만 보여줄 뿐, 게이트가 실제로 허용하는지·회수 후 차단되는지는 말해주지 않습니다 (워크스페이스가 active 가 아니면 바인딩이 있어도 열리지 않습니다).

  1. 발급 후 — 대상 워크스페이스 사용자로 실제 쓰기(데이터셋 생성 등)를 수행해 200 을 확인합니다.

  2. 회수 후 — 같은 조작이 403 이 되는지 봅니다. 양성만 보고 끝내면 "회수가 동작한다"를 증명하지 못합니다.

  3. 메트릭으로 교차 확인 — 두 차단 사유를 모두 봅니다. unbound 만 보면 워크스페이스 없는 주체(개인 트래킹 키·M2M)의 403 을 놓칩니다:

    increase(gend_write_namespace_gap_total{mode="enforce",reason=~"unbound|no_workspace"}[15m])

5.1 회수 안전장치의 집계 규칙

회수·강등이 "남아 있는 데이터"를 세는 방식은 다음과 같습니다 — 운영자가 409 의 개수를 해석할 때 필요합니다.

  • status='deleting' 데이터셋도 셉니다. 회수는 삭제·정리까지 막으므로, 정리 중인 데이터셋이야말로 바인딩이 더 필요합니다. 제외하면 "치우는 중이라 괜찮다"는 반대 결론이 나옵니다.
  • 좌표 매칭: workspace_id + trino_schema(정규화 비교). 네임스페이스 레지스트리 행의 catalog_names 가 비어 있으면 스키마만으로 셉니다 — 좁히지 못하는 것을 좁힌 척하면 개수가 0 으로 나와 안전장치가 조용히 무력해집니다.
  • 현재 집계 대상은 training_datasets 뿐입니다. by_resource키는 늘어날 수 있으므로, 자동화는 total 로 판정하고 내역은 표시용으로만 쓰십시오.

6. 감사 조회

실제로 상태가 바뀐 발급·변경·회수는 서명된 감사 레코드로 남습니다.

unchanged(이미 그 모드였음)는 감사 레코드를 만들지 않습니다. 이 인덱스의 레코드는 "권한이 바뀌었다"는 사실을 뜻해야 하는데, 재요청까지 섞이면 자동화 (헬스체크·리트라이)의 반복이 노이즈로 신호를 덮습니다. unchangedAPI 응답의 outcome 으로만 확인하고, 호출 자체는 일반 요청 감사 로그에 남습니다.

필드
eventNAMESPACE_BINDING_ADMIN
actionissue | update_mode | revoke
outcomecreated | promoted | demoted | revoked (★ unchanged 는 감사에 남지 않음 — API 응답에만)
reason회수 사유 (회수 시에만)
forced?force=true 로 강행했는지
data_refs_total강행 당시 남아 있던 데이터 건수

OpenSearch 조회 방법은 query-grant-enforcement.md 의 "차단된 주체 조회" 와 같은 절차이며, eventNAMESPACE_BINDING_ADMIN 으로 바꿔 검색합니다.

7. 배치 스크립트와의 관계

python -m gend_api.scripts.issue_namespace_bindings [--write] [--execute]

배치 스크립트는 증거에서 소급 발급하는 도구입니다(read 는 DataGrant, write 는 활성 data_marts). 근거 테이블에 기록이 없는 경로 — datasets 가 정확히 그 경우입니다 — 는 파생되지 않으므로, 개별 발급·회수는 이 API 를 쓰십시오.

8. 자주 겪는 상황

증상확인조치
데이터셋 생성이 403GET ?workspace=<slug>&namespace=datasets없으면 §2 로 write 발급
발급했는데 여전히 403응답의 workspace_statusactive 가 아니면 워크스페이스를 먼저 활성화
발급했는데 여전히 403 (status 는 active)게이트 사유가 no_workspace 인지호출자에게 활성 워크스페이스가 없는 경우 — 스위처/멤버십 문제이지 바인딩 문제가 아님
POST 가 409 NAMESPACE_AMBIGUOUSerrors 의 후보요청에 metastore 추가
DELETE 가 409 NAMESPACE_BINDING_DATA_PRESENTerrors 의 건수데이터 정리 후 재시도, 또는 승인받고 ?force=true
404 "네임스페이스를 찾을 수 없습니다"레지스트리에 그 database 가 있는지census 미실행일 수 있음 — catalog_namespace 확인

관련 문서