본문으로 건너뛰기

Workspace Isolation (Multi-Tenant 격리)

GenD 의 Workspace 는 다중 고객사 (테넌트) 격리를 위한 가장 외곽 컨테이너입니다. 기존 ResourceGroup (부서 단위 격리) 의 상위에 위치하며, ADR-006 의 L4 (워크스페이스) 와 정확히 1:1 매핑됩니다.

격리 계층

각 Layer 는 상위가 가시성을 게이트 합니다. 즉 caller 가 workspace A 에 속하지 않으면 그 안의 어떤 ResourceGroup / DataGrant / Query / DataMart 도 보이지 않습니다.

데이터 모델 (#1018 M1 Step 2 시점)

class Workspace(Base):
__tablename__ = "workspaces"
id: UUID # FK 용 안정적 식별자
slug: str # URL-safe, unique (e.g. "samsung-card")
name: str # 표시명
keycloak_group_path: str # /tenants/\{slug\}
status: str # active | suspended | deleted

FK 전파 (M1 Step 2 — 4 모델, NULLABLE):

테이블workspace_id 컬럼인덱스ondelete
resource_groupsRESTRICT
query_historyRESTRICT
saved_queriesRESTRICT
data_martsRESTRICT

M2 계획: 나머지 cross-cutting 모델 22+ 곳에 FK 전파 + ABAC workspace 차원 적용 + NOT NULL 전환 (백필 완료 후).

Keycloak Group 계층

/tenants/samsung-card
/tenants/samsung-card/marketing
/tenants/samsung-card/finance
/tenants/kogas
/tenants/kogas/production

JWT claim:

  • tenant — primary tenant slug (group path prefix)
  • groups — legacy compat, tenant prefix 제거 후 leaf 만 (["marketing", "finance"])
  • workspace_id — UI/API token 발급 시 해석된 UUID

운영 절차

1. Workspace 생성

# 어드민 권한 (require_admin) 으로 호출 — M1 Step 3+ 에서 라우터 노출 예정
POST /api/v1/workspaces
{
"slug": "samsung-card",
"name": "삼성카드",
"keycloak_group_path": "/tenants/samsung-card"
}

Keycloak 측 group 도 자동 생성됩니다 (M1 Step 4 의 keycloak_sync).

2. ResourceGroup 할당

기존 ResourceGroup 에 workspace_id 채우기 (백필):

UPDATE resource_groups
SET workspace_id = (SELECT id FROM workspaces WHERE slug = 'samsung-card')
WHERE name IN ('marketing', 'finance', 'hr');

3. 백필 정책 (legacy NULL row)

M1 Step 2 시점에는 workspace_id NULLABLE — 기존 row 는 NULL 상태입니다. M2 진입 전 다음 정책으로 백필:

  • 모든 NULL row → default-workspace (system-managed) UUID 로 채움
  • admin 이 후속에 재할당 (UI 또는 SQL)
  • 백필 완료 후 ALTER TABLE … ALTER COLUMN workspace_id SET NOT NULL

M1 Step 3 — apply_workspace_scope + tenant_slug 주입 (#1018)

JWT 디코드 단계 (auth/jwt_bearer.py) 가 Keycloak group path /tenants/<slug>/... 의 prefix 를 TokenPayload.tenant_slug 로 추출. caller 컨텍스트가 자동으로 tenant 인지.

apply_workspace_scope(stmt, model, user) 우선순위

시나리오동작
user is Nonefilter 없음 (stmt 그대로)
user.is_admin == Truefilter 없음 (모든 workspace 노출)
user.tenant_slug is None (legacy)filter 없음 (single-tenant 가정)
user.tenant_slug == "samsung-card"workspace_id IN (SELECT id FROM workspaces WHERE slug='samsung-card' AND status='active') OR workspace_id IS NULL

사용 예

from gend_api.services.group_filter import (
apply_group_filter,
apply_workspace_scope,
)

stmt = select(SavedQuery)
stmt = apply_workspace_scope(stmt, SavedQuery, current_user) # L0 tenant
stmt = apply_group_filter(stmt, SavedQuery, current_user) # L1 부서
rows = (await db.execute(stmt)).scalars().all()

회귀 가드

본 기능은 다음 가드로 보호됩니다:

  • tests/test_workspace_model.py — Workspace 테이블 자체 (20 cases)
  • tests/test_workspace_fk_propagation.py — 4 모델 FK 전파 (19 cases)
  • tests/test_workspace_scope.py — Step 3 scope 헬퍼 (9 cases)
    • TokenPayload tenant_slug 필드 + default None
    • apply_workspace_scope 4 분기 (admin/legacy/tenant/None)
    • Cross-tenant E2E (workspace A token → B row 차단)
    • JWT 디코드 단계 slug 추출 (/tenants/<slug>/...)

PG-only 동작 (실제 IntegrityError) 은 통합 테스트로 분리 (SQLite 단위 테스트는 PRAGMA foreign_keys=OFF 기본).

M1 Step 4 — Keycloak group path 자동 sync (#1042)

Workspace.slug ↔ Keycloak realm 의 /tenants/<slug> group path 를 idempotent 로 동기화.

KeycloakWorkspaceSyncService

메서드효과
sync_workspace(workspace)/tenants parent + /tenants/<slug> child 보장
sync_all(workspaces)active workspace 일괄 sync (non-active skip)
delete_workspace_group(slug)Workspace.status='deleted' 전환 시 group 제거

Idempotency 보장

  • 이미 존재 → GET 만, POST 호출 0
  • 동시 sync 409 Conflict → graceful 재조회
  • non-409 에러는 raise (silent fail 차단)
  • empty slug 즉시 ValueError (Keycloak 호출 0)

호출 지점

  • Step 5 의 workspaces POST/PUT 라우터에서 자동 호출
  • 부팅 시 init_db 다음 (GEND_KEYCLOAK_SYNC_ON_STARTUP=true, graceful)
  • admin endpoint POST /api/v1/workspaces/sync-keycloak

회귀 가드 (10 cases)

tests/test_keycloak_workspace_sync.py:

  • parent + child 신규 / 양쪽 존재 / parent 만 존재 / 409 race / non-409 propagate
  • non-active skip / empty slug 거부 / delete present + absent / repr

M1 Step 5 — Workspace CRUD 라우터 + Keycloak 통합 (#1046)

Admin 전용 /api/v1/workspaces REST API + Step 4 의 KeycloakWorkspaceSyncService 자동 통합. 본 Step 머지로 Workspace Isolation Epic #1018 의 M1 종결.

Endpoints

MethodPath효과
GET/api/v1/workspaces모든 workspace 목록
POST/api/v1/workspaces생성 + Keycloak group 자동 sync
GET/api/v1/workspaces/{id}단건 조회
PUT/api/v1/workspaces/{id}부분 갱신 (name / status) — slug 변경 미지원
POST/api/v1/workspaces/{id}/sync-keycloak단일 sync 강제 (운영 도구)
POST/api/v1/workspaces/sync-keycloak/allactive 일괄 sync

보안 정책

  • 모두 admin 전용 — _protected_routers 등록 + 핸들러 내부 require_admin 게이트
  • keycloak_group_path 는 server-side 에서 /tenants/<slug> 자동 강제 (클라이언트 입력 무시)
  • slug 변경은 의도적 금지 (Keycloak group + cross-cutting FK 동기 비용 — slug 변경 워크플로우는 M2)

Atomicity

  • POST: Keycloak sync 실패 시 DB row rollback → 502 응답
  • PUT to status='deleted': Keycloak group cascade 정리 (delete_workspace_group) 호출

회귀 가드 (17 cases)

tests/test_workspaces_router.py:

  • require_admin (admin pass / viewer 403)
  • WorkspaceCreate validation (slug regex / empty)
  • DB 통합 (list / create / 409 duplicate / Keycloak rollback / 404 / PUT / cascade delete)
  • sync 라우터 2종 (단일 / 일괄, active 필터)
  • ADR-004 protected_routers 등록 가드

알려진 갭 (M2 에서 닫힘)

영향해소 시점
workspace_id NULL row 노출legacy 데이터가 모든 caller 에 보임M2 백필 완료 + NOT NULL 전환
라우터 측 apply_workspace_scope 호출 미적용신규 헬퍼 작성됐지만 50+ 라우터에 점진 적용 필요M2 follow-up (라우터별 PR)
Audit chain workspace 미분리tenant 별 chain 분리 안 됨M3 GA

관련

  • Epic: #1018 (Workspace Isolation cross-cutting prereq)
  • ADR: ADR-006 Ontology Layered Packaging — L4 정의
  • 메모리: project_data_isolation, project_auth_arch
  • 부모 Epic: #989 (Phase 4 RFP 갭 개선)

배포 전 영향 측정 — 고아 워크스페이스 게이트

X-Workspace-Slug 반영 범위를 넓히는 변경(#2714 계열)을 배포하기 전, 누가 영향을 받는지 실측한다. 스크립트는 read-only 다.

kubectl --context aks-genos-prod -n gend exec deploy/gend-api -- \
python -m gend_api.scripts.census_ws_catalog_visibility --json > diff.json

코호트

코호트의미조치
ORPHAN_WORKSPACEKeycloak 멤버십에는 있는데 workspaces 행이 없는 slug 보유배포 차단 — 아래 절차로 해소
IMPACTED워크스페이스별 가시 카탈로그가 서로 다른 다중 멤버십 사용자필요 시 권한 선발급
SAFE다중 멤버십이나 모든 워크스페이스에서 가시 집합이 동일조치 불필요
NOT_AFFECTED_ADMIN플랫폼 관리자 — 가시성 필터를 우회한다조치 불필요
NO_ACCESSviewer/analyst/admin 역할 없음 — 카탈로그 조회 자체가 403조치 불필요
SINGLE_MEMBERSHIP멤버십 1개 이하조치 불필요

deploy_gate_ok: false 일 때

고아 slug 를 가진 사용자는 그 워크스페이스에서 카탈로그가 빈 목록으로 보이고 /query/execute 가 403 이 된다. 둘 중 하나로 해소한 뒤 배포한다.

  1. 워크스페이스를 만든다POST /api/v1/workspaces 로 해당 slug 의 행을 생성. Keycloak 그룹이 정본이고 DB 행이 누락된 경우.
  2. Keycloak 그룹을 정리한다/tenants/<slug> 그룹이 오타이거나 폐기된 워크스페이스인 경우, 해당 그룹에서 멤버를 제거하거나 그룹을 삭제.

orphan_users 필드에 영향 사용자 목록이 나온다.

게이트를 무시하지 말 것

이 게이트가 없던 시절, 고아 slug 사용자는 가시 집합이 비어 "영향 없음"으로 분류돼 거짓 통과했다. 실제 체감은 거부다.

벡터 검색(RAG) 워크스페이스 격리 — 단계 전환

AI 문서 검색이 반환하는 청크를 활성 워크스페이스로 제한하는 기능이다. GEND_VECTOR_WS_ENFORCEMENT_MODE 로 단계 전환한다.

모드동작
off워크스페이스 축을 적용하지 않는다
observe (기본)축을 적용하지 않고 불일치 건수만 계측한다
enforce검색 시 활성 워크스페이스의 문서만 반환한다
enforce 전환 전 반드시 확인할 것

미태깅 문서가 남은 상태에서 enforce 로 바꾸면 그 문서는 오류가 아니라 빈 결과로 조용히 사라진다. 사용자에게는 "업로드는 됐는데 검색이 안 됨"으로 보이고, 서버 로그에는 경고 한 줄만 남는다.

두 수치를 모두 확인한 뒤에만 전환한다.

1. 태깅률 확인

kubectl --context aks-genos-prod -n gend exec deploy/gend-api -- \
python -m gend_api.scripts.census_weaviate_workspace_tag

phase2_gate_ok: true 여야 한다. 아니면 백필을 먼저 실행한다:

kubectl --context aks-genos-prod -n gend exec deploy/gend-api -- \
python -m gend_api.scripts.backfill_weaviate_workspace_id \
--class DocumentChunks --execute

2. 관측 카운터 확인

gend_vector_ws_mismatch_total{reason="untagged"}0 이어야 한다.

트래픽이 없으면 이 수치는 아무것도 증명하지 않는다

카운터가 0인 이유가 "전부 태깅됨"인지 "아무도 검색을 안 함"인지 구분되지 않는다. RAG 사용량이 적은 환경에서는 검색을 실제로 한 번 호출해 계측이 발화하는지 확인한 뒤 판단할 것. (mismatch 가 증가하면 계측은 정상이다.)

3. 전환

kubectl --context aks-genos-prod -n gend set env deploy/gend-api \
GEND_VECTOR_WS_ENFORCEMENT_MODE=enforce

롤백

kubectl --context aks-genos-prod -n gend set env deploy/gend-api \
GEND_VECTOR_WS_ENFORCEMENT_MODE=observe

설정값만 되돌리면 즉시 이전 동작으로 돌아간다 — 데이터 변경이 없다.

알아둘 것

  • 워크스페이스를 해석할 수 없는 호출(관리자 일부 경로, 단일테넌트 구성)은 축이 적용되지 않는다. 전 결과가 사라지는 것을 막기 위한 의도된 동작이다.
  • 오타로 잘못된 모드값을 넣으면 observe 로 폴백하고 오류 로그를 남긴다 — 오타가 전면 차단이나 전면 무시로 번지지 않는다.

NULL workspace_id 청산 — 단계 전환 (#2798 PR-18)

레거시 NULL workspace_id 행은 지금까지 "전원 통과(펜스)·전 워크스페이스 가시(스코프)·기본 워크스페이스 grant(COALESCE)" 로 해석됐습니다. 사용자가 체감하는 "워크스페이스를 바꿔도 같은 것이 보인다" 의 상당 부분이 이 경로입니다. PR-18 은 백필로 NULL 을 청산한 뒤 이 시맨틱을 제거합니다.

모드 (env GEND_WS_NULL_SEMANTICS_MODE)

모드동작
legacy종전 그대로 — NULL 행 전원 통과/전체 가시
observe (기본)동작은 legacy 와 동일, 펜스 표면에서 NULL 행 조우를 계측
strict펜스가 NULL 행을 403 거부, 스코프/grant 술어에서 NULL 분기 제거

오타 값은 observe 폴백 (전면 비가시 사고 방지).

:::warning ⑤ 의도적 NULL 시맨틱 — 백필·NOT NULL·strict 의 예외 (2026-08-04 실측) NULL 이 "미백필 레거시"가 아니라 설계인 테이블 3계열이 prod --apply 중 발각됐습니다 (UNIQUE 충돌이 오염 전에 차단):

  • llm_usecase_bindings — NULL = 전역 기본 바인딩 (llm_resolver Tier 3)
  • ontology_classes/properties/relations/class_grantsL3 공용 팩 (linkml_loader 가 "항상 NULL" 로 upsert)
  • ps_* 5종 — 정확 파티션 시맨틱 (NULL 파티션 = admin/tenantless 뷰)

이들은 백필·NOT NULL 대상에서 영구 제외(_P4B_DELIBERATE_NULL)입니다. fence 는 이 테이블들의 행을 __tablename__ 예외로 전 모드에서 통과시키므로 (PR-18c — L3 공용 팩 조회가 strict 에서 403 되지 않음), strict 전환의 게이트가 충족됩니다. ⑤ 행은 관측 카운터에도 섞이지 않아 gend_ws_null_row_total진짜 스탬핑 누락만 셉니다 — strict 에서 이 값이 오르면 새 쓰기 경로가 NULL 행을 만들고 있다는 뜻입니다. :::

함께 들어간 변경: Workspace.status 시행 — suspended/deleted 워크스페이스는 모든 resolver(활성 해석·읽기 펜스·스탬핑·기본 ws)에서 해석되지 않습니다 (모드 무관 상시 — 종전엔 정지시켜도 데이터 접근이 유지됐습니다).

strict 전환 절차

  1. 백업: 대상 테이블 PG 논리 백업 (정비창 권장 — 백필은 비가역 UPDATE)
  2. 백필: python -m gend_api.scripts.backfill_workspace_nulls (census dry-run 으로 분류표 확인) → --apply. FK 부모 상속(document_* ← ingested_files) 먼저, 나머지는 finance-invest 귀속 (런북 확정 결정). 예외 2부류는 건드리지 않는다: ④ admin-only 감사로그 3종(영구 nullable) + ⑤ 의도적 NULL 시맨틱 10종(위 경고 박스 — census 가 분류로 표시). 0-NULL 검증 대상은 _P4B_NOTNULL_TABLES(33 — P4B 32종 + intel_filings, ADR-0035 D-1) 뿐이다
  3. 0-NULL 검증: --apply 가 마지막에 자동 검증 (실패 시 exit 1)
  4. NOT NULL: gend-api 재기동 — init_db _P4B_NOTNULL_TABLES DO-block 이 0-NULL 테이블에 SET NOT NULL 적용 (NULL 잔존 테이블은 NOTICE 로 skip 후 다음 재기동에서 수렴)
  5. 분포 확인: gend_ws_null_row_total{mode="observe"} 가 실사용 관측창에서 0 유지 — 0 이 아니면 펜스가 아직 NULL 행을 만나고 있다는 뜻 (⑤ 시맨틱 경로 또는 백필 누락 — 감사 로그로 어느 쪽인지 특정)
  6. 전환: 분포 0 확인 후 configmap GEND_WS_NULL_SEMANTICS_MODE: "strict"
    • 재시작 (⑤ 시맨틱은 fence 의 __tablename__ 예외가 상시 보호 — PR-18c)

롤백

kubectl --context aks-genos-prod -n gend set env deploy/gend-api \
GEND_WS_NULL_SEMANTICS_MODE=observe

시맨틱은 설정만으로 즉시 복귀합니다. 백필 자체는 비가역(UPDATE)이므로 §1 의 백업으로만 되돌릴 수 있으나, 귀속 방향이 COALESCE 등가(기본 워크스페이스)라 strict 이전에는 동작 차이가 없습니다.

관측

sum by (surface, mode) (increase(gend_ws_null_row_total[24h]))

surface=fence_read|fence_mutation. strict 에서 이 카운터가 오르면 어떤 경로가 아직 NULL 행을 만들고 있다는 뜻입니다 — 쓰기 경로 스탬핑 누락을 의심하십시오.

워크스페이스 프로비저닝 (#2798 PR-14)

POST /api/v1/workspaces 가 이제 DB 행·Keycloak group 에 더해 물리 자원까지 함께 만듭니다 (실패 시 전체 롤백 + 502):

  1. 물리 스키마명ws_<slug의 dash→underscore>Workspace.physical_schema 에 저장 (충돌 시 숫자 접미, immutable — 재명명 불가)
  2. Trino 스키마iceberg.<physical_schema> + hive.<physical_schema>_staging (IF NOT EXISTS — 재시도 안전)
  3. 네임스페이스 레지스트리CatalogNamespace(ownership_status=assigned, owner_workspace_id=신규 ws) — 신규 프로비저닝은 소유가 정의상 확정이라 백필의 "추측 금지" 원칙과 별개
  4. owner 바인딩NamespaceBinding(access_mode=write) — PR-15 쓰기 게이트가 새 워크스페이스의 자기 스키마 쓰기를 인정
  5. schema-scope group grantgrantee_name="/tenants/<slug>" (Keycloak group path 원문 — 파싱된 slug 를 넣으면 조용히 미매칭), ALL. ★ catalog-scope USE 는 발급하지 않습니다 — 하위 grant 로 카탈로그가 보이는 파생 가시성이 이번에 함께 들어갔습니다

소급 프로비저닝 / 재시도

기존(레거시) 워크스페이스는 physical_schema 가 NULL 입니다. admin 이:

POST /api/v1/workspaces/{id}/provision

멱등 — 이미 프로비저닝된 워크스페이스에는 사실상 no-op. active 상태에서만 허용(409). 생성 시 프로비저닝이 502 로 실패한 경우의 재시도 경로이기도 합니다.

알아둘 것

  • suspend/delete 는 물리 자원을 건드리지 않습니다 (soft-delete 정책 — 스키마 DROP 은 정비창 수동 결정)
  • 새 스키마는 Medallion layer 해석(catalog_layer_resolver)에 잡히지 않습니다 — 워크스페이스 스키마는 레이어 무관 (후속 트랙)

워크스페이스 미배정 호출자의 읽기 — 단계 전환 (신규 가입자 기본 상태 설계)

인증은 됐지만 워크스페이스 멤버십이 없는 호출자(JWT 에 /tenants/<slug> group 이 없음 — 신규 가입 직후가 전형)의 읽기를 GEND_TENANTLESS_READ_MODE 로 단계 전환한다. 종전 동작은 resolve_caller_workspace 가 필터 없음(None)을 반환하는 것이었다 — 인증만 됐을 뿐 워크스페이스에 귀속되지 않은 호출자가 전 워크스페이스의 행을 그대로 봤다.

모드

모드동작
off신규 계측(gend_tenantless_read_denied_total)은 멈춘다 (긴급 롤백용) — 단 gend_workspace_fence_bypass_total{reason="no_slug"} 는 fence 경로에서 이 모드와 무관하게 계속 증가한다 (기존 동작). off 로 내려도 이 카운터가 계속 오르는 것은 정상이다
observe (기본)동작은 종전과 동일 — 전 워크스페이스 통과 — 하되 조우를 계측한다
enforce403 워크스페이스가 배정되지 않았습니다 — 관리자에게 워크스페이스 배정을 요청하세요

오타 값은 observe 로 폴백한다 (다른 게이트 계열과 같은 원칙 — 오타 하나로 전면 차단이 되지 않게). 기본값이 observe 이므로 이 플래그를 배포한 것만으로는 동작이 바뀌지 않는다.

대상 밖

user is None(인증 비활성화) 경로와 admin 호출자는 이 플래그의 대상이 아닙니다 — 둘 다 tenant_slug 를 읽기도 전에 무조건 통과합니다(별도 계측: gend_workspace_fence_bypass_total{reason="no_user"|"admin"}).

관측

gend_tenantless_read_denied_total{mode=...}gend_workspace_fence_bypass_total{reason="no_slug"}상위 집합입니다 (, 등가 아님) — fence 경로(21개 라우터)의 no_slug 증가분에 더해 services/workspace_resolver 를 쓰는 3곳(workspace_mcp_tool, intel/sources)의 거부분도 여기 잡힙니다. enforce 로 전환하면 전자의 증가가 멈추고 후자는 그만큼 이상 늘어야 합니다 — "정확히 같은 수"를 기대하면 정상 상태를 이상으로 오판합니다. 둘 다 정체되면 계측이 죽은 것입니다.

2026-08-07 실측: gend_workspace_fence_bypass_total{reason="no_slug"} 가 7일간 80건 — enforce 전환 시 예상 영향 범위는 작습니다(이 수치는 fence 경로만이며, resolver 경로 3곳의 트래픽은 별도 집계입니다).

enforce 전환 절차

  1. gend_tenantless_read_denied_total{mode="observe"} 분포로 영향 대상 규모 확인
  2. configmap GEND_TENANTLESS_READ_MODE: "enforce" + 재시작
  3. gend_workspace_fence_bypass_total{reason="no_slug"} 증가가 멈추고 gend_tenantless_read_denied_total{mode="enforce"}그 이상으로 오르는지 확인합니다(등가가 아니라 상위 집합 — 위 관측 절 참고)

롤백

다른 단계 플래그(GEND_WRITE_NAMESPACE_MODE, GEND_WS_NULL_SEMANTICS_MODE 등, infra/helm/gend-api/configmap.yaml 롤백 주석 참고)와 같은 방식을 씁니다 — ConfigMap 값을 되돌리고 apply + 재시작합니다. kubectl set env 는 쓰지 않습니다(아래 경고 참고):

# infra/helm/gend-api/configmap.yaml 의 GEND_TENANTLESS_READ_MODE 를
# "observe" 로 되돌린 뒤:
kubectl --context aks-genos-prod -n gend apply -f infra/helm/gend-api/configmap.yaml
kubectl --context aks-genos-prod -n gend rollout restart deploy/gend-api

설정값만 되돌리면 즉시 이전 동작(전 워크스페이스 통과)으로 복귀합니다 — 코드 변경이 필요 없습니다.

kubectl set env 로 롤백하지 말 것 — ConfigMap 이 조용히 죽습니다

deployment.yamlenvFrom: configMapRef(82-84행)로 gend-api-config 를 통째로 주입합니다. kubectl set env deploy/gend-api GEND_TENANTLESS_READ_MODE=observe 를 실행하면 컨테이너 레벨의 명시적 env 항목이 Deployment 오브젝트에 영구히 추가되고, 이 항목이 envFrom 의 같은 키보다 우선합니다(114-115행 GEND_ARANGODB_PASSWORD 주석이 이 우선순위를 그대로 보여줍니다). 그 뒤로는 ConfigMap 의 GEND_TENANTLESS_READ_MODE 를 아무리 고쳐도 조용히 무시되는 죽은 키가 됩니다.

이미 kubectl set env 로 긴급 롤백했다면, ConfigMap 을 다시 유효하게 만들기 위해 오버라이드를 명시적으로 제거해야 합니다(트레일링 -):

kubectl --context aks-genos-prod -n gend set env deploy/gend-api \
GEND_TENANTLESS_READ_MODE-