아, 그러면 구조가 훨씬 명확해집니다. 수천 개 프로젝트라면 bucket-per-project보다 지금 말씀하신 1 bucket + project별 prefix가 훨씬 현실적입니다.
그리고 지금 이미 Kafka → StarRocks 실시간 집계 + daily Inventory reconciliation을 구축해 놓았다면, 저는 오히려 AIStor의 bucket quota를 억지로 사용하지 않고, 여러분의 Project Portal을 quota control-plane으로 만드는 방향을 추천합니다.
다만 "quota 도달 → access key의 policy에서 해당 prefix의 PutObject를 제거"하는 부분은 한 가지 중요한 설계 수정이 필요합니다.
예를 들어 하나의 MLflow bucket:
mlflow-artifacts/
│
├── project-000001/
│ ├── experiment-1/
│ ├── run-001/
│ └── ...
│
├── project-000002/
│ ├── experiment-1/
│ └── ...
│
├── project-000003/
│
└── ...
Project Portal:
Project Portal
│
┌──────────┼──────────┐
│ │ │
Project Usage IAM
Catalog Engine Control
│ │ │
▼ ▼ ▼
PostgreSQL StarRocks AIStor
│ ▲ │
│ │ │
│ Kafka │
│ ▲ │
│ │ │
│ AIStor │
│ Events │
│ │
└────────────────────┘
즉 AIStor는 data plane, 여러분의 Portal은 tenant/project control plane입니다.
AIStor 자체도 PBAC으로 resource/action을 명시적으로 허용하는 구조이고, S3 policy resource에 bucket/prefix를 지정할 수 있습니다. (MinIO AIStor Documentation)
저라면 Project를 이렇게 정의합니다.
Project
├── project_id = P000123
├── bucket = mlflow-artifacts
├── prefix = projects/P000123/
├── quota_bytes = 5 TB
├── current_bytes = 3.7 TB
├── object_count = 1,823,123
├── status = NORMAL
├── access_key = AKIA...
├── policy = project-P000123
└── put_enabled = true
즉:
Project = bucket + prefix + access key + policy + quota
입니다.
이 부분은 제가 오히려 AIStor native bucket quota보다 현재 설계가 더 적합하다고 봅니다.
AIStor의 native quota는 bucket 단위이고, 내부 scanner에 의한 best-effort / non-real-time enforcement입니다. 따라서 prefix 단위 quota가 필요한 지금 구조에는 맞지 않습니다. (MinIO AIStor Documentation)
여러분은 이미:
AIStor
│
├── PUT notification ──┐
│ │
└── DELETE notification┤
▼
Kafka
│
▼
StarRocks
│
┌───────┴───────┐
│ │
object count total bytes
가 있고,
Daily
AIStor Inventory
↓
StarRocks
↓
reconciliation
을 하고 있으므로:
StarRocks를 project usage의 authoritative/near-real-time usage store로 사용하는 게 좋습니다.
여기가 핵심입니다.
처음 생각하신 방식:
Project A
AccessKey A
│
└── Policy A
├── GetObject
├── ListBucket
└── PutObject
quota 초과:
Policy A 수정
│
└── PutObject 제거
도 가능합니다.
AIStor는 service account를 생성/업데이트할 수 있고 service account에 custom policy를 적용할 수 있습니다. Admin API에는 service-account update와 policy association/update 관련 기능이 있습니다. (MinIO AIStor Documentation)
하지만 저는 이 방식보다 정책을 2개로 나누겠습니다.
project-readwrite + project-readonly예를 들어:
project-P000123-rw
{
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::mlflow-artifacts"
],
"Condition": {
"StringLike": {
"s3:prefix": [
"projects/P000123/*"
]
}
}
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::mlflow-artifacts/projects/P000123/*"
]
}
]
}
그리고 quota 초과 상태:
project-P000123-ro
GetObject
ListBucket
DeleteObject
만 허용.
즉:
NORMAL
AccessKey
│
└── project-P000123-rw
│
├── GET
├── PUT ✓
└── DELETE
quota exceeded:
QUOTA_EXCEEDED
AccessKey
│
└── project-P000123-ro
│
├── GET ✓
├── PUT ✕
└── DELETE ✓
이 구조가 훨씬 좋습니다.
DELETE는 남겨야 하나?이건 상당히 중요합니다.
quota가:
5 TB
이고 현재:
4.99 TB
인데 PUT이 들어와서:
5.01 TB
가 됐다고 합시다.
이 상태에서:
PUT DENY
DELETE DENY
를 해버리면 사용자가 공간을 줄일 방법이 없습니다.
따라서 quota exceeded 상태에서는:
GET ✓
LIST ✓
DELETE ✓
PUT ✕
로 해야 합니다.
사용자가 object를 삭제해서:
5.01 TB
↓
4.7 TB
가 되면 자동으로:
PUT ✓
상태로 되돌립니다.
Project마다:
NORMAL
│
│ usage >= quota
▼
QUOTA_EXCEEDED
│
│ usage < quota
▼
NORMAL
그리고 조금 더 정교하게:
NORMAL
│
│ 80%
▼
WARNING
│
│ 100%
▼
QUOTA_EXCEEDED
│
│ usage < 90%
▼
NORMAL
을 추천합니다.
여기서 hysteresis를 넣는 것이 중요합니다.
예:
PUT 차단 >= 100%
PUT 재허용 <= 95%
그러면 usage가 99.9% ↔ 100.1%를 왔다 갔다 할 때 policy가 계속 변경되는 것을 방지할 수 있습니다.
여러분의 usage가:
PUT event
↓
Kafka
↓
StarRocks
이므로 usage 판단에는 latency가 있습니다.
예:
현재 usage = 4.95 TB
quota = 5 TB
동시에 100GB PUT
│
▼
AIStor
│
└── PUT accepted
아직 Kafka/StarRocks에는 반영 안 됨
│
▼
usage = 4.95 TB로 보임
그러면 실제로는:
5.05 TB
가 될 수 있습니다.
즉 여러분의 시스템은:
hard quota가 아니라 "application-level near-real-time quota enforcement"
입니다.
이건 충분히 좋은 설계지만 이 특성을 명확히 정의해야 합니다.
예를 들어:
Quota = 5 TB
80% → WARNING
90% → CRITICAL
95% → PRE-BLOCK
100% → BLOCK
그런데 실제로는 Kafka latency + event processing latency + concurrent upload를 고려해서:
Effective quota
↓
95%에서 Put 제한 준비
↓
100%에서 Put 차단
처럼 운영할 수도 있습니다.
더 좋은 방법은 프로젝트별 reserved_bytes 또는 inflight_bytes를 함께 관리하는 것입니다.
StarRocks usage:
actual_usage
외에:
estimated_inflight_usage
를 둡니다.
예:
quota = 5TB
actual_usage = 4.7TB
pending_put_bytes = 0.25TB
effective_usage
= 4.95TB
그러면:
effective_usage >= quota
를 기준으로 판단할 수 있습니다.
하지만 이건 PUT request의 Content-Length를 event보다 먼저 받아야 하므로, Portal이 PUT path에 직접 관여하지 않는 현재 구조에서는 구현이 어렵습니다.
따라서 처음에는 단순하게:
Kafka event lag + daily reconciliation을 고려한 best-effort quota
로 정의하는 것이 현실적입니다.
이 부분은 걱정하지 않으셔도 됩니다.
AIStor policy의 Resource에:
arn:aws:s3:::mlflow-artifacts/projects/P000123/*
를 넣어서 특정 project prefix만 접근하도록 만들 수 있습니다. AIStor는 AWS IAM-compatible policy 구조를 사용하고 bucket/prefix resource에 wildcard를 사용할 수 있습니다. (MinIO AIStor Documentation)
따라서:
AccessKey-A
↓
projects/P000123/*
만 접근 가능하게 하고,
projects/P000124/*
projects/P000125/*
에는 접근할 수 없게 만들 수 있습니다.
여기서는 조금 고민이 필요합니다.
수천 개:
project-000001
project-000002
...
project-010000
이면:
10,000 policies
10,000 access keys
가 될 수 있습니다.
10,000개의 policy 자체가 불가능하다고 단정할 정도는 아니지만, 저는 정책 관리 구조를 조금 다르게 가져가겠습니다.
AIStor service account는 parent identity의 policy를 상속할 수 있고, custom policy를 service account에 부여할 수 있습니다. AIStor 문서상 service account는 application credential로 scoped access를 제공하는 용도입니다. (MinIO AIStor Documentation)
즉:
Project User
│
└── Access Key
│
└── embedded/session policy
│
└── project prefix
로 가져가는 방법입니다.
다만 여기에도 중요한 함정이 있습니다.
AIStor 문서에 따르면 access key 생성 시 embedded session policy는 생성 시점에 캡처되며 나중에 parent user policy를 수정해도 embedded policy가 자동으로 갱신되지 않습니다. (MinIO AIStor Documentation)
따라서 quota 상태 변경을 위해 service account 자체의 policy를 update하는 방식을 명확히 설계해야 합니다.
AIStor의 UpdateServiceAccount는 policy 변경을 지원합니다. (MinIO AIStor Documentation)
project
──────────────────────────────
project_id
bucket
prefix
quota_bytes
usage_bytes
object_count
quota_state
NORMAL
WARNING
EXCEEDED
write_state
ENABLED
DISABLED
aistor_access_key
aistor_policy_version
last_usage_event
last_reconciliation
last_policy_update
그리고 중요한:
desired_state
actual_state
를 분리하세요.
예:
desired_write_state = DISABLED
actual_aistor_state = ENABLED
이면 controller가 다시 AIStor policy update를 수행합니다.
이걸 단순 REST API로:
usage > quota
↓
policy update
하지 말고,
Project Quota Controller를 별도로 두는 것을 추천합니다.
Kafka
│
▼
StarRocks
│
▼
Quota Controller
│
├── Project DB
│
├── usage calculation
│
└── desired state
│
▼
AIStor IAM
Controller:
reconcile(project)
를 수행합니다.
예:
1. Project 조회
↓
2. StarRocks usage 조회
↓
3. quota 비교
↓
4. desired state 계산
↓
5. 현재 AIStor policy 상태 조회
↓
6. 차이가 있으면 policy update
↓
7. 결과 기록
예:
usage = 5.2 TB
quota = 5 TB
desired:
PUT = DENY
actual:
PUT = ALLOW
↓
UpdateServiceAccount()
↓
actual:
PUT = DENY
이렇게 하면 AIStor API 호출 실패에도 다음 reconcile에서 자동 복구됩니다.
이건 그대로 유지하는 것을 추천합니다.
실시간:
S3 PUT/DELETE notification
↓
Kafka
↓
StarRocks
↓
near-real-time usage
그리고 daily:
AIStor Inventory
↓
StarRocks
↓
reconciliation
↓
event-based usage 보정
즉:
Kafka = fast path / Inventory = truth reconciliation
으로 보면 됩니다.
AIStor Inventory는 prefix filter도 지원하므로 특정 project prefix만 대상으로 하는 reconciliation도 가능합니다. (MinIO AIStor Documentation)
정리하면:
bucket
↓
quota
→ 지금 구조와 안 맞음.
bucket
↓
prefix
↓
StarRocks usage
↓
Portal
↓
AIStor IAM
→ 정확히 여러분의 요구사항에 맞음.
AIStor native bucket quota는 scanner 기반이고 bucket 단위이기 때문에, prefix별 quota가 필요한 이 구조에서는 오히려 이중 quota 시스템이 될 수 있습니다. (MinIO AIStor Documentation)
┌───────────────────────┐
│ Project Portal │
│ │
│ Project CRUD │
│ Access Key lifecycle │
│ Quota configuration │
│ Usage dashboard │
└───────────┬───────────┘
│
Project DB
│
▼
┌───────────────────────┐
│ Quota Controller │
│ │
│ usage vs quota │
│ state machine │
│ IAM reconciliation │
└───────────┬───────────┘
│
AIStor Admin API
│
▼
┌───────────────────────┐
│ AIStor │
│ │
│ mlflow-artifacts │
│ │
│ projects/P001/* │
│ projects/P002/* │
│ projects/P003/* │
│ ... │
└───────────┬───────────┘
│
S3 notification
│
▼
Kafka
│
▼
StarRocks
│
│
Daily reconciliation
▲
│
Inventory
이게 이 설계의 가장 큰 장점입니다.
MLflow 입장에서는:
MLflow
│
│ S3
▼
s3://mlflow-artifacts/projects/P001/
일 뿐입니다.
MLflow는:
를 알 필요가 없습니다.
단순히:
PutObject
↓
403 AccessDenied
를 받게 됩니다.
즉 MLflow와 quota management를 완전히 decouple할 수 있습니다.
예를 들어 Portal에서:
Project P001
Storage
────────────────────────
Usage 5.03 TB
Quota 5.00 TB
Usage 100.6%
Status
🔴 QUOTA EXCEEDED
Write
🔴 DISABLED
Reason
Project storage quota exceeded.
To resume writes:
Delete objects until usage falls below 4.75 TB.
이렇게 보여주면 좋습니다.
MLflow 자체에는:
AccessDenied
만 발생합니다.
이건 구현할 때 놓치기 쉬운 부분입니다.
MLflow artifact가 대형 파일이면 multipart upload를 사용할 수 있습니다.
따라서 write-disable policy에서:
s3:PutObject
만 제거하면 안 되고 multipart 관련 write operation도 함께 확인해야 합니다.
예를 들면:
PutObject
CreateMultipartUpload
UploadPart
CompleteMultipartUpload
AbortMultipartUpload
중에서 실제 AIStor가 지원/평가하는 action과 MLflow client의 upload 경로를 테스트해서 write path 전체를 차단해야 합니다.
AIStor policy는 multipart upload 관련 S3 actions도 지원합니다. (MinIO AIStor Documentation)
특히 이미 진행 중인 multipart upload가 quota exceeded 시 어떻게 처리되는지 별도로 테스트하는 것을 권장합니다.
지금 말씀하신 설계라면 저는 bucket-per-project를 포기하고 single bucket + project prefix로 가는 것에 동의합니다.
오히려 여러분 환경에서는 이게 더 적합합니다.
1 AIStor bucket
│
├── projects/P001/
├── projects/P002/
├── projects/P003/
└── ... 수천~수만 project
각 project:
Project
├── prefix
├── access key
├── scoped policy
├── quota
├── StarRocks usage
└── quota state
그리고:
Kafka/StarRocks → Usage 판단 → Quota Controller → AIStor service-account policy 변경
을 control loop로 만드는 겁니다.
특히 저는 PUT 제거를 직접 policy JSON을 매번 새로 생성하는 방식보다는 RW policy ↔ RO policy 상태 전환 또는 service-account custom policy update + reconciliation으로 구현하는 것을 추천합니다. AIStor가 service-account update와 policy 관리를 API로 제공하기 때문에 이 구조가 잘 맞습니다. (MinIO AIStor Documentation)
그리고 Quota 초과 상태에서도 GET/LIST/DELETE는 허용해야 합니다. 그래야 사용자가 직접 데이터를 지워 quota를 회복할 수 있습니다.
이 설계에서 다음으로 가장 중요해지는 건 "수천~수만 개 prefix/access key/policy를 AIStor IAM에서 어떻게 효율적으로 관리할 것인가"입니다. 이 부분은 AIStor의 IAM 구조와 service-account policy 특성을 기준으로 정책을 project마다 만들지, access key embedded policy를 쓸지, 혹은 STS/Keycloak 기반 temporary credential로 바꿀지를 비교해서 결정하는 게 좋습니다.