Ken
Langfuse Enterprise 기능을 직접 검증하며 확인한 데이터 마스킹, RBAC·SCIM, 보존 정책, 프롬프트 거버넌스, 데이터 내보내기의 운영 원리와 주의점
Langfuse Enterprise 기능 검증
LLM 관측성 도입은 흔히 “트레이스를 보낸다”에서 시작합니다. 하지만 운영 환경에서 더 어려운 질문은 그 다음에 나옵니다. 누가 특정 프로젝트의 프롬프트를 바꿀 수 있을까요? 개인정보가 섞인 입력은 저장소에 남지 않는다고 어떻게 증명할 수 있을까요? 보존 기간이 끝난 데이터를 지우기 전에 분석용 사본은 어떻게 남길 수 있을까요?
이번 글에서는 Langfuse v3.197.1을 ClickHouse 25.11.2.24 위에 셀프호스팅하고, Enterprise 기능을 실제로 끝까지 실행한 기록을 바탕으로 이 질문에 답합니다. 단순 기능 목록이 아니라, 운영자가 데이터를 통제하고 그 통제를 데이터로 검증하는 방식에 초점을 둡니다.
이 글의 수치와 콘솔 결과는 2026-07-26에 수행한 실습 환경의 관찰값입니다. 특히 API 허용값과 저장소 스키마는 실행 이미지 버전에 따라 달라질 수 있으므로, 운영 적용 전에는 반드시 현재 실행 중인 버전에서 재검증하셔야 합니다.
한 장으로 보는 구조
Langfuse의 셀프호스팅 아키텍처는 데이터 성격에 따라 저장소를 나눕니다. 사용자·조직·프롬프트 같은 트랜잭션 성 데이터는 Postgres가, 트레이스·Observation·Score처럼 대량으로 쌓여 분석되는 이벤트 데이터는 ClickHouse가 맡습니다. Langfuse 공식 문서도 ClickHouse를 이 세 엔터티의 주 OLAP 저장소로 설명합니다. ClickHouse 운영 문서
실습 환경은 Docker Desktop 위에 Postgres 17, Redis 7, MinIO, Langfuse web/worker, ClickHouse를 올렸습니다. LANGFUSE_INIT_* 환경 변수로 첫 조직·프로젝트·사용자·API 키를 헤드리스 초기화했기 때문에, UI 클릭 없이 곧바로 데이터를 전송할 수 있었습니다. 워밍된 이미지 캐시에서는 약 10초 만에 헬스 엔드포인트가 {"status":"OK","version":"3.197.1"}을 반환했습니다.
관측성 데이터는 분석 가능한 원천
Python SDK로 오프라인 시뮬레이션 트레이스 40개를 전송했습니다. 각 트레이스는 support-request → retrieve-context → answer-generation → 선택적 self-check로 이어지는 중첩 Observation 트리이며, 사용자·세션·고객 등급·점수·토큰 사용량을 포함합니다.
이 데이터를 ClickHouse에서 바로 집계하면 UI 대시보드의 요약을 넘어, 제품 및 비용 의사결정에 맞춘 질문을 만들 수 있습니다.
질문 | 실측 결과 예시 | 의미 |
모델별 비용 | Claude Sonnet $0.077313, GPT-4o $0.058698, GPT-4o mini $0.003565 | 모델 선택의 비용 차이를 확인합니다. |
모델별 p95 지연 | Sonnet 1,599.55ms, GPT-4o 1,444.30ms, GPT-4o mini 559.40ms | 평균보다 꼬리 지연을 기준으로 SLO를 검토합니다. |
모델별 오류율 | GPT-4o 13.33%, Sonnet 10%, GPT-4o mini 4% | 비용·지연과 오류를 함께 봅니다. |
품질 신호 | thumbs-up 75%, 평균 grounding 0.788 | 사용자의 반응과 자동 평가를 함께 해석합니다. |
여기서 중요한 전제는 ClickHouse의 테이블 엔진입니다. traces, observations, scores는 모두 ReplacingMergeTree였고, 월별 파티션을 사용했습니다. 관찰 직후 traces는 41행이었지만, FINAL과 is_deleted = 0 조건을 적용한 활성·중복 제거 결과는 40행이었습니다.
-- 원시 행 수는 수렴 중인 중복 버전을 포함할 수 있습니다.
SELECT count() AS raw_rows
FROM traces;
-- 감사·분석의 기준은 활성 레코드의 최종 상태입니다.
SELECT count() AS deduped_active
FROM traces FINAL
WHERE is_deleted = 0;ReplacingMergeTree는 적절한 엔진 선택이지만, “방금 INSERT한 행 수”와 “업무적으로 참인 행 수”가 일시적으로 다를 수 있다는 뜻이기도 합니다. 직접 SQL을 운영 분석에 사용할 때는 이 차이를 쿼리 규약으로 고정해야 합니다. 또한 Langfuse는 내부 ClickHouse 스키마를 안정적인 공개 API 계약으로 보지 않으므로, 직접 조회 쿼리는 업그레이드 때마다 검증하고, 운영 연동에는 Public API·SDK·Blob Storage Export를 우선하라고 권고합니다. 직접 ClickHouse 조회 시 주의사항
Enterprise의 출발점: 관측성에 권한과 책임의 경계를 추가하기
Enterprise 오버레이에 라이선스 키와 관리 API 키를 주입한 뒤, /api/admin/organizations가 HTTP 200을 반환하는 것으로 활성화를 확인했습니다. 이후의 실습은 서로 분리된 기능처럼 보이지만, 실제로는 다음 질문을 해결하는 하나의 거버넌스 흐름입니다.
1. RBAC와 SCIM: 조직 역할과 프로젝트 역할을 분리합니다
실습에서는 Instance Management API로 조직과 프로젝트를 만들고, SCIM으로 Alice와 Bob을 프로비저닝했습니다. Alice는 조직 MEMBER, Bob은 조직 VIEWER였지만, acme-prod 프로젝트에만 Bob을 ADMIN으로 부여했습니다.
사용자 | 조직 역할 | acme-prod 프로젝트 역할 | 결과 |
Alice | MEMBER | 없음 | 조직 범위의 일반 구성원 권한 |
Bob | VIEWER | ADMIN | 조직 전체는 조회자이지만 해당 프로젝트에서는 관리자 |
이는 예외 권한을 “공유 관리자 계정”이나 수작업으로 처리하지 않고, 프로젝트 경계 안에 명시적으로 둔다는 점에서 중요합니다. Langfuse RBAC는 기본적으로 조직 역할을 부여하되, 더 세밀한 제어가 필요하면 프로젝트 역할을 추가할 수 있도록 설계돼 있습니다. Langfuse RBAC 문서
SCIM을 통해 이 구성을 IdP, Terraform, CI 파이프라인과 연결하면 입·퇴사나 팀 이동을 권한 변경 티켓이 아니라 선언형 프로비저닝으로 다룰 수 있습니다. 다만 실습의 API 키와 사용자 식별자는 예시일 뿐이며, 실제 환경에서는 SCIM 토큰·관리 API 키의 보관, 키 회전, 최소 권한을 별도 운영 기준으로 두셔야 합니다.
2. 데이터 보존과 감사 로그: 삭제는 정책이고, 변경은 증거여야 합니다
llm-observability 프로젝트에 14일 보존 정책을 설정했습니다. 이 값은 단지 UI의 숫자가 아닙니다. Langfuse는 매일 밤 보존 기간을 넘긴 Trace, Observation, Score, Media Asset을 선택해 삭제합니다. 복구할 수 없는 삭제이므로, 장기 분석이 필요하다면 삭제 전에 Blob Storage Export로 내보내는 흐름을 설계해야 합니다. Data Retention 문서
동시에 Postgres의 audit_logs에는 바로 앞 단계에서 수행한 조직 생성, API 키 발급, 조직 멤버십 생성이 순서대로 남았습니다. 즉, RBAC는 “현재 누가 할 수 있는가”를, 감사 로그는 “누가 무엇을 했는가”를 담당합니다. 두 기능을 분리해 생각하면 운영 사고 시점의 재현 가능성이 크게 떨어집니다.
보존 기간은 보안 설정이면서 동시에 데이터 제품의 계약입니다. 데이터셋이 트레이스를 참조하더라도, 트레이스가 보존 기간에 따라 삭제되면 그 참조는 더 이상 유효하지 않을 수 있습니다. 실험 재현성까지 고려해 기간을 정하셔야 합니다.
핵심 사례: 서버 측 마스킹을 ClickHouse에서 증명하기
이번 실습의 핵심은 “PII를 마스킹한다”는 선언을 SQL로 검증 가능한 명제로 바꾼 점입니다.
마스킹 sidecar는 Worker가 OTEL 이벤트를 처리할 때 호출됩니다. 카드 번호, 이메일, 한국 주민등록번호 형식, API 키가 포함된 트레이스 12개를 전송했고, 콜백은 원문을 [REDACTED_CC], [REDACTED_EMAIL], [REDACTED_KR_RRN], [REDACTED_API_KEY]로 교체했습니다. 결과는 다음과 같았습니다.
검증 항목 | 결과 |
원문 시크릿 4종의 observations 누출 건수 | 0 |
원문 시크릿 4종의 traces 누출 건수 | 0 |
[REDACTED_*]를 포함한 행 수 | 24 |
sidecar가 기록한 총 치환 횟수 | 84 |
마스킹 후 표본의 입력은 my resident number is [REDACTED_KR_RRN]로, 출력은 number [REDACTED_KR_RRN] on file로 남았습니다. 즉 분석에 필요한 문맥과 사건 구조는 보존하면서 민감값만 제거했습니다. 이 접근의 강점은 “마스킹 서비스 로그가 그렇게 말한다”에 그치지 않고, 최종 분석 저장소에서 원문 부재와 대체값 존재를 동시에 검사한다는 데 있습니다. 배포 파이프라인의 회귀 테스트나 정기 컴플라이언스 점검으로 발전시키기 좋은 패턴입니다.
다만 보장 범위를 정확히 표현해야 합니다. Langfuse의 서버 측 ingestion masking은 Enterprise 기능이며 Worker의 콜백에서 처리되고, OTEL 엔드포인트(/api/public/otel)로 들어온 이벤트에 적용됩니다. Python SDK v3+와 TypeScript SDK v4+가 이 경로에 해당하며, 레거시 ingestion 엔드포인트에는 적용되지 않습니다. 또한 이벤트는 Worker가 콜백을 호출하기 전에 이벤트 Blob Storage 버킷에 기록됩니다. 따라서 ClickHouse와 후속 Langfuse 뷰에 원문을 남기지 않는 통제이지, 민감값이 애플리케이션 경계를 절대 벗어나지 않는다는 보장은 아닙니다. 그 요구에는 SDK에서 먼저 제거하는 클라이언트 측 마스킹을 병행해야 합니다. Server-side Data Masking 문서
콜백 실패 시 정책도 제품 보안의 일부입니다. 이번 실습은 FAIL_CLOSED=true로 설정해, 콜백 오류 시 이벤트를 저장하지 않고 드롭했습니다. 반대로 fail-open은 관측성 연속성에는 유리하지만, 콜백 장애 시 원문이 처리될 수 있습니다. 어떤 쪽이 맞는지는 기술 옵션이 아니라 데이터 분류와 위험 수용 수준에 대한 조직의 결정입니다.
프롬프트 거버넌스: production은 태그가 아니라 움직이는 배포 포인터
프롬프트 v1을 production, latest 라벨과 함께 만들고, 더 엄격한 v2를 생성하면서 동일 라벨을 붙였습니다. 이후 Postgres를 조회하니 v1의 라벨은 {}가 되었고 v2만 {production,latest}를 가졌습니다.
버전 | 라벨 | 해석 |
v1 | {} | production이 v2로 이동하면서 비었습니다. |
v2 | {production, latest} | 현재 배포 대상입니다. |
이 결과는 배포 라벨을 누적 태그로 오해하면 안 된다는 점을 보여 줍니다. production은 특정 버전을 가리키는 고유 포인터이며, 재지정은 곧 배포 대상 변경입니다. 그래서 Protected Prompt Label은 특히 중요합니다. 실습에서는 UI에서 production을 보호해 Owner/Admin 외에는 재지정·삭제할 수 없도록 구성했습니다. 앞서 Bob에게 프로젝트 단위 ADMIN을 준 사례와 결합하면, 권한 모델이 실제 배포 통제를 어떻게 뒷받침하는지 분명해집니다.
그리고 프롬프트는 ClickHouse가 아닌 Postgres에 있습니다. 이는 “모든 Langfuse 데이터가 분석 DB에 있다”는 흔한 오해를 바로잡습니다. 트레이스 분석은 OLAP, 프롬프트의 버전·라벨 관리와 감사는 OLTP라는 분리가 각 데이터의 접근 패턴과 일관성 요구에 맞습니다.
보존 전에 반출하기: Parquet와 ClickHouse s3() 라운드트립
장기 보관 또는 외부 분석을 위해서는 archive-then-delete가 자연스러운 운영 모델입니다. 실습에서는 ClickHouse가 활성 트레이스를 MinIO에 Parquet로 쓰고, 같은 파일을 s3() 테이블 함수로 다시 읽었습니다.
활성 트레이스 수와 다시 읽은 Parquet 행 수는 모두 93이었습니다. 파일 크기는 4.99 KiB였고, id, timestamp, input, output 등의 스키마도 추론됐습니다. 이 검증은 “파일이 생성됐다”보다 강합니다. 내보낸 데이터가 적어도 행 수 기준으로 다시 읽을 수 있음을 확인하기 때문입니다.
여기에는 중요한 버전 함정이 있었습니다. 실행 이미지인 Langfuse v3.197.1의 blob-storage integration API는 fileType=PARQUET에 HTTP 400을 반환했고, 허용값은 JSON, CSV, JSONL뿐이었습니다. 반면 ClickHouse 자체의 Parquet 쓰기·읽기는 정상 동작했습니다. 즉 공개 OpenAPI 문서나 최신 Cloud 기능이, 현재 고정한 셀프호스팅 이미지의 API 표면보다 앞설 수 있습니다.
운영 원칙은 간단합니다.
- 공개 문서로 기능 가능성을 확인합니다.
- 고정한 컨테이너 이미지의 OpenAPI 또는 실제 요청으로 허용값을 확인합니다.
- 내보내기 후
count, 스키마, 표본값, 실패 재시도까지 검증합니다. - 검증된 포맷과 경로만 보존 정책의 선행 단계로 자동화합니다.
현재 공식 ClickHouse 문서는 Langfuse v3에 ClickHouse 24.3 이상을 지원한다고 명시합니다. 다만 이 실습에서 확인한 Parquet 관련 동작과 API 제약은 v3.197.1 + ClickHouse 25.11.2.24의 관찰값으로 한정해 다루는 편이 안전합니다. Langfuse ClickHouse 버전·운영 가이드
운영에서 놓치기 쉬운 세 가지
오버레이는 누적되지 않을 수 있습니다
실습에서는 마스킹과 인스턴스 거버넌스를 각각 별도 Compose overlay로 구성했습니다. 뒤에서 거버넌스 overlay로 web과 worker를 재생성하자, 마스킹 sidecar가 orphan으로 표시되고 마스킹 연결도 사라졌습니다. 기능별 overlay가 독립적이라면, 동시에 필요한 환경 변수와 서비스를 하나의 배포 정의로 합성해야 합니다. “기능을 각각 테스트했다”와 “기능들이 동시에 켜진 운영 상태”는 다릅니다.
ClickHouse 직접 조회는 읽기 전용 분석으로 제한합니다
ClickHouse는 감사·대시보드·임시 분석에 매우 강력하지만, 내부 테이블에 직접 쓰는 것은 피해야 합니다. 전용 읽기 전용 사용자, 프로젝트·시간 필터, 별도 읽기 컴퓨트 또는 엔드포인트를 사용해 ingestion 경로와 경합하지 않게 구성하는 것이 좋습니다. 이는 Langfuse의 공식 권고이기도 합니다. 직접 조회 권고사항
로컬 재현성도 운영 품질의 일부입니다
macOS에서 python 대신 python3만 있는 문제로 lab-08 드라이버를 수정했습니다. .venv/bin/python을 우선 탐색하도록 바꾸자 별도 활성화 없이 재현됐습니다. 사소해 보이지만, 보안 검증 시나리오가 특정 개발자 머신에서만 실행되는 상태는 곧 운영 리스크입니다. 실행 파일 탐색, 버전 출력, 사전 점검을 자동화해야 합니다.
결론: Enterprise의 가치는 기능 수가 아니라 증명 가능한 통제에 있습니다
이번 실습에서 가장 인상적인 결과는 마스킹 자체가 아니라, ClickHouse SQL로 원문 누출이 0건임을 확인한 과정이었습니다. RBAC는 권한을 프로젝트까지 좁혔고, SCIM은 그 권한을 자동화 가능한 라이프사이클로 옮겼습니다. Protected Label은 프롬프트 배포 포인터를 지켰으며, 보존 정책과 Parquet 라운드트립은 삭제와 장기 보관을 하나의 데이터 수명주기로 연결했습니다.
셀프호스팅 Langfuse는 단지 추적 UI를 내 컴퓨터에 띄우는 일이 아닙니다. ClickHouse를 중심으로 관측성, 비용, 품질, 보안, 컴플라이언스를 함께 다루는 데이터 시스템을 운영하는 일입니다. 그리고 Enterprise 기능의 진짜 가치는 “설정했다”가 아니라 권한·저장·삭제·반출의 결과를 재현 가능하게 검사할 수 있다는 데 있습니다.
재현을 위한 최소 순서
실제 운영 환경에서는 위 순서에 더해, 마스킹 회귀 SQL, 보존 전 export 완전성 검사, 버전별 API 계약 검사, overlay 합성 검증을 CI/CD의 배포 게이트로 두는 것을 권장드립니다.