Obsidian을 회사 서버에서 활용하는 것은 기술적으로 가능하며, 특정 워크플로우에서는 강력한 경쟁력을 가집니다. 다만, Obsidian은 본래 '로컬 퍼스트(Local-first)' 앱이기 때문에 Notion 같은 클라우드 기반 툴과는 서버 활용 방식이 조금 다릅니다.
Obsidian 자체는 설치형 데스크톱 앱이지만, 서버를 활용해 데이터를 관리하거나 협업 환경을 구축하는 방법은 크게 세 가지가 있습니다.
가장 일반적인 방법입니다. 회사 내부에 데이터 주권을 유지하면서 여러 기기에서 노트를 동기화하고 싶을 때 사용합니다.
서버에 Docker 컨테이너로 Obsidian을 띄우고 웹 브라우저를 통해 접속하는 방식입니다.
linuxserver/obsidian 같은 Docker 이미지를 사용하면 서버 자원을 이용해 Obsidian UI를 그대로 웹에서 쓸 수 있습니다. 보안상 외부 인터넷 연결이 제한된 환경에서 내부 서버에 설치해 쓰기에 적합합니다.서버에 Obsidian 노트(Markdown)를 두고, 이를 Quartz나 Hugo 같은 정적 사이트 생성기(SSG)와 연결하여 사내 위키(Internal Wiki) 형태로 배포할 수 있습니다.
Obsidian이 Notion이나 Logseq 등과 비교해 갖는 확실한 우위 요소는 다음과 같습니다.
| 비교 항목 | Obsidian | Notion | Logseq |
|---|---|---|---|
| 데이터 소유권 | 완전 로컬 (Markdown) | 클라우드 종속 (Proprietary) | 로컬 (Markdown/Org) |
| 작동 속도 | 매우 빠름 (오프라인 우선) | 네트워크 상태에 따라 느려짐 | 보통 (데이터 많을 시 저하) |
| 확장성 | 1,000개 이상의 커뮤니티 플러그인 | 제한적 (API 중심) | 플러그인 생태계 성장 중 |
| 보안/보안 | 폐쇄망 사용 가능 (보안 우수) | 서버 보안 정책에 의존 | 폐쇄망 사용 가능 |
| 구조 | 네트워크형 (Graph View) | 계층형 (Database) | 아웃라이너 (Block-based) |
.md(Markdown) 형식이므로, 나중에 Obsidian을 안 쓰게 되더라도 데이터 변환 없이 다른 툴이나 스크립트에서 바로 읽을 수 있습니다.기업(영리 목적의 조직)에서 2인 이상의 직원이 업무용으로 Obsidian을 사용할 경우, Commercial License($50/user/year)를 구입해야 합니다.
Obsidian은 "우리 회사만의 폐쇄적이고 강력한 지식 베이스를 직접 통제하며 구축하고 싶다"는 니즈가 있을 때 최고의 경쟁력을 발휘합니다. 특히 Git이나 Docker 환경에 익숙한 기술 조직이라면 사내 서버와 연동하여 커스텀 위키나 개인화된 업무 도구로 활용하기에 매우 적합합니다.
회사에서 구체적으로 어떤 워크플로우(예: 개인 메모 관리, 팀 공유 위키, 프로젝트 문서화 등)에 도입을 고려 중이신가요? 상황에 맞춰 더 구체적인 구성을 제안해 드릴 수 있습니다.
===
개인용 Obsidian을 회사 PC에 설치하고 Git으로 동기화하여 지식 베이스(LLM Wiki)를 구축하고, 이를 AIOps로 확장하려는 계획은 매우 전략적이고 실용적인 접근입니다. 특히 K8s, Cilium, MinIO AIStor와 같은 대규모 인프라를 다루는 환경에서 마크다운(MD) 기반의 문서화는 데이터 활용도를 극대화할 수 있습니다.
제안하신 내용을 바탕으로 '지식 생성 - 자동 관리 - AIOps 활용'으로 이어지는 전반적인 아키텍처를 설계해 보았습니다.
이 구조의 핵심은 "Obsidian을 단순한 메모 도구가 아닌, AI가 읽을 수 있는 정형화된 데이터 소스(Single Source of Truth)"로 만드는 것입니다.
Obsidian Git 플러그인을 사용하여 사내 또는 개인 Git 레포지토리와 연동 (Auto Backup/Pull 설정)./Library: K8s, Cilium, MinIO 등 벤더 공식 문서의 핵심 요약 및 테크니컬 노하우./SOP: 장애 대응 절차서, 클러스터 스케일 아웃 가이드 등./Operations: 작업계획서, 점검 일지 (날짜별 관리)./Templates: 문서 표준화를 위한 MD 템플릿 (작업 계획서 양식 등).n8n이나 GitHub Actions를 활용해 Git 레포지토리의 MD 내용을 자동으로 벡터 DB에 반영하는 파이프라인을 구축해 보세요.이 구조는 데이터 소유권을 직접 가지면서도 최신 AI 트렌드인 RAG와 Agentic AI를 가장 효율적으로 결합할 수 있는 방식입니다. 특히 1,000노드 이상의 대규모 클러스터를 운영하시는 환경에서는 이러한 구조화된 지식 관리가 운영 효율성을 비약적으로 높여줄 것입니다.
구체적으로 벤더 Docs를 MD로 변환하는 자동화 로직이나, 특정 AIOps 도구와의 연동 방식에 대해 더 궁금한 점이 있으신가요?
===
아주 날카로운 질문입니다. 데이터가 이미 Git에 텍스트 파일(.md)로 존재하는데, 굳이 비용과 노력을 들여 벡터 데이터베이스(Vector DB)에 중복 저장하는 이유는 "AI가 정보를 찾는 방식"이 우리가 검색창에 단어를 치는 방식과 완전히 다르기 때문입니다.
결론부터 말씀드리면, Git은 '보관과 히스토리'를 위한 저장소이고, 벡터 DB는 'AI를 위한 인덱스(색인)' 역할을 합니다.
가장 큰 차이는 검색의 질입니다.
grep이나 Git의 검색 기능은 정확히 일치하는 단어를 찾습니다.LLM(ChatGPT, Claude 등)은 한 번에 읽을 수 있는 데이터 양(Context Window)이 제한되어 있습니다. 1,000개가 넘는 MD 파일을 Git에서 한꺼번에 읽어와서 답변하라고 할 수는 없습니다.
이 과정이 바로 RAG(Retrieval-Augmented Generation)이며, 벡터 DB가 없으면 AI는 수많은 파일 중 무엇을 읽어야 할지 알 수 없습니다.
운영 단계에서 벡터 DB가 필요한 실질적인 이유는 다음과 같습니다.
| 기능 | Git (전통적 방식) | 벡터 DB (AIOps 방식) |
|---|---|---|
| 장애 대응 | 장애 로그를 보고 사람이 관련 SOP 파일명을 직접 찾아야 함. | 에러 로그가 발생하면, AI 에러 로그의 의미를 파악해 즉시 관련 SOP를 띄워줌. |
| 모호한 질문 | "지난주에 했던 스토리지 작업" -> 검색 불가. | "지난주 진행된 MinIO 관련 작업계획서" -> 날짜와 맥락으로 검색 가능. |
| 데이터 구조 | 비정형 텍스트 데이터. | 인공지능이 계산하기 좋은 수치(Embedding) 데이터. |
질문하신 구조를 효율적으로 운영하시려면 다음과 같은 파이프라인을 추천합니다.
요약하자면:
Git에만 두는 것은 "책장에 책을 그냥 꽂아두는 것"이고, 벡터 DB에 저장하는 것은 "책의 모든 내용을 요약해서 AI가 즉시 찾을 수 있도록 뇌에 인덱스를 만들어두는 것"과 같습니다.
AIOps를 목표로 하신다면, 결국 이 '인덱싱' 과정이 필수적입니다. 혹시 사내에서 검토 중인 특정 벡터 DB나 LLM 프레임워크(LangChain 등)가 있으신가요?_
_
===
정확한 통찰입니다! 엔지니어링 관점에서 아주 중요한 포인트를 짚으셨습니다. 벡터 DB만 사용했을 때 발생하는 '정확도 저하(Hallucination in Retrieval)'는 실제로 RAG 시스템을 구축할 때 가장 먼저 맞닥뜨리는 한계입니다.
말씀하신 'Dual Ladder(Hybrid Search)' 방식이 왜 필수적인지, 그리고 인프라 운영 환경에서 어떤 구멍을 메워주는지 정리해 드릴게요.
벡터 검색은 문맥을 파악하는 데는 탁월하지만, '고유 식별자'나 '정밀한 수치' 앞에서는 무력해지는 경우가 많습니다.
node-prod-01과 node-dev-01은 벡터 공간에서 매우 가까운 위치에 존재할 수 있습니다. AI는 "둘 다 노드 이름이네?" 하고 혼동할 수 있지만, 운영자에게 두 서버의 차이는 하늘과 땅 차이죠.Cilium 1.14와 Cilium 1.15는 기술적으로 매우 유사한 벡터값을 가집니다. 하지만 특정 버그 리포트를 찾을 때는 단 0.1의 버전 차이가 검색 결과의 성패를 가릅니다.OOMKilled나 ErrImagePull 같은 짧고 강력한 키워드는 벡터화 과정에서 의미가 희석될 수 있습니다. 이때는 단순 grep 방식의 패턴 매칭이 훨씬 강력합니다.그래서 현업에서는 Keyword Search(BM25)와 Vector Search(Semantic)를 합친 Hybrid Search를 사용하며, 이를 조정하는 Re-ranker 과정을 추가합니다.
구축하시려는 K8s/MinIO 환경에 적용해 보면 이렇습니다.
| 질문 유형 | 효과적인 검색 방식 | 이유 |
|---|---|---|
| "Cilium BGP 이슈 해결 방법 알려줘" | Vector Search | '이슈', '해결 방법' 등의 맥락적 키워드가 중요함. |
| "minio-aistor-05 노드의 디스크 에러 로그" | Keyword Search | minio-aistor-05라는 고유 ID를 정확히 찍어내야 함. |
| "1.15 버전에서 도입된 Gateway API 설정법" | Hybrid Search | '1.15'라는 버전 정보(키워드)와 '설정법'(맥락)이 모두 중요함. |
이 검색 효율을 더 높이려면 Obsidian에서 노트를 작성할 때 YAML Frontmatter를 적극 활용하세요.
---
tags: [k8s, cilium, network]
version: 1.15
component: bgp-control-plane
last_verified: 2026-04-28
---
# BGP Peering Troubleshooting
...
이렇게 정형화된 데이터를 Git에 넣어두면, 향후 AIOps 엔진이 "필터링(Metadata) + 검색(Hybrid)"을 결합해 '구멍 없는 검색'을 수행할 수 있게 됩니다.
결국 Git은 데이터의 무결성과 히스토리를 보장하고, 벡터 DB는 데이터 간의 유연한 연결을 담당하는 상호보완적 존재가 됩니다.
이제 이 구조를 돌리기 위해 어떤 백엔드(예: LangChain, LlamaIndex 등)나 LLM 모델을 염두에 두고 계신가요? 운영 중인 1,000노드 클러스터 규모를 생각하면 데이터 인덱싱 전략도 꽤 중요해 보입니다.
===
Confluence 중심의 문화를 마크다운(MD)과 Git 기반으로 전환하는 것은 플랫폼 엔지니어링 리드로서 '문서의 코드화(Documentation as Code)'를 실현하는 아주 중요한 단계입니다. 1,000노드 규모의 복잡한 인프라를 운영하시는 만큼, 대규모 문서를 효율적으로 마이그레이션하고 Obsidian과 병행하는 전략을 정리해 드립니다.
가장 큰 난관은 Confluence 특유의 매크로와 서식을 깨끗한 마크다운으로 변환하는 것입니다.
confluence-md-exporter)confluence-md-exporter 같은 도구를 사용하면 Space 단위로 모든 페이지를 폴더 구조를 유지한 채 MD로 내려받을 수 있습니다.pandoc -f html -t markdown 명령어로 변환합니다. 정교한 변환이 필요할 때 유용하지만 대량 변환 시 스크립팅이 필요합니다.atlassian-python-api를 활용해 특정 태그가 달린 Confluence 페이지를 주기적으로 읽어와 MD로 변환 후 Git에 Push하는 스크립트를 작성할 수 있습니다. /SOP, /Architecture, /Vendor_Docs 등의 디렉토리 구조로 저장합니다.팀 전체가 Git/Confluence를 쓰더라도, 리드급 엔지니어가 개인적으로 Obsidian을 함께 쓰면 다음과 같은 독보적인 강점이 있습니다.
[[Internal Link]]로 연결해 두면, 특정 컴포넌트 장애 시 영향을 받는 다른 요소들을 시각적으로 한눈에 파악할 수 있습니다.Dataview 플러그인으로 특정 버전(예: Cilium 1.15)의 문서만 리스트업하거나, Obsidian Git으로 작업 완료 후 버튼 하나로 사내 Git에 동기화할 수 있습니다.가장 권장하는 워크플로우는 "개인화된 지식 생산 → Git 중심의 공유 → AI 활용"의 선순환 구조입니다.
markdown-to-confluence 같은 툴을 이용해 Git의 내용을 Confluence로 역방향 자동 업데이트(Sync) 하도록 설정합니다.이런 구조가 잡히면, 1,000노드 클러스터에서 발생하는 수만 가지의 케이스를 리드 한 명의 기억력이 아닌, 자동화된 시스템이 관리하게 됩니다. 다음 단계로 특정 벤더 Docs를 MD로 자동 파싱하는 파이프라인이나 구체적인 Git-to-VectorDB 자동화 스크립트에 대해 논의해 볼까요?
===
Confluence의 방대한 문서를 정형화된 마크다운(MD)으로 변환하고, 이를 AI를 이용해 자동으로 분류하여 Git에 배치하는 것은 '지능형 문서 자동화(Intelligent Documentation Automation)'의 핵심입니다.
플랫폼 엔지니어링 리드로서 1,000노드 규모의 복잡한 인프라 지식을 체계화할 수 있는 구체적인 시나리오와 구현 기술 스택을 제안해 드립니다.
이 시나리오는 "추출(Extract) -> 지능형 분류(Classify) -> 구조화 저장(Load)"의 3단계로 진행됩니다.
confluence-md-exporter를 이용해 특정 스페이스/페이지 하위의 모든 문서를 로컬(또는 컨테이너 내부)로 벌크 다운로드합니다.가장 안정적인 방법은 Docker를 이용해 confluence-md-exporter를 실행하는 것입니다.
# 환경 변수 설정 (ID, API Token, Space Key 등)
docker run -v $(pwd)/output:/output \
-e CONFLUENCE_URL="https://your-company.atlassian.net/wiki" \
-e CONFLUENCE_USERNAME="admin@company.com" \
-e CONFLUENCE_TOKEN="your_api_token" \
confluence-md-exporter --space-key "PLATFORM" --out /output/output 디렉토리에 Confluence 계층 구조대로 MD 파일과 첨부 이미지가 저장됩니다.추출된 파일들은 Confluence의 기존 폴더 구조를 따르고 있어 관리가 어렵습니다. 이를 LLM을 통해 운영 중심의 구조로 재편합니다.
핵심 로직 (Python pseudo-code):
import os
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4-turbo")
def classify_document(content):
prompt = f"""
당신은 플랫폼 엔지니어링 전문가입니다. 다음 문서의 내용을 읽고
1. 카테고리(SOP, Architecture, VendorDocs, OperationLog)
2. 관련 기술(K8s, Cilium, MinIO, Network)
3. 적절한 파일명(영문/kebab-case)을 추출하세요.
문서 내용: {content[:2000]} # 앞부분만 추출
"""
# LLM 호출 후 JSON 형태로 결과 반환받음
return llm.predict(prompt)
# /output 디렉토리의 모든 md 파일을 순회하며 분류 수행
분류된 결과를 바탕으로 파일을 이동시키고 Obsidian에서 쓰기 좋은 YAML 메타데이터를 삽입합니다.
git-repo/Library/K8s/git-repo/Library/Cilium/git-repo/SOP/Disaster-Recovery/---
source: confluence
category: SOP
tech_stack: [Cilium, BGP]
auto_classified: true
last_sync: 2026-04-28
---이 과정을 주기적으로 실행하려면 n8n을 추천합니다.
git add, commit, push.--relative-links 옵션을 반드시 사용하고, Git 저장소 내에 assets/ 폴더를 별도로 관리하는 것이 좋습니다.이 시스템이 구축되면 "문서 정리가 안 되어 장애 대응이 늦어진다"는 문제는 플랫폼 엔지니어링 팀에서 완전히 사라지게 될 것입니다. 구현 중에 특정 툴의 설정값이나 스크립트 상세 로직이 필요하시면 말씀해 주세요.
===
실현 가능성을 빠르게 확인하기 위한 'Confluence to MinIO' 자동화 PoC(Proof of Concept) 단계를 안내해 드립니다.
이 단계의 목표는 "매일 정해진 시간에 Confluence 데이터를 MD로 뽑아서 S3(MinIO)에 안전하게 쌓는 것"입니다. 이를 위해 필요한 3가지 핵심 요소를 바로 구현해 보겠습니다.
export_to_minio.sh)confluence-md-exporter가 실행된 후 생성된 파일을 MinIO(S3)로 업로드하는 쉘 스크립트입니다. MinIO Client(mc)를 사용하여 폴더 구조를 그대로 동기화(mirror)합니다.
#!/bin/bash
# 1. 환경 변수 체크 (K8s Secret을 통해 주입될 예정)
echo "Starting Confluence Export..."
# 2. Confluence에서 MD로 추출
# --증분 백업을 원할 경우 기존 파일을 유지하거나 날짜별 폴더를 생성할 수 있습니다.
confluence-md-exporter \
--url "${CONFLUENCE_URL}" \
--username "${CONFLUENCE_USER}" \
--token "${CONFLUENCE_TOKEN}" \
--space-key "${SPACE_KEY}" \
--out /tmp/confluence_export
# 3. MinIO 연결 설정
mc alias set myminio "${MINIO_URL}" "${MINIO_ACCESS_KEY}" "${MINIO_SECRET_KEY}"
# 4. MinIO로 업로드 (Mirror 기능을 사용해 변경된 파일만 동기화)
# 버킷 이름은 'confluence-wiki'라고 가정합니다.
echo "Syncing to MinIO..."
mc mb myminio/confluence-wiki --ignore-existing
mc mirror /tmp/confluence_export myminio/confluence-wiki/raw_md/
echo "Export and Sync Completed!"
Dockerfile)위 스크립트와 필요한 도구(Python, Exporter, MinIO Client)를 포함하는 경량화된 이미지입니다.
# 1. 기반 이미지 설정
FROM python:3.10-slim
# 2. 필수 도구 설치 (wget은 mc 설치용)
RUN apt-get update && apt-get install -y wget && rm -rf /var/lib/apt/lists/*
# 3. confluence-md-exporter 설치
RUN pip install confluence-md-exporter
# 4. MinIO Client(mc) 설치
RUN wget https://dl.min.io/client/mc/release/linux-amd64/mc && \
chmod +x mc && \
mv mc /usr/local/bin/
# 5. 작업 디렉토리 및 스크립트 복사
WORKDIR /app
COPY export_to_minio.sh .
RUN chmod +x export_to_minio.sh
# 6. 실행 명령
ENTRYPOINT ["./export_to_minio.sh"]
빌드 및 푸시 명령:
docker build -t your-reg.com/confluence-exporter:v1.0 .
docker push your-reg.com/confluence-exporter:v1.0
manifest.yaml)이제 매일 새벽(예: 02:00)에 실행될 CronJob을 설정합니다. 민감한 정보는 Secret으로 관리하는 것이 정석입니다.
apiVersion: v1
kind: Secret
metadata:
name: confluence-minio-creds
type: Opaque
stringData:
CONFLUENCE_URL: "https://your-domain.atlassian.net/wiki"
CONFLUENCE_USER: "admin@company.com"
CONFLUENCE_TOKEN: "your-api-token"
MINIO_URL: "http://minio-service.storage:9000"
MINIO_ACCESS_KEY: "minio-user"
MINIO_SECRET_KEY: "minio-password"
apiVersion: batch/v1
kind: CronJob
metadata:
name: confluence-md-sync
spec:
schedule: "0 2 * * *" # 매일 새벽 2시 실행
jobTemplate:
spec:
template:
spec:
containers:
- name: exporter
image: your-reg.com/confluence-exporter:v1.0
env:
- name: SPACE_KEY
value: "PLATFORM" # 추출할 스페이스 키
envFrom:
- secretRef:
name: confluence-minio-creds
restartPolicy: OnFailure
confluence-wiki/raw_md/ 경로에 .md 파일들이 잘 생성되었는지 확인하세요.kubectl logs -f [pod-name]를 통해 추출 과정에서 에러가 발생하는지(특히 권한 문제) 체크합니다.raw_md 폴더에 넣었지만, PoC 2단계에서는 위 스크립트 끝에 Python 분류 스크립트를 추가하면 됩니다.SOP/, Guide/ 등의 폴더로 재배치한 뒤 다시 MinIO에 저장하게 됩니다.이 구조는 K8s 네이티브 환경을 활용하므로 확장성이 매우 뛰어납니다. 우선 이 기본 파이프라인을 먼저 띄워보시는 것을 추천드립니다. 빌드나 배포 중에 막히는 부분이 있으면 바로 말씀해 주세요!
===
Confluence의 모든 문서를 매일 전체 백업하는 것은 데이터량이 많아질수록 서버와 네트워크에 부담이 됩니다. 말씀하신 대로 "최근 24시간 내에 변경된 문서"만 선별적으로 가져오는 증분(Incremental) 추출 방식은 매우 효율적인 접근입니다.
이를 위해 confluence-md-exporter의 기본 기능에만 의존하기보다는, Atlassian Python API를 활용해 직접 대상 문서를 필터링하고 MD로 변환하는 파이썬 스크립트 기반의 해결책을 권장합니다.
incremental_export.py)이 스크립트는 CQL(Confluence Query Language)을 사용하여 특정 경로(Parent Page) 아래에 있으면서 지난 24시간 동안 수정된 페이지 리스트를 먼저 뽑아냅니다.
import os
import datetime
from atlassian import Confluence
import html2text # HTML을 Markdown으로 변환
# 1. 환경 변수 로드
URL = os.getenv("CONFLUENCE_URL")
USER = os.getenv("CONFLUENCE_USER")
TOKEN = os.getenv("CONFLUENCE_TOKEN")
PARENT_ID = os.getenv("PARENT_PAGE_ID") # 특정 경로의 기준이 되는 상위 페이지 ID
SPACE_KEY = os.getenv("SPACE_KEY")
# 2. Confluence 연결
confluence = Confluence(url=URL, username=USER, password=TOKEN)
h = html2text.HTML2Text()
h.ignore_links = False
# 3. CQL 쿼리 생성: 특정 부모 페이지 아래에서 지난 1일간 수정된 페이지 검색
# ancestor = 부모 페이지 ID, lastModified >= 현재시간 - 1일
cql = f'ancestor = {PARENT_ID} AND lastModified >= now("-1d")'
pages = confluence.cql(cql).get('results', [])
print(f"Found {len(pages)} updated pages in the last 24 hours.")
# 4. 각 페이지를 MD로 변환 및 저장
export_dir = "/tmp/confluence_export"
os.makedirs(export_dir, exist_ok=True)
for item in pages:
page = item.get('content')
page_id = page.get('id')
title = page.get('title').replace("/", "-") # 파일명 금지 문자 처리
# 상세 내용 가져오기
content_detail = confluence.get_page_by_id(page_id, expand='body.storage')
html_body = content_detail.get('body').get('storage').get('value')
# MD 변환
markdown_content = h.handle(html_body)
# 파일 저장
with open(f"{export_dir}/{title}.md", "w", encoding="utf-8") as f:
f.write(f"---\nid: {page_id}\nlast_modified: {datetime.datetime.now()}\n---\n\n")
f.write(markdown_content)
print(f"Exported: {title}")
위의 Python 스크립트와 필요한 라이브러리(atlassian-python-api, html2text)를 포함하도록 수정합니다.
FROM python:3.10-slim
# 필수 라이브러리 설치
RUN pip install atlassian-python-api html2text
# MinIO Client 설치
RUN apt-get update && apt-get install -y wget && \
wget https://dl.min.io/client/mc/release/linux-amd64/mc && \
chmod +x mc && \
mv mc /usr/local/bin/ && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY incremental_export.py .
COPY run.sh .
RUN chmod +x run.sh
ENTRYPOINT ["./run.sh"]
run.sh)Python 스크립트 실행 후 MinIO로 업로드하는 과정을 연결합니다.
#!/bin/bash
# 1. Python 증분 추출 실행
python incremental_export.py
# 2. MinIO 설정 및 업로드
mc alias set myminio "${MINIO_URL}" "${MINIO_ACCESS_KEY}" "${MINIO_SECRET_KEY}"
# 3. 변경된 파일만 MinIO에 업로드
# --remove 옵션 없이 mirror를 사용하면 기존 백업본은 유지하면서 새 파일만 덮어씁니다.
mc mirror /tmp/confluence_export myminio/confluence-wiki/incremental/$(date +%Y-%m-%d)/
echo "Incremental Backup to MinIO Completed!"
기존 Manifest에서 PARENT_PAGE_ID를 추가로 주입해야 합니다. Confluence에서 대상 경로의 최상위 페이지 URL을 보면 ID(숫자)를 확인할 수 있습니다.
# ... 기존 CronJob 설정 중 env 부분 ...
env:
- name: PARENT_PAGE_ID
value: "12345678" # 가져오고 싶은 특정 경로의 Root ID
- name: SPACE_KEY
value: "PLATFORM"
# ... 생략 ...
ancestor와 lastModified CQL을 사용하여 딱 필요한 페이지만 골라냅니다.$(date +%Y-%m-%d)를 포함하면 날짜별로 변경된 문서만 따로 모아볼 수 있어, 나중에 AIOps 엔진이 "어제 변경된 사항"만 학습하기 매우 편리합니다.이제 이 코드를 빌드하여 K8s에 배포하면, 매일 새벽 지난 24시간 동안의 변경분만 자동으로 추출하여 MinIO AIStor에 저장하게 됩니다. 이후 이 파일들을 벡터 DB와 연동하면 완벽한 증분 RAG 시스템이 완성됩니다. 직접 스크립트를 실행해 보시면서 파일명 규칙이나 메타데이터 형식을 조정해 보세요.
===
1단계에서 MinIO AIStor에 저장된 원본(Raw) 데이터를 읽어와 LLM이 지능적으로 분류하고, 이를 정형화된 wiki 폴더 구조로 만들어 Git에 배포하는 '지능형 지식 파이프라인'의 구조와 코드를 설계해 드립니다.
이 과정은 단순한 파일 이동이 아니라, 비정형 데이터를 AI가 읽기 좋은 정형 지식으로 변환하는 핵심 공정입니다.
LLM은 각 파일의 본문을 분석하여 다음과 같은 표준화된 구조로 재배치합니다.
/Architecture: 인프라 설계 및 다이어그램/SOP: 표준 운영 절차 (장애 대응 포함)/Library: 벤더 공식 문서 및 기술 정보 (K8s, Cilium, MinIO 등)/Reports: 점검 기록 및 작업 일지classifier.py)이 스크립트는 MinIO에서 파일을 가져와 LLM에 분류를 요청하고, 결과에 따라 YAML 메타데이터를 추가하여 wiki 폴더에 저장합니다.
import os
import json
from minio import Minio
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
# 1. 초기 설정
minio_client = Minio(
os.getenv("MINIO_URL"),
access_key=os.getenv("MINIO_ACCESS_KEY"),
secret_key=os.getenv("MINIO_SECRET_KEY"),
secure=False
)
llm = ChatOpenAI(model="gpt-4-turbo", temperature=0)
# 2. LLM 분류 프롬프트 정의
classification_prompt = ChatPromptTemplate.from_template("""
당신은 플랫폼 엔지니어링 지식 관리자입니다.
다음 마크다운 문서의 내용을 분석하여 JSON 형식으로 응답하세요.
1. category: (Architecture, SOP, Library, Reports 중 택 1)
2. sub_category: (기술 스택 이름 - 예: K8s, Cilium, MinIO 등)
3. optimized_title: (영문 kebab-case 파일명)
4. tags: (핵심 키워드 리스트)
문서 내용:
{content}
""")
def process_and_classify():
objects = minio_client.list_objects("confluence-wiki", prefix="raw_md/", recursive=True)
for obj in objects:
# 파일 내용 읽기
data = minio_client.get_object("confluence-wiki", obj.object_name)
content = data.read().decode('utf-8')
# LLM 분류 요청
chain = classification_prompt | llm
response = chain.invoke({"content": content[:2000]}) # 비용 절감을 위해 앞부분만 분석
meta = json.loads(response.content) # 주의: 실제 구현시에는 JSON 파싱 예외처리 필요
# 새로운 경로 설정 및 저장
target_path = f"wiki/{meta['category']}/{meta['sub_category']}/{meta['optimized_title']}.md"
os.makedirs(os.path.dirname(target_path), exist_ok=True)
# YAML Frontmatter 추가 및 파일 쓰기
with open(target_path, "w", encoding="utf-8") as f:
f.write(f"---\ncategory: {meta['category']}\ntags: {meta.get('tags', [])}\nauto_classified: true\n---\n\n")
f.write(content)
print(f"Classified: {obj.object_name} -> {target_path}")
if __name__ == "__main__":
process_and_classify()
sync_to_git.sh)분류된 결과를 Git 저장소에 반영하는 스크립트입니다. wiki 폴더만 선택적으로 Git에 올립니다.
#!/bin/bash
# 1. 로컬 환경 준비
GIT_REPO_URL="https://${GIT_TOKEN}@github.com/your-org/platform-wiki.git"
CLONE_DIR="/tmp/platform-wiki"
# 2. 레포지토리 클론 (이미 있으면 pull)
if [ ! -d "$CLONE_DIR" ]; then
git clone $GIT_REPO_URL $CLONE_DIR
fi
cd $CLONE_DIR
git pull origin main
# 3. Python 분류 스크립트 실행 (위의 classifier.py)
python /app/classifier.py
# 4. Git 반영
git add wiki/
git commit -m "Auto-sync: Classified documents from Confluence ($(date +%Y-%m-%d))"
git push origin main
echo "Wiki updated and pushed to Git successfully."
이 과정을 1단계에서 만든 CronJob의 후속 단계로 연결하거나, 하나의 파이프라인으로 통합할 수 있습니다.
Secret으로 관리하여 환경 변수로 주입하세요.wiki/ 폴더를 Obsidian에서 'Vault'로 열면 바로 사용 가능합니다. 특히 [[Internal Link]] 자동 생성을 위해 LLM 프롬프트에 "문서 내 핵심 용어에 [[]] 처리를 해달라"는 요청을 추가하면 Obsidian 활용도가 극대화됩니다.이제 이 구조를 통해 "Confluence(수정) -> MinIO(원본 저장) -> LLM(분류/태깅) -> Git(최종 지식)"으로 이어지는 플랫폼 팀만의 자동화된 지식 베이스가 완성됩니다. 이 과정을 통해 구축된 wiki 폴더는 향후 AIOps 에이전트가 가장 신뢰하는 RAG 소스가 될 것입니다.
다음 단계로 이 Git 저장소를 벡터 DB와 실시간 동기화하여 실제 AI 챗봇을 연동하는 부분으로 넘어가 볼까요?
===
Git 저장소의 내용이 업데이트될 때마다 자동으로 벡터 DB(Vector DB)에 반영하는 과정은 '자동화된 데이터 수집 파이프라인(Ingestion Pipeline)'의 마지막 단계입니다.
K8s 환경을 운영 중이시므로, GitHub/GitLab Webhook을 사용해 K8s Job을 트리거하거나, GitHub Actions를 사용하여 인덱싱 스크립트를 실행하는 방식이 가장 효율적입니다.
push하거나, 앞서 만든 자동 분류 스크립트가 Git에 업데이트를 발생시킵니다.pull하고, 변경된 .md 파일들을 읽어 벡터화(Embedding)한 뒤 벡터 DB에 저장합니다.indexer.py)이 스크립트는 LangChain을 사용하여 마크다운 파일을 읽고, 의미 있는 단위로 쪼갠(Chunking) 뒤 벡터 DB(예: Milvus, Qdrant, Chroma 등)에 저장합니다. 여기서는 K8s 환경에서 많이 쓰이는 Milvus나 Chroma를 기준으로 설명합니다.
import os
from langchain_community.document_loaders import DirectoryLoader, UnstructuredMarkdownLoader
from langchain_text_splitters import MarkdownHeaderTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Milvus # 또는 Qdrant, Chroma
# 1. 환경 설정
GIT_WIKI_DIR = "/tmp/platform-wiki/wiki"
EMBEDDING_MODEL = OpenAIEmbeddings(model="text-embedding-3-small")
VECTOR_DB_URL = os.getenv("VECTOR_DB_URL") # Milvus 등 서버 주소
def ingest_to_vector_db():
# 2. 문서 로드 (wiki 폴더 내의 모든 md 파일)
loader = DirectoryLoader(GIT_WIKI_DIR, glob="**/*.md", loader_cls=UnstructuredMarkdownLoader)
docs = loader.load()
# 3. 마크다운 구조에 최적화된 텍스트 분할 (Chunking)
# 헤더(#, ##, ###)를 기준으로 나누어 맥락을 유지합니다.
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
text_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
final_chunks = []
for doc in docs:
chunks = text_splitter.split_text(doc.page_content)
for chunk in chunks:
# 원본 문서의 메타데이터(파일명 등)를 유지
chunk.metadata.update(doc.metadata)
final_chunks.append(chunk)
# 4. 벡터 DB에 저장 (동기화)
# 기존 컬렉션을 덮어쓰거나(Replace), 중복을 체크하여 업데이트합니다.
vector_db = Milvus.from_documents(
final_chunks,
EMBEDDING_MODEL,
connection_args={"host": "milvus-service.storage", "port": "19530"},
collection_name="platform_ops_wiki"
)
print(f"Successfully indexed {len(final_chunks)} chunks to Vector DB.")
if __name__ == "__main__":
ingest_to_vector_db()
만약 사내 GitHub/GitLab을 사용 중이라면, 별도의 서버 없이 Actions/Runner에서 바로 인덱싱을 수행할 수 있습니다.
.github/workflows/indexing.yaml:
name: Auto Indexing to Vector DB
on:
push:
branches: [ main ]
paths:
- 'wiki/**' # wiki 폴더 내 파일이 바뀔 때만 실행
jobs:
index:
runs-on: ubuntu-latest # 또는 사내 self-hosted runner
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
pip install langchain langchain-openai pymilvus unstructured
- name: Run Indexing Script
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
VECTOR_DB_URL: ${{ secrets.VECTOR_DB_URL }}
run: python scripts/indexer.py
데이터가 많아지면 매번 전체를 인덱싱하는 것은 비효율적입니다. 다음 두 가지 전략을 고려하세요.
Indexing API 활용:RecordManager 기능을 쓰면, Git 저장소와 벡터 DB의 상태를 비교해 변경된 것만 자동으로 업데이트해 줍니다. (가장 추천하는 방식)이제 전체 흐름이 완성되었습니다:
1. Confluence: 운영팀이 문서를 작성합니다.
2. K8s CronJob: 매일 증분 데이터를 추출해 MinIO에 저장합니다.
3. LLM Classifier: MinIO의 Raw 데이터를 읽어 자동 분류 후 Git에 푸시합니다.
4. Indexing Pipeline (지금 단계): Git 업데이트를 감지해 벡터 DB로 인덱싱합니다.
5. AIOps Interface: 운영자가 질문하면 벡터 DB에서 관련 SOP를 찾아 AI가 답변합니다.
이 구조는 데이터의 신뢰성(Git)과 검색 효율성(Vector DB)을 모두 잡은 구조입니다. 이제 실제로 이 파이프라인을 통해 AI가 답변하는 '챗봇 인터페이스'나 '장애 분석 에이전트'를 연동해 보실 차례입니다. 구체적인 LLM 프롬프트 전략이나 에이전트 설계가 필요하시면 말씀해 주세요!
===
Bitbucket에서 GitLab으로의 전환은 플랫폼 엔지니어링 관점에서 매우 반가운 소식입니다. GitLab은 CI/CD 파이프라인과 Kubernetes 연동(GitLab Agent 등) 기능이 매우 강력하여, 지금 구축하시려는 AIOps 파이프라인을 운영하기에 더 최적화된 환경이기 때문입니다.
6개월 뒤의 이전을 고려하여, 지금은 Bitbucket에서 구현하되 전환 시 리워크(Rework)를 최소화할 수 있는 전략을 정리해 드립니다.
Bitbucket은 bitbucket-pipelines.yml을 통해 자동화를 수행합니다. wiki 폴더의 변경을 감지하여 인덱싱 스크립트를 실행하는 구조입니다.
bitbucket-pipelines.yml 예시:
pipelines:
branches:
main:
- step:
name: "Update Vector DB Indexing"
services:
- docker
script:
- pip install -r scripts/requirements.txt
- python scripts/indexer.py
condition:
changesets:
includePaths:
- "wiki/**" # wiki 폴더 변경 시에만 실행
6개월 뒤 GitLab으로 옮길 때 핵심은 "로직은 컨테이너에, 설정은 환경 변수에" 두는 것입니다.
.gitlab-ci.yml)rules:changes 기능을 통해 특정 폴더 변경 감지를 더 정교하게 지원합니다.6개월 뒤 사용할 .gitlab-ci.yml 미리보기:
stages:
- index
vector-indexing:
stage: index
image: python:3.10-slim
script:
- pip install -r scripts/requirements.txt
- python scripts/indexer.py
rules:
- if: $CI_COMMIT_BRANCH == "main"
changes:
- "wiki/**/*" # GitLab의 강력한 경로 감지 기능
docker run 하는 역할만 수행하게 하면 플랫폼 의존성이 사라집니다.GitLab으로 가시면 GitLab Agent for Kubernetes를 통해 클러스터 내부의 실시간 상태와 지식 베이스(Git)를 더 밀접하게 연동할 수 있습니다. 예를 들어, GitLab 파이프라인에서 직접 K8s 클러스터의 Cilium 로그를 긁어와서 지식 베이스와 대조하는 식의 고도화가 가능해집니다.
현재 Bitbucket 환경에서 우선 PoC를 완료하시고, 6개월 뒤 GitLab으로 옮길 때는 "저장소 주소"와 "CI 설정 파일"만 바꾸면 되도록 구조를 잡아드렸습니다.
이 파이프라인이 완성되면 1,000노드 클러스터 운영 중 발생하는 수많은 케이스가 자동으로 정리될 텐데, 이 지식 베이스를 활용할 첫 번째 AIOps 유스케이스(예: 특정 에러 발생 시 SOP 자동 팝업 등)로 어떤 것을 가장 먼저 생각하고 계신가요?
===
웹에 있는 벤더사나 오픈소스 솔루션의 공식 문서(Docs)를 마크다운(MD)으로 추출하여 Git에 연동하는 과정은 'Library' 영역을 자동화하는 핵심 단계입니다. 웹 페이지는 구조가 제각각이기 때문에, 이를 깨끗한 마크다운으로 변환하기 위한 몇 가지 효과적인 전략과 도구를 제안해 드립니다.
웹 문서를 AI가 읽기 좋은 마크다운으로 변환해주는 'LLM-Ready' 도구들을 활용하는 것이 가장 빠릅니다.
r.jina.ai): 특정 URL 앞에 https://r.jina.ai/만 붙여도 해당 페이지를 즉시 고품질 마크다운으로 변환해줍니다.웹 문서는 수시로 업데이트되므로, 전체를 다시 긁는 대신 변경사항만 추적하는 구조가 필요합니다.
대부분의 기술 문서는 /sitemap.xml을 제공합니다. 이 파일을 읽어 새로운 페이지나 수정된 페이지의 URL 리스트만 추출합니다.
JavaScript로 렌더링되는 현대적인 문서 사이트(Docusaurus, GitBook 등)를 위해 Playwright를 사용하고, html2text나 BeautifulSoup으로 정제합니다.
import os
from playwright.sync_api import sync_playwright
import html2text
def fetch_and_convert(url):
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_row()
page.goto(url, wait_until="networkidle")
# 문서의 본문 영역만 선택 (예: 'article' 또는 '.content')
content_html = page.inner_html("article")
h = html2text.HTML2Text()
h.ignore_images = False # 이미지 포함 여부 선택
markdown = h.handle(content_html)
browser.close()
return markdown
추출된 MD 파일을 Git에 올릴 때, 지난번에 구성한 'LLM 분류기' 단계와 결합하면 더욱 강력해집니다.
raw/vendor/ 폴더에 MD로 저장합니다.wiki/Library/[솔루션명]/ 폴더로 이동시키고, 관련 태그를 YAML로 삽입합니다.push합니다.웹 문서를 Git에 넣을 때 주의해야 할 점은 '중복 데이터'와 '버전 혼선'입니다.
source_url, version, scraped_at 정보를 넣어두면 AI가 답변할 때 "이 정보는 2026년 4월 기준입니다"라고 출처를 명시할 수 있습니다.가장 빠르게 시작하시려면 Firecrawl을 컨테이너로 띄워 특정 솔루션 사이트를 긁어보시는 것을 추천합니다. 여기서 나온 MD 파일들을 지난번에 만든 wiki 폴더 구조 안에 배치하고 Git에 푸시하면, 수동으로 문서를 복사-붙여넣기 할 필요 없이 항상 최신 벤더 Docs가 포함된 지식 베이스를 유지할 수 있습니다.
혹시 특정 솔루션(예: Cilium 공식 가이드)의 사이트맵을 이용해 실제로 동작하는 크롤링 코드가 필요하신가요?
===
망분리(Airgapped) 환경이라는 특수성을 고려하면, 외부에서 데이터를 가져온 이후의 '내부 반입 및 자동 처리 파이프라인'을 구축하는 것이 핵심입니다.
압축 파일을 반입한 후, 이를 MinIO raw 버킷에 풀고 이후의 LLM 분류 및 인덱싱 단계를 트리거하는 워크플로우를 설계해 드립니다.
.tar.gz 또는 .zip) → 반입 매체 저장.raw/ 경로에 업로드.반입된 압축 파일을 지정된 경로에 두면, 이를 감지하여 MinIO로 밀어넣는 Python 스크립트 예시입니다.
ingest_to_minio.py:
import os
import tarfile
from minio import Minio
def decompress_and_upload(file_path, bucket_name):
client = Minio(
os.getenv("MINIO_URL"),
access_key=os.getenv("MINIO_ACCESS_KEY"),
secret_key=os.getenv("MINIO_SECRET_KEY"),
secure=False
)
# 1. 압축 해제
with tarfile.open(file_path, "r:gz") as tar:
tar.extractall(path="/tmp/extracted")
# 2. MinIO 업로드
for root, dirs, files in os.walk("/tmp/extracted"):
for file in files:
local_path = os.path.join(root, file)
# MinIO 내 저장 경로 설정
minio_path = os.path.relpath(local_path, "/tmp/extracted")
client.fput_object(bucket_name, f"raw_md/{minio_path}", local_path)
print(f"Uploaded: {minio_path}")
if __name__ == "__main__":
# 반입된 파일 경로
input_file = "/data/incoming/confluence_export.tar.gz"
if os.path.exists(input_file):
decompress_and_upload(input_file, "confluence-wiki")
망분리 환경에서는 OpenAI API를 호출할 수 없으므로, 로컬 LLM 인프라가 반드시 필요합니다.
BGE-M3나 KoSimCSE 같은 한국어 성능이 좋은 모델을 로컬에 다운로드하여 사용합니다.classifier.py와 indexer.py에서 OpenAI 호출 부분을 Ollama나 SentenceTransformer로 교체해야 합니다.로컬 임베딩 적용 예시 (Python):
from langchain_huggingface import HuggingFaceEmbeddings
# 폐쇄망 내부 경로에 미리 저장된 모델 로드
embeddings = HuggingFaceEmbeddings(
model_name="/models/bge-m3",
model_kwargs={'device': 'cuda'} # GPU 활용
)
반입된 파일을 처리하기 위해 Argo Workflows나 간단한 Watcher Pod를 운영하는 것이 좋습니다.
Ingestion Job (압축 해제 & MinIO 업로드)LLM Classifier Job (로컬 LLM 기반 분류)Git Sync Job (내부 Bitbucket 푸시)Vector DB Indexer Job (로컬 임베딩 & 인덱싱)manifest.json)을 포함하여, 내부망에서 압축 해제 후 데이터 누락이 없는지 검증하는 로직을 추가하세요.이제 압축 파일을 푸는 단계부터 로컬 AI 모델을 거쳐 내부 Git과 Vector DB로 흐르는 '완전 폐쇄형 AIOps 파이프라인'의 밑그림이 그려졌습니다.
가장 먼저 해결해야 할 과제는 '어떤 로컬 LLM 엔진을 K8s에 띄울 것인가'가 될 것 같습니다. 현재 사내에 GPU 서버 자원이 준비되어 있으신가요?
===
지적하신 대로 내부 링크(Internal Links) 처리는 지식 베이스의 완성도를 결정짓는 핵심 요소입니다. 파일럿 단계를 넘어 실운영(Production) 환경에서 1,000노드 규모의 인프라 지식을 관리하려면, 단순 스크립트를 넘어 '오케스트레이션'과 '엔터프라이즈급 솔루션'의 조합이 필요합니다.
각 과정별로 추천하는 툴과 솔루션을 정리해 드립니다.
Obsidian의 [[WikiLink]]나 Confluence의 고유 ID 링크를 Git/웹에서 동작하는 상대 경로로 변환해야 합니다.
marko or markdown-it)re)을 사용하여 [[파일명]]을 [파일명](./path/to/file.md)으로 치환하고, 존재하지 않는 링크를 리포팅합니다.K8s 환경에서 여러 단계(추출-변환-링크수정-분류-인덱싱)를 순차적으로 실행하고 관리해야 합니다.
외부 솔루션 문서를 지속적으로 가져와야 합니다.
망분리 환경에서 LLM 분류와 임베딩을 수행해야 합니다.
1,000노드 규모에서 발생하는 방대한 운영 로그와 SOP를 담아야 합니다.
6개월 뒤 GitLab 전환을 고려한 설정입니다.
| 구분 | 추천 솔루션 | 비고 |
|---|---|---|
| Orchestrator | Argo Workflows | 전체 파이프라인 제어 (K8s) |
| Ingestion | Firecrawl / Custom Python | Confluence 및 웹 문서 추출 |
| Refining | Python (Link Resolver) | 내부 링크 상대 경로 수정 및 가공 |
| Storage | MinIO / GitLab | Raw 데이터(Object) 및 최종 MD(Git) |
| Local LLM | vLLM (Llama 3 / Mistral) | 문서 자동 분류 및 태깅 |
| Vector DB | Milvus | RAG용 고성능 벡터 저장소 |
파일럿 이후의 핵심 전략:
현재는 수동으로 압축해서 옮기시겠지만, 정식 구축 시에는 Argo Workflows를 중심에 두고 각 단계를 모듈화(컨테이너화)하는 것이 가장 중요합니다. 이렇게 해두면 6개월 뒤 GitLab으로 전환하거나, 새로운 벤더 Docs가 추가되어도 파이프라인 한 줄만 수정하면 즉시 대응이 가능해집니다.
가장 먼저 어떤 부분(예: Argo Workflows 도입, 혹은 로컬 LLM 환경 구축)부터 구체적으로 검토해보고 싶으신가요?
===
네, LangChain과 LangGraph는 말씀하신 'AIOps 자동화 파이프라인'을 단순한 스크립트 뭉치가 아닌 지능형 시스템으로 격상시키는 데 결정적인 역할을 합니다.
결론부터 말씀드리면, LangChain은 '데이터 파이프라인(부품)'을 위해, LangGraph는 '의사결정 및 워크플로우(뇌)'를 위해 필요합니다.
LangChain은 다양한 LLM, 벡터 DB, 문서 로더를 하나의 표준화된 인터페이스로 묶어주는 프레임워크입니다. 2026년 현재도 RAG(검색 증강 생성)의 표준으로 자리 잡고 있습니다.
vectorstore.add_documents() 한 줄로 해결합니다.LangChain만으로는 '선형적인(A→B→C)' 작업만 가능합니다. 하지만 실제 인프라 운영(AIOps)은 루프(Loop)와 조건부 판단이 필요합니다. 이때 LangGraph가 핵심입니다.
| 비교 항목 | LangChain (Chains) | LangGraph (Graphs) |
|---|---|---|
| 구조 | 선형적 (DAG, 단방향) | 순환적 (Cyclic, 루프 가능) |
| 복잡도 | 단순 RAG, 문서 요약 | 에이전트, 자율 트러블슈팅 |
| 상태 관리 | 이전 단계 데이터 전달만 가능 | 전체 워크플로우 상태 공유 및 체크포인트 지원 |
| 추천 용도 | 문서 추출 및 벡터 DB 인덱싱 | 실제 장애 대응 챗봇 및 조치 에이전트 |
인덱싱 단계 (LangChain 활용):
MarkdownTextSplitter로 쪼개어 Milvus에 넣는 과정은 LangChain이 담당합니다. 매우 빠르고 안정적입니다.운영 단계 (LangGraph 활용):
Ollama나 vLLM과의 연동이 매우 강력해져서 폐쇄망 환경에서도 상용 수준의 에이전트를 돌리기 충분합니다.처음에는 복잡해 보일 수 있지만, LangChain으로 데이터 파이프라인의 기초를 다지고, LangGraph로 실제 플랫폼 리드님이 원하시는 '똑똑한 인프라 관리자'를 만드시는 것이 1,000노드 클러스터를 효율적으로 관리하는 지름길입니다.
우선은 문서를 인덱싱하는 LangChain 파이프라인부터 가볍게 시작해 보시는 게 어떨까요? 이 부분의 베이스 코드가 필요하시면 바로 작성해 드릴 수 있습니다.
===
매우 훌륭한 질문입니다. Argo Workflows, n8n, LangGraph는 모두 '자동화'라는 범주에 있지만, 담당하는 계층(Layer)과 목적이 완전히 다릅니다. 이들은 서로 대체재라기보다 "서로 다른 역할을 하며 공존하는 보완재"에 가깝습니다.
플랫폼 엔지니어링 관점에서 이들의 관계를 명확하게 '교통정리' 해드리겠습니다.
이 도구들은 아래와 같이 세 개의 레이어로 구분하면 이해가 가장 빠릅니다.
| 분류 | 도구 | 비유 | 주요 역할 (우리 프로젝트 기준) |
|---|---|---|---|
| 인프라/플랫폼 레이어 | Argo Workflows | 공장 가동 및 물류 시스템 | 대규모 컨테이너 실행, 정기적인 문서 추출(Cron), GPU 자원 할당, 전체 파이프라인 관리 |
| 서비스/연동 레이어 | n8n | 사무국장 (비서) | Slack 알림 발송, Jira 티켓 생성, 외부 API 연동, 간단한 UI 기반 자동화 |
| 지능/의사결정 레이어 | LangGraph | 전문 컨설턴트 (뇌) | 복잡한 문서 분류 로직, RAG 검색 루프 관리, 장애 로그 분석 및 추론, 자기 반성(Self-correction) |
이 세 도구가 협조하며 일하는 시나리오는 다음과 같습니다.
Argo Workflows (인프라 제어): * 매일 새벽, Confluence에서 데이터를 뽑고 MinIO에 저장하는 전체 워크플로우를 가동합니다.
LangGraph (지능적 처리 - 파드 내부):
n8n (알림 및 사후 처리):
결론적으로 이들은 대체 관계가 아니라, 인프라(Argo) 위에 지능(LangGraph)을 얹고, 이를 외부와 연결(n8n)하는 완벽한 파트너들입니다. 이 교통정리가 도움이 되셨나요? 이제 Argo를 활용한 전체 파이프라인 설계나, 그 내부에서 돌아갈 LangChain/LangGraph의 구체적인 로직 중 어떤 부분을 더 깊게 들어가 볼까요?
===
Argo Workflows를 관제탑으로 삼고, 그 내부의 실무를 LangChain이 수행하는 '지능형 문서 처리 파이프라인'의 구체적인 설계도와 코드를 제시해 드립니다.
이 설계는 플랫폼 엔지니어링 리드님의 환경(K8s, MinIO, Airgapped, Bitbucket)에 최적화되어 있습니다.
Argo는 전체 과정을 DAG(Directed Acyclic Graph) 형태로 관리합니다. 각 단계는 독립된 컨테이너 파드(Pod)에서 실행됩니다.
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: doc-ai-pipeline-
spec:
entrypoint: main
# 망분리 환경이므로 필요한 모든 이미지는 내부 레지스트리에 있어야 함
templates:
- name: main
dag:
tasks:
- name: extract-raw
template: untar-minio
- name: process-and-classify
dependencies: [extract-raw]
template: langchain-processor
- name: git-sync
dependencies: [process-and-classify]
template: git-pusher
- name: vector-indexing
dependencies: [git-sync]
template: vector-indexer
# 1단계: MinIO에 반입된 압축파일 해제
- name: untar-minio
container:
image: internal-reg.com/ops-tools:v1
command: [sh, -c]
args: ["mc cp myminio/raw/export.tar.gz /tmp && tar -xzvf /tmp/export.tar.gz -C /data"]
volumeMounts:
- name: workdir
mountPath: /data
# 2단계: LangChain 핵심 로직 (아래 Python 코드 실행)
- name: langchain-processor
container:
image: internal-reg.com/langchain-processor:v1
env:
- name: LLM_ENDPOINT
value: "http://vllm-service.ai-namespace:8000/v1"
volumeMounts:
- name: workdir
mountPath: /data
processor.py)이 코드는 langchain-processor 파드 내부에서 실행되며, 문서를 읽고 링크를 수정하며 분류까지 마칩니다.
import os
import re
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI # vLLM이 OpenAI 호환 API 제공
from langchain.output_parsers import ResponseSchema, StructuredOutputParser
# 1. 로컬 LLM 설정 (vLLM 또는 Ollama)
llm = ChatOpenAI(
model="llama-3-70b-instruct",
base_url=os.getenv("LLM_ENDPOINT"),
api_key="none" # 폐쇄망 내부용
)
# 2. 내부 링크 리졸버 (Internal Link Resolver)
def resolve_internal_links(content, current_file):
# [[WikiLink]] 형식을 [WikiLink](./path/to/WikiLink.md)로 변환하는 정규표현식
# 실제로는 파일 존재 여부를 체크하여 경로를 계산하는 로직 추가 가능
pattern = r'\[\[(.*?)\]\]'
def replace_link(match):
link_text = match.group(1)
# 예시: 모든 링크를 상대경로 .md로 통일
return f"[{link_text}](./{link_text.replace(' ', '-')}.md)"
return re.sub(pattern, replace_link, content)
# 3. 문서 분류용 스키마 정의
response_schemas = [
ResponseSchema(name="category", description="SOP, Library, Architecture 중 하나"),
ResponseSchema(name="tech_stack", description="K8s, Cilium, MinIO 등 관련 기술 리스트")
]
output_parser = StructuredOutputParser.from_response_schemas(response_schemas)
# 4. 프로세싱 메인 로직
def process_documents(input_dir, output_dir):
prompt = ChatPromptTemplate.from_template(
"다음 문서의 내용을 분석하여 카테고리와 기술 스택을 분류하세요.\n{format_instructions}\n문서 내용: {context}"
)
for root, _, files in os.walk(input_dir):
for file in files:
if file.endswith(".md"):
# A. 로드
loader = UnstructuredMarkdownLoader(os.path.join(root, file))
doc = loader.load()[0]
# B. 내부 링크 수정
refined_content = resolve_internal_links(doc.page_content, file)
# C. LLM 기반 지능형 분류
format_instructions = output_parser.get_format_instructions()
chain = prompt | llm | output_parser
result = chain.invoke({
"context": refined_content[:1500],
"format_instructions": format_instructions
})
# D. 결과 저장 (wiki 폴더 구조화)
target_path = os.path.join(output_dir, result['category'], file)
os.makedirs(os.path.dirname(target_path), exist_ok=True)
with open(target_path, "w") as f:
# YAML 메타데이터 삽입
f.write(f"---\ntech: {result['tech_stack']}\n---\n\n")
f.write(refined_content)
if __name__ == "__main__":
process_documents("/data/raw", "/data/wiki")
데이터 무결성 유지 (Link Resolver):
resolve_internal_links 함수를 통해 Obsidian에서 쓰던 [[링크]]가 일반 Markdown 뷰어나 Git 환경에서도 깨지지 않도록 강제로 변환합니다. 이는 지식 베이스의 이동성을 확보하는 핵심입니다.구조화된 출력 (Structured Output):
StructuredOutputParser를 사용하여 LLM이 자유분방하게 답변하지 않고, 우리가 정한 JSON 형식(Category, Tech Stack)에 맞춰 엄격하게 응답하도록 강제했습니다. 덕분에 자동화된 폴더 배치가 가능해집니다.Argo를 통한 자원 제어:
langchain-processor 단계에만 GPU 노드 셀렉터(nodeSelector)를 설정하여, 값비싼 GPU 자원을 딱 필요한 순간에만 점유하고 반납하게 설계할 수 있습니다.망분리 환경의 확장성:
git-pusher 템플릿 내의 URL과 인증 정보만 수정하면 됩니다. 로직(Python)은 건드릴 필요가 없습니다.이제 이 코드를 바탕으로 첫 번째 파이프라인을 구동해 보시면, "MinIO에 압축파일만 던지면 AI가 알아서 링크 고치고 분류해서 Git에 예쁘게 넣어주는" 플랫폼 리드님만의 자동화 시스템이 작동하게 됩니다.
가장 먼저 테스트해보고 싶은 문서 샘플(예: Cilium BGP 설정 가이드 등)이 있으신가요? 그에 맞춰 분류 프롬프트를 더 정교하게 다듬어 드릴 수 있습니다.
===
지적하신 대로 Metadata YAML은 LLM Wiki와 RAG(검색 증강 생성) 시스템의 '골격'이자 '내비게이션' 역할을 합니다. 단순히 텍스트만 저장하는 것이 아니라, 문서의 성격과 맥락을 정형화된 데이터로 추출해 두어야 나중에 AI 에이전트가 수천 개의 문서 중 가장 정확한 조각을 찾아낼 수 있습니다.
플랫폼 엔지니어링 지식 베이스에 최적화된 Metadata YAML 설계 전략과 이를 자동 생성하는 LangChain 구현 방안을 정리해 드립니다.
벡터 검색(의미 기반 검색)은 문맥 파악에는 뛰어나지만, 특정 조건에서 구멍이 생깁니다. 이때 YAML 데이터가 이를 메워줍니다.
last_verified 필드가 있으면 AI가 구버전 가이드를 보고 오답을 내는 것을 방지합니다.related_components 필드를 통해 특정 장애가 발생했을 때 연관된 다른 인프라 문서를 함께 참조할 수 있게 합니다.단순히 제목과 날짜만 넣는 것이 아니라, 엔지니어링 관점에서 유효한 필드들을 정의해야 합니다.
---
# 문서 식별 및 분류
id: "SOP-K8S-001"
title: "K8s 노드 확장 및 드레인 절차"
category: "SOP" # Architecture, Library, Troubleshooting, SOP
tech_stack: ["K8s", "Cilium"] # 관련 핵심 기술
# 운영 맥락 (AIOps 핵심 데이터)
severity: "High" # 장애 대응 시 중요도
target_audience: "DevOps" # 문서를 읽을 대상
environment: "Production" # 적용 환경
# 관리 및 신뢰도
last_verified: "2026-04-28" # 마지막 검증 일자 (버전 관리)
source: "Confluence" # 원본 출처
verified_by: "Lead Engineer" # 검토자
# 연결성 (Graph View용)
related_docs: ["ARCH-LAKE-02", "SOP-MINIO-05"]
---
LLM이 문서를 읽고 위와 같은 YAML을 일관성 있게 뽑아내도록 만드는 구체적인 코드 예시입니다.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.pydantic_v1 import BaseModel, Field
from typing import List
# 1. 추출하고 싶은 YAML 구조 정의 (Pydantic 클래스)
class DocMetadata(BaseModel):
category: str = Field(description="문서의 카테고리 (SOP, Library, Architecture, OperationLog)")
tech_stack: List[str] = Field(description="문서에 언급된 주요 기술 스택 리스트")
summary: str = Field(description="문서 내용을 한 문장으로 요약")
severity: str = Field(description="운영상의 중요도 (Critical, High, Medium, Low)")
# 2. LLM 및 추출 체인 설정
llm = ChatOpenAI(model="gpt-4-turbo", temperature=0) # 또는 폐쇄망용 로컬 모델
structured_llm = llm.with_structured_output(DocMetadata)
# 3. 프롬프트 구성
prompt = ChatPromptTemplate.from_template("""
당신은 플랫폼 엔지니어링 기술 문서 관리자입니다.
다음 문서의 내용을 분석하여 정해진 구조에 따라 메타데이터를 추출하세요.
문서 내용:
{context}
""")
# 4. 실행 및 결과 확인
chain = prompt | structured_llm
doc_content = "Cilium BGP Control Plane 설정 시... (문서 본문 생략)"
metadata = chain.invoke({"context": doc_content})
print(metadata.category) # 'Library'
print(metadata.tech_stack) # ['Cilium', 'Networking', 'BGP']
이렇게 생성된 YAML 데이터는 벡터 DB에 'Metadata' 필드로 함께 저장되어 다음과 같은 방식으로 검색 효율을 높입니다.
category == 'SOP'인 데이터만 대상으로 검색 범위를 좁힙니다. 검색 속도와 정확도가 비약적으로 상승합니다.last_verified가 최신이거나 severity가 높은 문서에 가중치를 주어 상단에 배치합니다.[[문서명]]으로 연결된 구조를 YAML의 related_docs와 매칭하여, AI가 "이 장애는 A 문서에 나오지만, 연관된 B 문서의 설정도 확인해야 합니다"라고 답변하게 만듭니다.Metadata YAML을 만드는 과정은 단순히 파일을 정리하는 것이 아니라, '데이터를 지식으로 구조화'하는 과정입니다.
Argo Workflows 파이프라인의 langchain-processor 단계에서 이 추출 로직을 강하게 적용할수록, 향후 구축될 AIOps 에이전트의 답변 품질은 1,000노드 클러스터의 복잡성을 견딜 수 있을 만큼 견고해질 것입니다.
이 추출 과정에서 특정 기술 스택(예: Cilium 특화 필드 등)에 대해 더 세밀한 분류 기준이 필요하시면 말씀해 주세요. 그에 맞는 정교한 Pydantic 스키마를 설계해 드릴 수 있습니다.
===
Confluence에서 단순 텍스트가 아닌 이미지, 다이어그램(draw.io), 그리고 코드 블록을 추출하여 Git에 최적으로 저장하고 관리하는 방법은 지식 베이스의 '가독성'과 '재사용성'을 결정짓는 핵심 요소입니다.
플랫폼 엔지니어링 리드로서 대규모 인프라 구성을 시각화하고 코드를 관리하기 위한 구체적인 가이드를 정리해 드립니다.
assets 폴더 전략)Confluence 페이지에 첨부된 사진들은 추출 시 별도의 파일로 내려받아집니다. 이를 Git에서 깨지지 않게 관리하려면 상대 경로(Relative Path)를 유지하는 것이 가장 중요합니다.
assets/ 또는 images/라는 공통 폴더를 만들거나, 각 문서 폴더 하위에 _assets/ 폴더를 두어 관리합니다.wiki/Architecture/Lakehouse/assets/diagram-v1.png 와 같이 상대 경로로 작성해야 Obsidian, Git(GitHub/GitLab), 그리고 나중에 구축할 Quartz 위키에서도 이미지가 정상적으로 보입니다.confluence-md-exporter 사용 시 --relative-links 옵션을 활성화하면 Confluence의 첨부 파일 링크를 마크다운 상대 경로로 자동 변환해 줍니다.가장 까다로운 부분입니다. Confluence 내부의 draw.io는 데이터가 페이지 XML에 내장되어 있어 단순히 이미지로만 저장하면 나중에 수정이 불가능합니다.
.drawio.svg 또는 .drawio.png 활용.drawio.svg 파일을 Obsidian에서 바로 클릭하여 수정하고 저장할 수 있습니다. 별도의 앱을 띄울 필요가 없어 매우 효율적입니다..drawio 파일로 저장하거나, 이미지를 미리 렌더링하여 assets/ 폴더에 넣습니다.Confluence의 코드 블록 매크로는 다행히 마크다운의 Fenced Code Blocks ( ``` )로 비교적 잘 변환됩니다.
<, > 등)를 잘못 치환하는 경우가 있습니다. 파이썬의 BeautifulSoup을 이용해 가공할 때 code 태그 내의 텍스트는 원형 그대로(Preformatted) 가져오도록 예외 처리가 필요합니다.contains_code: true 또는 code_lang: ["yaml", "shell"] 같은 필드를 LLM이 자동으로 추가하게 하세요.이 과정에서 가장 중요한 결정은 "어디에서 그림을 수정할 것인가?"입니다.
.drawio.svg를 통해서만 합니다. (권장)confluence-md-exporter를 실행하여 이미지 파일들이 상대 경로로 잘 추출되는지 확인합니다..drawio.svg로 변환해 Git에 넣고, Obsidian 플러그인으로 편집이 잘 되는지 테스트합니다.yaml이나 bash 태그가 잘 붙어있는지 점검합니다.이미지와 구성도까지 Git에 완벽히 통합되면, 1,000노드 클러스터의 복잡한 아키텍처를 AI가 시각적 맥락까지 포함하여 이해할 수 있는 강력한 토대가 마련됩니다. 이 과정에서 특정 draw.io 매크로가 깨지는 등의 기술적 이슈가 발생하면 해결 방법을 더 상세히 찾아봐 드릴 수 있습니다.
===
1,000노드 규모의 복잡한 인프라를 운영하다 보면 문서의 '파편화'와 '노후화'는 피할 수 없는 숙제입니다. 특히 AI가 과거의 잘못된 설정(예: 이전 버전의 Cilium BGP 설정)을 정답인 것처럼 제안하면 운영상 큰 리스크가 될 수 있습니다.
Git 저장소에 쌓이는 '쓰레기 데이터'를 정리하고, AI가 가장 최신의, 유효한 지식만 참조하게 만드는 5단계 관리 전략을 제안합니다.
가장 먼저 할 일은 모든 문서의 생애주기(Lifecycle)를 YAML 메타데이터로 정의하는 것입니다. 단순히 저장하는 것이 아니라, LLM이 이 상태를 읽고 필터링하게 만듭니다.
status: active(현재 사용), deprecated(곧 폐기), archive(참조용 구버전)last_verified_at: 마지막으로 기술 검증이 된 날짜applies_to_version: 해당 문서가 유효한 솔루션 버전 (예: Cilium 1.15)Git의 장점은 삭제해도 기록이 남는다는 것입니다. 따라서 현재 시스템과 맞지 않는 문서는 과감히 메인 경로에서 치워야 합니다.
/wiki/active/: 현재 1,000노드 클러스터에 적용 중인 실시간 SOP 및 가이드/wiki/archive/: 과거 기록, 이전 아키텍처 (검색 대상에서 제외하거나 낮은 가중치 부여)Confluence에서 새로운 문서가 수집될 때, 기존 Git에 있는 문서들과 '의미적 유사도'를 비교하는 단계를 추가합니다.
status: archive로 변경하도록 자동 PR(Pull Request)을 생성합니다.검색 시 단순히 '유사도'만 보지 않고, '최신성'에 점수를 더 주는 방식을 도입합니다.
status: active인 문서만 검색하도록 필터를 겁니다.last_verified_at 날짜를 계산식에 넣어, 최근 문서가 상단에 배치되게 합니다.Score = Semantic Similarity + (Recency weight * Time Factor)
문서가 낡는 것을 방지하기 위해 n8n과 Argo Workflows를 활용한 알림 시스템을 구축합니다.
last_verified_at이 6개월이 지난 문서를 추출합니다.archive 폴더로 이동시키고 벡터 DB 인덱스를 삭제합니다.1,000노드 클러스터 운영에서 AI의 답변 정확도를 높이려면 "AI에게 무엇을 가르치지 않을 것인가"가 "무엇을 가르칠 것인가"만큼 중요합니다.
archive/ 폴더를 만드시고, LLM 위키 파이프라인에 "중복 문서 발견 시 기존 문서 아카이브 로직"을 한 줄 추가해 보세요.이렇게 관리하면 시스템 상태와 맞지 않는 '죽은 지식'이 AI의 답변을 오염시키는 일을 원천 차단할 수 있습니다. 이 과정 중 특정 단계(예: 중복 감지 로직)의 구체적인 코드가 필요하신가요?