네임스페이스 바인딩 발급·회수 런북
:::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-invest 를 DB 직접 UPDATE 로 승격해서야 해소됐습니다.
발급 경로가 배치 스크립트뿐이고 회수 경로는 코드에 아예 없었던 것이 원인입니다. 이 API 가 그 두 구멍을 메웁니다.
추기(2026-08-08): 공유 datasets 발급 누락 경로는 ADR-0044 좌표 이관으로
구조적으로 사라졌습니다. 아래 발급·회수 절차 자체는 다른 네임스페이스에 그대로
유효합니다.
:::
0. 전제
- admin 전용입니다. 전 핸들러가
require_admin입니다 (ws-admin 으로는 불가). - DDL·마이그레이션이 필요 없습니다.
namespace_binding테이블은 이미 존재합니다. - 좌표는
(catalog, schema)가 아니라(metastore, database_name)입니다.hive와iceberg가 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_status 가 active 가 아니면 바인딩이 있어도 접근이 열리지 않습니다 —
게이트는 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 으로 알려줍니다.
outcome | 뜻 | HTTP |
|---|---|---|
created | 신규 발급 | 201 |
promoted | 기존 read → write 승격 | 200 |
demoted | 기존 write → read 강등 — 회수와 같은 안전장치를 통과해야 함 | 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"}'
승격(read→write)은 읽기를 깨지 않습니다 — 읽기 게이트는 모드 무관 바인딩을
인정합니다(write ⊇ read).
강등(write→read)은 회수와 같습니다. 쓰기를 즉시 끊으므로 §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 가 아니면 바인딩이 있어도 열리지 않습니다).
-
발급 후 — 대상 워크스페이스 사용자로 실제 쓰기(데이터셋 생성 등)를 수행해 200 을 확인합니다.
-
회수 후 — 같은 조작이 403 이 되는지 봅니다. 양성만 보고 끝내면 "회수가 동작한다"를 증명하지 못합니다.
-
메트릭으로 교차 확인 — 두 차단 사유를 모두 봅니다.
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(이미 그 모드였음)는 감사 레코드를 만들지 않습니다. 이 인덱스의
레코드는 "권한이 바뀌었다"는 사실을 뜻해야 하는데, 재요청까지 섞이면 자동화
(헬스체크·리트라이)의 반복이 노이즈로 신호를 덮습니다. unchanged 는 API 응답의
outcome 으로만 확인하고, 호출 자체는 일반 요청 감사 로그에 남습니다.
| 필드 | 값 |
|---|---|
event | NAMESPACE_BINDING_ADMIN |
action | issue | update_mode | revoke |
outcome | created | promoted | demoted | revoked (★ unchanged 는 감사에 남지 않음 — API 응답에만) |
reason | 회수 사유 (회수 시에만) |
forced | ?force=true 로 강행했는지 |
data_refs_total | 강행 당시 남아 있던 데이터 건수 |
OpenSearch 조회 방법은
query-grant-enforcement.md 의 "차단된 주체 조회"
와 같은 절차이며, event 를 NAMESPACE_BINDING_ADMIN 으로 바꿔 검색합니다.
7. 배치 스크립트와의 관계
python -m gend_api.scripts.issue_namespace_bindings [--write] [--execute]
배치 스크립트는 증거에서 소급 발급하는 도구입니다(read 는 DataGrant, write 는
활성 data_marts). 근거 테이블에 기록이 없는 경로 — datasets 가 정확히 그
경우입니다 — 는 파생되지 않으므로, 개별 발급·회수는 이 API 를 쓰십시오.
8. 자주 겪는 상황
| 증상 | 확인 | 조치 |
|---|---|---|
| 데이터셋 생성이 403 | GET ?workspace=<slug>&namespace=datasets | 없으면 §2 로 write 발급 |
| 발급했는데 여전히 403 | 응답의 workspace_status | active 가 아니면 워크스페이스를 먼저 활성화 |
| 발급했는데 여전히 403 (status 는 active) | 게이트 사유가 no_workspace 인지 | 호출자에게 활성 워크스페이스가 없는 경우 — 스위처/멤버십 문제이지 바인딩 문제가 아님 |
POST 가 409 NAMESPACE_AMBIGUOUS | errors 의 후보 | 요청에 metastore 추가 |
DELETE 가 409 NAMESPACE_BINDING_DATA_PRESENT | errors 의 건수 | 데이터 정리 후 재시도, 또는 승인받고 ?force=true |
| 404 "네임스페이스를 찾을 수 없습니다" | 레지스트리에 그 database 가 있는지 | census 미실행일 수 있음 — catalog_namespace 확인 |
관련 문서
query-grant-enforcement.md— 게이트 판정 규칙·모드 전환·알람workspace-isolation.md— 워크스페이스 프로비저닝(자기 소유 스키마 발급)access-control-canonical-model.md— 2축 직교 접근 제어 모델