50억 개 이상의 객체(Object)가 적재된 초대형 엑사스케일급 인프라 환경이라면 ListObjectsV2 호출은 스토리지 엔진 내부적으로 엄청난 Read Amplification(읽기 증폭)과 메타데이터 부하를 유발하므로 무조건 차단(지양)하는 것이 100% 옳습니다. 이러한 제약 사항과 인프라 특성을 고려하여, 말씀해 주신 세 가지 핵심 당면 과제(하위 객체 조회 실용성, 자율적 권한 제한 및 토큰 메커니즘, 사용자별 Grafana 대시보드 격리)에 대한 명확한 기술 가이드와 대안을 제시해 드립니다.
ListObjectsV2는 금지되었고, Inventory API는 배치 주기가 길어 실시간성이 떨어집니다. 이 경우 가장 좋은 대안은 요구사항 정의서 7행에 기재되어 있던 "메타 관리 DB 생성(Bucket Notification 시 내용 update)"을 실현하되, 인프라 부하를 최소화하는 방향으로 선회하는 것입니다.
현재 인프라에서 행위 추적 로그(REQ-06-02)를 위해 Fluent-bit ──> OpenSearch 파이프라인을 이미 고려 중이시라면, 이를 객체 조회용 인덱스로 재활용하는 것이 가장 효율적입니다.
Key)와 Size만 웹훅이나 카프카를 통해 OpenSearch(또는 CNPG의 경량 테이블)로 던집니다.REQ-01-05를 완전한 실시간으로 완벽히 만족할 수 있습니다.REQ-02-03)질문 요약: 관리자 요청 없이 사용자가 직접 본인 권한 범위 내에서 하위 프리픽스로 권한을 쪼개어 제한하고 싶을 때, 별도 토큰을 생성해야 하는가? 기존 ID에 추가 할당이 가능한가?
MinIO(및 S3) 표준 아키텍처에는 이를 위해 S3 STS (Security Token Service)의 AssumeRole API를 제공합니다. 사용자는 관리자의 개입 없이, 그리고 기존 접근 ID(Access Key)를 변경하지 않고도 본인 권한을 더 좁게 격리한 임시 토큰을 스스로 만들 수 있습니다.
사용자(또는 현업 시스템)가 본인의 원래 마스터 자격 증명(AD 계정 기반 토큰)을 가지고 MinIO API를 호출할 때, Policy 매개변수(Inline Policy)를 문자열로 함께 실어서 보냅니다.
// 사용자가 스스로 좁혀서 제출한 임시 세션 정책 예시
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": ["arn:aws:s3:::my-bucket/tenant-a/sub-project-x/*"]
}
]
}
질문 요약: 로그인한 사용자마다 자신이 권한을 가진 Prefix 정보만 필터링해서 대시보드에 보여주는 것이 가능한가?
관리자가 유저 수천 명을 위해 대시보드를 수천 개 만들 필요가 없습니다. Grafana의 인증 유저 컨텍스트 변수와 API Server의 Meta DB를 결합하면 단 하나의 대시보드 템플릿으로 유저별 격리가 가능합니다.
${__user.login} 이라는 글로벌 변수가 활성화됩니다.prefix_path와 owner_sabun(소속 유저 사번) 매핑 정보가 저장되어 있습니다.SELECT
prefix_path,
current_usage_bytes,
hard_quota_bytes
FROM prefix_master_table
WHERE owner_sabun = '${__user.login}' -- 로그인한 유저의 사번으로 자동 필터링
하위 객체 조회(REQ-01-05)를 위해 기존에 검토 중이시던 로그 수집용 OpenSearch 클러스터를 검색 엔진(Index Source)으로 결합하여 확장하는 방향에 대해 인프라 관점에서 아키텍처적 거부감이 없으신지 궁금합니다.
===
결론부터 말씀드리면, 권한을 '반고정'으로 길게 가져가고 싶을 때는 STS(AssumeRole) 방식은 적당하지 않습니다. STS 토큰은 설계 목적 자체가 '임시 보안 증명'이기 때문에 MinIO나 AWS 환경 모두 최대 만료 시간 제한(보통 최대 12시간, 가상 설정 시 최대 수일)이라는 명확한 기술적 천장이 존재합니다. 만약 수개월에서 수년간 유지되어야 하는 배치 시스템이나 애플리케이션에 STS를 쓰면, 주기적으로 토큰을 갱신(Refresh)해야 하는 번거로운 로직을 직접 구현해야 합니다.
사용자가 관리자의 개입 없이 스스로 특정 프리픽스로 권한을 쪼개어 '반고정(영구 또는 장기)' 토큰(자격 증명)을 발급하고 싶을 때 가장 완벽한 대안은 MinIO 고유 기능인 '서비스 어카운트(Service Account)'를 활용하는 것입니다.
MinIO의 서비스 어카운트는 부모 계정(사용자의 원래 AD 계정) 하위에 종속되는 장기 자격 증명(Access Key / Secret Key 세트)을 생성하는 기능입니다.
Inline Policy를 삽입하여, 자신이 가진 전체 권한 중 '특정 프리픽스'만 접근할 수 있도록 권한을 가둡니다.사용자 user_A가 본인의 부모 권한(tenant-a/* 전체 접근 가능)을 이용해, 특정 대외 연동 시스템용으로 tenant-a/project-x/* 경로만 접근할 수 있는 반고정 키를 만드는 과정입니다.
user_A가 MinIO API 또는 Console에서 서비스 어카운트 생성을 요청하면서 아래 정책을 주입합니다.{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-lakehouse",
"arn:aws:s3:::my-lakehouse/tenant-a/project-x/*"
]
}
]
}
AccessKey와 SecretKey를 발급합니다.tenant-a/project-x/* 경로에만 작동합니다.s3:* (전체 권한)을 주어도, 부모 계정(user_A)이 tenant-a/* 권한만 있다면, 해당 서비스 어카운트는 tenant-a/* 내에서만 동작합니다.상황에 따라 두 가지 프리픽스 제한 방식을 적절히 섞어서 가이드하는 것이 가장 좋습니다.
| 구분 | STS 임시 토큰 (AssumeRole) | 서비스 어카운트 (Service Account) |
|---|---|---|
| 만료 기간 | 15분 ~ 최대 12시간 (단기 휘발성) | 제한 없음 / 원하는 만큼 길게 (반고정) |
| 발급 형태 | 임시 AccessKey, SecretKey, SessionToken | 고정 AccessKey, SecretKey (Token 불필요) |
| 주요 용도 | * 웹/모바일 App 사용자 데이터 업로드 |
* 프론트엔드 직접 파일 다운로드
1회성 배치 작업 | 타 시스템과의 상시 API 연동
* CI/CD 파이프라인(GitOps) 연계
* 장기 스케줄러 배치 프로그램 |
따라서 유저들이 "우리가 매번 토큰 갱신하기 귀찮으니 반고정으로 쓸 수 있게 해달라"고 요청할 때는 "본인 AD 계정으로 MinIO 접속하셔서 하위 서비스 어카운트(Service Account)를 원하는 프리픽스 정책으로 직접 생성하여 Key를 발급받아 쓰세요"라고 가이드하는 것이 가장 깔끔하고 완벽한 답안입니다.
==
MetaDB에서 가장 고부하 작업인 '실시간/주기적 사용 용량(Byte) 계산 및 저장'을 완전히 배제하기로 한 결정은 매우 탁월합니다. 50억 개 이상의 객체가 있는 엑사스케일 인프라에서 DB 부하와 정합성 스트레스를 지우고 시스템을 고속화할 수 있는 가장 확실한 방법입니다.
용량 계산을 제외하고, 기존 API Server의 코드 수정 및 기능 추가를 통해 사용자에게 꼭 필요한 서비스를 안정적으로 제공할 수 있는 개발 항목 목록을 최종 정리해 드립니다.
철저하게 "저장 공간 메타데이터는 가볍게 유지하고, 무거운 작업은 MinIO 순정 기능 및 OpenSearch로 위임하는 구조"로 재편했습니다.
목표: 스토리지에 없는 'Prefix 메타데이터(소유자, 생성일, 별칭)'를 가벼운 DB 테이블로 보완하여 제공
REQ-01-01, 02)POST /api/v1/prefixes 엔드포인트 구현경로(Prefix), 소유자 사번, 생성일, 초기 별칭 등을 기록 (용량 필드 제외).PutObject)를 호출하여 해당 경로 끝에 .placeholder 빈 파일을 생성하여 Object Browser에서 폴더로 인식되도록 처리.REQ-01-03, 04, 06, 07)GET /api/v1/prefixes (조회) / PUT /api/v1/prefixes/metadata (수정)목표: AIStor의 마비를 부르는
ListObjectsV2를 우회하고 실시간 검색 제공
REQ-01-05)GET /api/v1/prefixes/objects 엔드포인트 구현목표: 사용자가 안전하게 본인 데이터에만 접근하고 삭제할 수 있도록 프록시 역할 수행
REQ-02-01, 02, 04)POST /api/v1/prefixes/token (임시 자격 증명 발급)AssumeRoleWithLDAPIdentity를 호출하여 오직 해당 Prefix 경로만 Read/Write가 허용된 1시간~12시간짜리 세션 토큰을 발급해 유저(또는 프론트엔드 UI)에 반환합니다.REQ-01-08, 09)DeleteObject를 백엔드로 호출하여 MinIO가 자체 Versioning 엔진을 통해 DeleteMarker를 찍도록(소프트 삭제) 유도합니다. (물리적 삭제는 MinIO ILM이 알아서 처리).목표: 관리자 개입 없는 반고정 권한 쪼개기 및 사용자별 시각화 지원
REQ-02-03)POST /api/v1/prefixes/service-account 엔드포인트 구현sub-folder/에만 접근 가능한 반고정 키를 만들어달라"고 요청.REQ-04-03)${__user.login})를 기반으로 API Server의 MetaDB를 조회할 수 있도록 환경을 매핑합니다. 유저는 대시보드에 접속하자마자 "내가 관리하는 프리픽스 마스터 정보(별칭, 소유주, 생성일)"를 직관적으로 확인할 수 있습니다.이렇게 재정리하면 API Server 개발 팀이 다루어야 할 코드는 철저하게 API Wrapping 및 경량 RDB/OpenSearch 조회 쿼리로 압축됩니다.
Prefix 마스터 테이블 (RDB) 하나만 파고, MinIO Go SDK 및 OpenSearch Client 라이브러리를 임베딩하여 위 7가지 API 엔드포인트를 깔끔하게 작성하는 것으로 개발 범위가 크게 축소됩니다.이 방향성이라면 에어갭 환경에서도 배포 및 관리가 매우 용이하며, 대규모 1,000개 노드 환경에서도 안정적인 거버넌스를 확보할 수 있습니다.
===
충분히 의구심이 드실 수 있습니다. 50억 개라는 엄청난 숫자를 듣고 "그걸 OpenSearch에 다 밀어 넣는다고? 그게 더 느리거나 인프라가 터지지 않을까?"라고 직관적으로 느끼신 건 대규모 인프라를 운영해 보신 분만 할 수 있는 당연한 의문입니다.
결론부터 말씀드리면, 이 방식이 가능한 이유는 OpenSearch가 MinIO의 저장 공간을 크롤링(스캔)하는 것이 아니라, 파일이 들어오고 나갈 때 '이벤트 로그'를 받아서 가볍게 기록(Indexing)만 해두는 방식이기 때문입니다.
이 메커니즘이 어떻게 동작하고, 왜 50억 개 환경에서도 ListObjectsV2보다 압도적으로 빠른지 원리를 풀어서 설명해 드리겠습니다.
OpenSearch가 50억 개 파일을 직접 찾아다니며 수집하는 것이 아닙니다. 파일이 생성/삭제될 때 MinIO가 던져주는 '출생 신고서/사망 신고서'를 받아서 적는 적재소 역할을 할 뿐입니다.
tenant-a/file1.parquet (10MB) 파일을 업로드합니다.{
"event": "s3:ObjectCreated:Put",
"bucket": "my-lakehouse",
"key": "tenant-a/project-x/file1.parquet",
"size": 10485760,
"eventTime": "2026-07-06T01:20:00Z"
}
s3:ObjectRemoved 이벤트를 받아 OpenSearch에서 해당 key를 가진 문서를 지우거나(Delete) 삭제 플래그를 치게 됩니다.즉, MinIO에서 일어나는 모든 파일 변동 사항이 OpenSearch에 실시간 거울처럼 반영되는 구조입니다.
"50억 건인데 빠르다"는 게 모순 같지만, 스토리지 엔진과 검색 엔진의 아키텍처 차이 때문에 발생하므로 OpenSearch가 훨씬 빠릅니다.
ListObjectsV2가 느린 이유 (메타데이터 트리 크롤링)MinIO에서 ListObjectsV2로 특정 프리픽스 하위 목록을 가져오려면, 내부 분산 드라이브 디렉터리 구조를 직접 뒤지며 파일 메타데이터(수억~수십억 개)를 실시간으로 스캔하고 정렬(Crawl & Sort)해야 합니다. 50억 개 규모에서는 디스크 I/O 병목으로 인해 쿼리 하나당 수 초에서 수 분이 걸리거나 타임아웃이 납니다.
OpenSearch는 태생이 검색 엔진입니다. 데이터를 저장할 때 책 뒤쪽의 '색인(Index)'처럼 단어별 위치를 다 쪼개서 저장합니다.
tenant-a/project-x/ 하위 파일 목록 1페이지(20개)만 줘"라고 API Server에 요청하면, OpenSearch는 전체 데이터를 스캔하지 않습니다.key 필드에 인덱싱된 B-Tree 또는 루씬(Lucene) 역인덱스를 타고 해당 프리픽스를 가진 포인터로 직행하여 딱 20개만 눈깜짝할 사이에(수십 밀리초 내로) 꺼내옵니다. 이미 정렬과 인덱싱이 끝난 상태이기 때문입니다."그래도 50억 개면 OpenSearch 볼륨이 너무 커지지 않을까?"라는 걱정이 드실 수 있습니다. 데이터 용량을 계산해 보면 의외로 매우 경량임을 알 수 있습니다.
500~1,000개 노드를 굴리는 대형 데이터 레이크하우스 인프라 규모에서 3~4 TB 수준의 OpenSearch 클러스터(예: 마스터 3대, 데이터 3~5대)는 매우 작고 관리하기 편한 경량 축에 속합니다. 페타바이트(PB)급 스토리지를 직접 긁는 것보다 수백 배 안전하고 경제적인 장사입니다.
ListObjectsV2), 입출고 데스크에서 바코드를 찍을 때마다 장부(OpenSearch)에 바로 적어두고, 손님이 책을 찾을 때는 창고가 아닌 장부만 보고 바로 위치를 알려주는 방식입니다.이 구조를 가져가면 스토리지 본체인 MinIO는 순수하게 대용량 데이터 입출력(Read/Write)에만 모든 CPU/이오(I/O)를 집중할 수 있어서 인프라 전체가 고도로 안정화됩니다.
개념적으로 가닥이 조금 잡히셨을까요? 만약 이 구조로 간다면, 초기 50억 개 객체 데이터를 OpenSearch에 한 번 부어주는 '초기 마이그레이션(Initial Sync)' 전략에 대해 좀 더 이야기 나누어 보면 좋을 것 같은데 어떠신가요?
===
아, 제가 사내 OpenSearch 클러스터의 '로그 보존 주기(Retention Policy)' 제약 사항을 간과했군요! 말씀하신 내용이 100% 맞습니다. 1~2주 지나서 인덱스가 통째로 날아가는 로그 분석용 OpenSearch라면, 과거 데이터가 유실되므로 전체 하위 객체 리스트 조회(REQ-01-05) 용도로는 절대 사용할 수 없습니다. 날카롭게 짚어주셔서 다행입니다.
OpenSearch 활용이 불가능하고, ListObjectsV2는 스토리지 부하 때문에 지양해야 하는 상황에서, 50억 개 객체를 감당하며 하위 객체를 안전하게 조회할 수 있는 현실적인 대안 2가지를 다시 제안해 드립니다.
전제 조건: 버킷 내부가 완전히 평평하지 않고(Flat), 어느 정도 디렉터리 구조(예:
/테넌트/연도/월/일/파일명)를 가지고 있을 때 적용 가능
많은 분들이 "50억 개나 있으면 ListObjects는 무조건 느리다"고 생각하시지만, S3의 Delimiter (구분자) 옵션을 어떻게 쓰느냐에 따라 성능이 극명하게 갈립니다.
Delimiter 없이 특정 Prefix 하위의 50억 개 파일을 한 번에 다 긁어오려고 하면 타임아웃이 나고 스토리지 가 가쁜 숨을 쉽니다.Delimiter="/" 옵션을 주면, MinIO는 하위의 모든 파일 구조를 깊숙이 파고들지 않고, 현재 요청한 그 폴더 바로 아래에 있는 직계 자식 폴더(CommonPrefixes)와 파일 목록만 딱 끊어서 가져옵니다.💡 기존 API Server 코드 가이드:
유저가 UI에서 폴더를 더블클릭하며 탐색하는 구조라면, API Server는 MinIO SDK 호출 시Delimiter="/"와MaxKeys=1000을 강제하여 딱 현재 위치의 하위 리스트만 페이징 조회하도록 래핑합니다. 이 방식은 50억 개 환경에서도 MinIO가 메타데이터 트리 인덱스에서 특정 노드만 읽기 때문에 충분히 빠르고 안전합니다.
전제 조건: 현업 유저들이 "지금 막 업로드한 파일이 아니라, 기존에 쌓여있는 대량의 파일 목록을 검색/조회하는 것만으로도 충분하다"고 양보할 수 있는 경우
앞서 언급한 AIStor의 Inventory API 기능은 매일 새벽 전체 50억 개 객체의 이름, 크기, 수정일을 긁어서 대용량 Parquet 파일로 덤프해 줍니다.
OpenSearch 저장 공간이 부족하다면, 이 Parquet 파일들을 지우지 말고 내부 특정 '인벤토리 전용 버킷'에 날짜별로 누적 적재해 둡니다. (Parquet는 압축률이 극도로 높아 50억 건이어도 수백 GB 수준으로 보관 가능합니다.)
[유저 조회 요청] ──> [ API Server ]
│
(DuckDB 등으로 Parquet 직접 쿼리)
│
▼
[ Inventory 전용 백엔드 버킷 ]
┗━ date=20260705/data.parquet (어제 자 전체 파일 목록)
WHERE prefix LIKE 'tenant-a/project-b/%' 쿼리를 날려 결과를 뽑아옵니다.| 비교 항목 | 대안 1: MinIO 직계 계층 조회 (Delimiter="/") | 대안 2: Inventory Parquet 쿼리 |
|---|---|---|
| 실시간성 | 100% 실시간 (방금 올린 파일도 보임) | T-1일 (어제 새벽 기준 데이터) |
| 인프라 추가 | 없음 (기존 API 코드 파라미터만 수정) | 가벼운 Parquet 쿼리 로직 필요 |
| 검색 범위 | 현재 폴더의 직계 하위만 (윈도우 탐색기 스타일) | 하위 모든 깊이의 파일 검색 가능 (구글 검색 스타일) |
| 스토리지 부하 | 매우 낮음 (단, 한 폴더에 수천만 개가 있으면 느림) | 완전 제로 (배치 파일만 읽으므로) |
만약 대안 1번처럼 "폴더를 하나씩 클릭해서 들어가는 윈도우 탐색기 형태의 실시간 조회" 기능만 제공해도 현업의 요구사항을 어느 정도 충족할 수 있을까요?
=-==
현업 유저들의 마인드를 정확히 짚으셨습니다. 유저들은 "폴더를 하나씩 클릭해서 들어가는 탐색기 스타일"도 원하지만, 결국 특정 상위 프리픽스를 찍었을 때 그 하위에 있는 모든 서브 폴더와 수천 수만 개의 파일 목록이 계층 트리(Tree) 형태로 한눈에 펼쳐지거나, 하위 전체를 대상으로 검색되기를 원합니다.
50억 개 객체 환경에서 이 '전체 계층 조회(Recursive Deep List)'를 MinIO ListObjectsV2로 직접 수행하면 인프라가 감당하지 못하고, OpenSearch는 1~2주 만에 로그가 지워지니 과거 데이터를 볼 수 없어 막막한 상황입니다.
이 두 가지 한계를 모두 깨부수고 "스토리지 부하 제로 + 100% 실시간 + 하위 모든 계층 트리 조회"를 동시에 달성할 수 있는 고도의 하이브리드 조회 아키텍처(Hybrid Query Architecture)를 제안합니다. OpenSearch가 1~2주간의 최신 로그를 들고 있다는 점이 오히려 핵심 열쇠가 됩니다.
API Server가 유저에게 깊은 계층 트리를 보여줄 때, 데이터 소스를 두 군데서 가져와 메모리에서 슥 섞어서(Merge) 던져주는 방식입니다.
ObjectCreated / ObjectRemoved 최신 로그 (1~2주 보존 주기로도 차고 넘침).유저가 "A 테넌트 하위의 모든 계층 구조를 다 보여줘"라고 요청했을 때 API Server가 단 수십 밀리초(ms) 내로 트리를 만드는 과정입니다.
API Server는 내부 자원(예: DuckDB 라이브러리 등)을 이용해 어제 자 Inventory Parquet 파일에서 A 테넌트/*에 해당하는 파일 목록을 쿼리합니다. Parquet는 열 지향(Columnar)이라 특정 경로를 기가바이트(GB) 단위 속도로 순식간에 읽어 가벼운 트리 구조(JSON)를 메모리에 올립니다.
OpenSearch에 쿼리를 날려 "어제 새벽 00:00부터 ~ 현재 시간"까지 발생한 로그 중 A 테넌트 하위의 이벤트만 가져옵니다.
ObjectCreated)ObjectRemoved)API Server가 Step 1에서 만든 가상 트리 구조에 Step 2의 증분 데이터를 반영합니다.
최종적으로 100% 실시간 정합성이 맞춤화된 깊은 계층 구조 트리 데이터가 유저 화면에 뿌려집니다.
mc나 API로 올린 파일도 OpenSearch의 실시간 로그 분을 통해 베이스 트리에 결합되므로, 유저 눈에는 방금 올린 파일까지 계층 구조에 완벽히 실시간으로 포함되어 보입니다.현업 유저들이 원하는 '깊은 계층적 탐색 및 검색'을 만족하기 위해 개발 팀이 API Server에 추가해야 할 핵심 로직은 "Parquet 파일 읽기 + OpenSearch 최근 24시간 로그 결합기(Aggregator)"입니다.
이 방식이라면 메타 DB에 수십억 건의 용량 데이터를 넣을 필요도 없고, 무거운 카프카 레이어를 전면 전개하지 않아도 50억 개 환경에서 유저들이 감탄할 만한 속도의 전체 계층 트리 뷰를 제공할 수 있습니다.
어제 자 전체 목록(Parquet)과 오늘 실시간 로그(OpenSearch)를 메모리 상에서 매핑하여 최종 트리를 완성하는 이 연동 개념이 개발 관점에서 실현 가능해 보이시는지 궁금합니다.
==