diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..4d93cc92 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,22 @@ +## 변경 내용 + +변경 목적과 독자가 얻는 결과를 간단히 적어 주세요. + +## 공개 문서 체크리스트 + +- [ ] 사례·가이드·실습·리서치 중 올바른 문서 유형과 폴더를 선택했습니다. +- [ ] 필수 front matter와 `docs-taxonomy.yml`의 분류 값을 사용했습니다. +- [ ] Microsoft/Azure 기술 주장을 Microsoft Learn MCP와 `verify-with-microsoft-learn` skill로 원문까지 확인했습니다. +- [ ] 실제 구독·테넌트 ID, 비밀, 내부 URL·IP·호스트명 등 공개 안전성 문제를 제거했습니다. +- [ ] 필요한 경우 현재 가이드와 시점 고정 사례를 분리하고 서로 연결했습니다. +- [ ] 아래 로컬 검증을 통과했습니다. + +```powershell +python -m pytest tests/docs -q +python scripts/docs/validate_metadata.py +python scripts/docs/validate_sources.py +python scripts/docs/validate_links.py +python scripts/docs/validate_public_safety.py +mkdocs build --strict +python scripts/docs/validate_search_index.py +``` diff --git a/.github/skills/verify-with-microsoft-learn/SKILL.md b/.github/skills/verify-with-microsoft-learn/SKILL.md new file mode 100644 index 00000000..efdaa13a --- /dev/null +++ b/.github/skills/verify-with-microsoft-learn/SKILL.md @@ -0,0 +1,52 @@ +--- +name: verify-with-microsoft-learn +description: Use when creating, revising, or reviewing public technical documentation that contains Microsoft or Azure product claims. +--- + +# Verify with Microsoft Learn + +## Core rule + +Verify product behavior, support status, limits, versions, configuration steps, and CLI or API usage against current official text. A source URL alone is not evidence of a completed review. + +## Workflow + +1. Extract every checkable claim from the changed document. Separate product claims from incident-specific observations. +2. Confirm the `microsoft.docs.mcp` server is connected. Inspect its current tool descriptions through `tools/list`; tool names, parameters, and response shapes can change. +3. Search Microsoft Learn for each claim using the official product name, feature, version, error, or command. Use the discovered documentation search tool rather than general web search. +4. Fetch every selected article in full with the discovered article-fetch tool. Search snippets are only candidates. +5. Compare each claim with the fetched article's scope, prerequisites, supported versions, limitations, and current terminology. +6. Correct unsupported claims. For a historical case, preserve the observation but state its date and contrast it with current documented behavior. +7. For Kubernetes or CNCF behavior that Microsoft Learn does not define, also check the relevant `kubernetes.io` or `cncf.io` source. Microsoft Learn remains mandatory for this repository's Azure context. +8. Record the exact article title and canonical URL in `official_sources`, set `sources_checked_at` to the actual review date, and set `verification_status: verified` only after every material claim is covered. +9. Run `python scripts/docs/validate_metadata.py`, `python scripts/docs/validate_sources.py`, and `python scripts/docs/validate_links.py`. + +If the server is unavailable, no relevant full article can be fetched, or a material conflict remains, set `verification_status: needs-review` and report the unresolved claim. Never use `last_verified` to imply a check that did not occur. + +## Evidence contract + +```yaml +verification_status: verified +sources_checked_at: 2026-09-12 +official_sources: + - title: Azure Kubernetes Service documentation + url: https://learn.microsoft.com/azure/aks/ +``` + +Use at least one claim-relevant `https://learn.microsoft.com/` article. Add upstream official sources when they hold the authoritative implementation detail. + +## Quick reference + +| Claim | Verify | +|---|---| +| Supported feature or version | Availability, region, tier, preview/GA status | +| Configuration procedure | Prerequisites, command syntax, defaults, permissions | +| Limit or performance statement | Units, scope, exceptions, last-updated context | +| Historical incident | Observation date separately from current product behavior | + +## Common mistakes + +- Treating a search snippet as the full source +- Citing a product landing page that does not support the claim +- Using blogs, Q&A, or copied examples when an official article exists +- Marking a document verified while one material claim is unresolved diff --git a/.github/skills/verify-with-microsoft-learn/agents/openai.yaml b/.github/skills/verify-with-microsoft-learn/agents/openai.yaml new file mode 100644 index 00000000..f26e49f6 --- /dev/null +++ b/.github/skills/verify-with-microsoft-learn/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Verify with Microsoft Learn" + short_description: "Verify technical docs against Microsoft Learn" + default_prompt: "Use $verify-with-microsoft-learn to verify this technical documentation against current official sources." diff --git a/.github/workflows/docs-ci.yml b/.github/workflows/docs-ci.yml new file mode 100644 index 00000000..e96f8def --- /dev/null +++ b/.github/workflows/docs-ci.yml @@ -0,0 +1,42 @@ +name: Documentation CI + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + cache: pip + cache-dependency-path: requirements-docs.txt + + - name: Install documentation dependencies + run: python -m pip install -r requirements-docs.txt + + - name: Run documentation contract tests + run: python -m pytest tests/docs -q + + - name: Validate public documentation + run: | + python scripts/docs/validate_metadata.py + python scripts/docs/validate_sources.py + python scripts/docs/validate_links.py + python scripts/docs/validate_public_safety.py + + - name: Build site strictly + run: mkdocs build --strict + + - name: Validate search index + run: python scripts/docs/validate_search_index.py diff --git a/.github/workflows/oryx-python-build-test.yml b/.github/workflows/oryx-python-build-test.yml index 4209b8fc..5f1240c9 100644 --- a/.github/workflows/oryx-python-build-test.yml +++ b/.github/workflows/oryx-python-build-test.yml @@ -3,7 +3,7 @@ name: Oryx Python 3.11 Build Test on: push: paths: - - 'oryx-test/**' + - 'samples/azure-app-service/oryx-test/**' - '.github/workflows/oryx-python-build-test.yml' workflow_dispatch: @@ -20,7 +20,7 @@ jobs: - name: Oryx build Python 3.11 run: | docker run --rm \ - -v ${{ github.workspace }}/oryx-test:/app \ + -v ${{ github.workspace }}/samples/azure-app-service/oryx-test:/app \ mcr.microsoft.com/oryx/build:github-actions-debian-bullseye \ oryx build /app \ --platform python \ @@ -29,4 +29,4 @@ jobs: - name: Verify build output run: | echo "=== Build output ===" - ls -la ./oryx-test/ + ls -la ./samples/azure-app-service/oryx-test/ diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 00000000..9942f783 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,71 @@ +name: Deploy documentation to Pages + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + permissions: + contents: read + pages: read + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + cache: pip + cache-dependency-path: requirements-docs.txt + + - name: Install documentation dependencies + run: python -m pip install -r requirements-docs.txt + + - name: Validate public documentation + run: | + python scripts/docs/validate_metadata.py + python scripts/docs/validate_sources.py + python scripts/docs/validate_links.py + python scripts/docs/validate_public_safety.py + + - name: Build site strictly + run: mkdocs build --strict + + - name: Validate search index + run: python scripts/docs/validate_search_index.py + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v4 + with: + path: site + + deploy: + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 641dc3ac..39f4f8dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,15 @@ node_modules/ .env -.github/skills/ +.github/skills/* +!.github/skills/verify-with-microsoft-learn/ +!.github/skills/verify-with-microsoft-learn/** .venv sim-env.json.vscode/ .mirrord/ __pycache__/ *.py[cod] +.pytest_cache/ +site/ .worktrees/ .superpowers/ monitor/sre-agent-event-lab/evidence/ diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 00000000..4873b588 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,8 @@ +{ + "servers": { + "microsoft-learn": { + "type": "http", + "url": "https://learn.microsoft.com/api/mcp" + } + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..5f173de7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,66 @@ +# Repository instructions + +## Public documentation contract + +- 상세 기준은 `docs/contributing/index.md`를 단일 설명 문서로 사용한다. +- 공개 기술 문서는 `docs////index.md` page bundle로 작성한다. +- ``은 `cases`, `guides`, `labs`, `research` 중 하나이며 front matter의 `document_type`과 일치해야 한다. +- Azure 제품의 ``는 `azure-`와 공식 전체 제품명을 kebab-case로 조합한다(예: `azure-kubernetes-service`). +- Microsoft 제품은 공식 명칭을 보존하여 이름이 `Microsoft`로 시작하면 `microsoft-` slug를 사용하고, 오픈소스와 그 밖의 제품은 공식 프로젝트·제품명을 kebab-case로 사용한다. +- 새 서비스, 기술 또는 태그가 필요할 때만 `docs-taxonomy.yml`을 함께 수정한다. +- 개별 문서를 추가하기 위해 `mkdocs.yml`, `docs/.nav.yml`, README 또는 수동 문서 목록을 수정하지 않는다. 빌드가 폴더와 메타데이터에서 메뉴·색인을 생성한다. + +## Publishing automation + +- 문서의 원본은 Markdown page bundle과 front matter다. 기존 분류의 새 글은 `docs/guides/azure-kubernetes-service//index.md` 같은 bundle만 추가하고, 메뉴에 문서 경로를 수동 등록하지 않는다. +- 메뉴 트리, 문서 유형별·서비스별 목록, 태그와 검색 인덱스는 빌드 시 자동 생성한다. 브라우저에서 Markdown을 다시 읽거나 별도의 수동 문서 목록을 동기화하는 구조를 추가하지 않는다. +- `mkdocs.yml`과 `docs/.nav.yml`은 사이트 공통 설정과 대메뉴 구성에만 사용한다. 새 서비스·기술·태그를 처음 도입할 때의 공통 분류 등록은 `docs-taxonomy.yml`에서 처리한다. +- 홈의 대표 글은 문서의 `featured: true`로 지정한다. 생성기에 특정 문서 경로나 제목 목록을 하드코딩하지 않는다. +- 게시 자동화를 바꿀 때는 Markdown bundle 하나를 추가하는 것만으로 메뉴·목록·검색이 갱신되는지 검증한다. 설정 파일의 문자열이 존재하는지만 검사하는 테스트로 대체하지 않는다. + +## Reader interface + +- 독자는 개발자다. 제목과 설명은 구체적인 증상, 기술적 선택, 관측과 판단의 근거를 드러내고 일반적인 홍보 문구나 관료적인 표현을 피한다. +- 메뉴는 대메뉴(문서 유형) → 중메뉴(서비스) → 소메뉴(문서)로 구분한다. 문서 이름은 front matter의 `title`, 서비스 이름은 taxonomy를 사용하고 bundle 폴더를 불필요한 추가 단계로 노출하지 않는다. +- 그룹은 접기·펼치기가 가능하고 현재 문서의 경로만 기본으로 열린다. `navigation.sections`나 `navigation.expand`로 모든 그룹을 강제로 펼치지 않는다. +- 큰 화면에서는 전체 문서 사이드바를, 작은 화면에서는 햄버거 메뉴를 제공한다. 들여쓰기·굵기·구분선으로 계층을 표시하고 현재 문서를 강조한다. +- 글자 크기는 GitHub Markdown 수준을 기준으로 한다: 본문 16px, H1 32px, H2 24px, H3 20px, 코드 약 13.6px. 홈 제목만 과도하게 키우지 않으며 밝은·어두운 테마와 모바일에서도 실제 크기·가독성을 확인한다. +- 검토 상태와 확인일은 사실대로 메타데이터에 유지하되, 마이그레이션·검토 미완료 같은 관리용 안내를 본문 배너나 목록 배지로 자동 노출하지 않는다. 출처 링크와 실제 기술적 제약·비용 경고는 보존한다. + +## Content lifecycle + +- 발생 시점의 환경, 관측값, 조사와 해결 이력은 `cases`에 보존한다. +- 현재 권장 절차, 지원 상태와 버전별 차이는 `guides`에서 계속 갱신한다. +- 한 문서에 두 성격이 섞이면 사례와 가이드로 분리하고 `related_cases`·`related_guides`로 연결한다. +- 실행 가능한 프로젝트, 배포 매니페스트와 대용량 결과물은 `samples/`에 두고 문서에서는 GitHub 소스 링크를 사용한다. +- 페이지에서 렌더링할 이미지만 같은 bundle의 `images/`에 두며 의미 있는 대체 텍스트를 작성한다. + +## Official-source verification + +- Microsoft 또는 Azure의 동작, 지원 여부, 제한, 버전, 구성 단계, CLI/API 사용법을 추가하거나 바꿀 때 `.github/skills/verify-with-microsoft-learn/SKILL.md`의 `verify-with-microsoft-learn` skill을 반드시 사용한다. +- Microsoft Learn MCP 검색 뒤 선택한 원문 전체를 조회하고, 주장과 적용 범위를 비교한다. +- `official_sources`, `sources_checked_at`, `verification_status`는 실제 검증 결과와 일치시킨다. URL만 추가한 것은 검증이 아니다. +- Microsoft Learn이 최종 권위가 아닌 Kubernetes·CNCF 세부 동작은 upstream 공식 문서도 함께 확인한다. +- 의미 검증을 완료하지 못하면 `verification_status: needs-review`로 남기고 완료했다고 표현하지 않는다. + +## Public safety + +- 고객·조직·사용자 실명, 구독·테넌트 ID, 비밀, 연결 문자열, 내부 URL·IP·호스트명, 개인정보가 담긴 로그와 승인되지 않은 화면을 공개 문서에 넣지 않는다. +- 실제 식별자는 문서 전체에서 일관된 가상 값으로 치환한다. + +## Required validation + +문서 CI는 `tests/docs`와 공개 문서·사이트 검증을 대상으로 한다. SRE 실습 등 `samples/`의 애플리케이션 테스트나 의존성을 문서 CI에 묶지 않는다. 게시 스크립트나 사이트 동작을 변경하면 `python -m pytest tests/docs -q`도 실행한다. + +문서 또는 사이트 구성을 변경한 뒤 저장소 루트에서 실행한다. + +```powershell +python scripts/docs/validate_metadata.py +python scripts/docs/validate_sources.py +python scripts/docs/validate_links.py +python scripts/docs/validate_public_safety.py +mkdocs build --strict +python scripts/docs/validate_search_index.py +``` + +테스트나 검증 실패를 무시하거나 생성된 `site/` 및 검색 인덱스를 커밋하지 않는다. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..a3ccb131 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,18 @@ +# 기여하기 + +새 공개 문서는 `docs////index.md`에 추가합니다. 올바른 폴더와 front matter를 사용하면 메뉴와 색인은 빌드 시 자동 생성됩니다. + +문서 유형 선택, 메타데이터, 본문 구조, 태그, 이미지와 공개 안전성의 기준 문서는 [상세 기여 지침](docs/contributing/index.md)입니다. + +Microsoft 또는 Azure 기술 내용을 새로 쓰거나 바꿀 때는 repository skill `verify-with-microsoft-learn`을 사용해 Microsoft Learn 원문과 대조하고 출처 메타데이터를 갱신해야 합니다. + +제출 전 다음을 실행하세요. + +```powershell +python scripts/docs/validate_metadata.py +python scripts/docs/validate_sources.py +python scripts/docs/validate_links.py +python scripts/docs/validate_public_safety.py +mkdocs build --strict +python scripts/docs/validate_search_index.py +``` diff --git a/README.md b/README.md index 5fc354c1..65944a3f 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,30 @@ # DevGuideSample -> Microsoft CSA(Customer Success Architect) 팀을 위한 Azure 기술 가이드 및 이슈 해결 사례 저장소 +실제 Azure 문제 해결 이력과 지속적으로 갱신하는 기술 가이드를 외부에 공개하는 문서 저장소입니다. -## 📌 저장소 소개 +## 문서 사이트 -이 저장소는 한국 지역에서 발생하는 다양한 Azure 관련 기술 이슈와 솔루션을 체계적으로 정리하고 공유하기 위한 목적으로 만들어졌습니다. Microsoft CSA 팀이 고객 지원 과정에서 경험한 실제 사례와 베스트 프랙티스를 문서화하여, 팀원들 간의 지식 공유와 빠른 문제 해결을 돕습니다. +**[DevGuideSample GitHub Pages에서 검색하기](https://hellices.github.io/devguidesample/)** -## 🎯 활용 플랜 +사이트에서는 한국어·영어 전체 텍스트, 제품명, 오류 메시지와 태그로 모든 공개 문서를 검색할 수 있습니다. -### 1. 지식 베이스로 활용 -- 과거에 해결했던 이슈를 빠르게 검색하고 참고 -- 유사한 문제 발생 시 검증된 솔루션 적용 -- 신규 팀원 온보딩 시 학습 자료로 활용 +| 모음 | 용도 | +|---|---| +| 문제 해결 사례 | 특정 시점의 증상, 조사, 근본 원인과 해결 결과를 보존합니다. | +| 일반 가이드 | 현재 재사용할 수 있는 절차를 제품 변화에 따라 계속 검증하고 갱신합니다. | +| 실습 | 배포, 실행, 검증과 정리를 재현할 수 있는 시나리오입니다. | +| 리서치 | 비교, 벤치마크와 아키텍처 조사를 기준 시점과 함께 기록합니다. | -### 2. 베스트 프랙티스 공유 -- 실제 프로덕션 환경에서 검증된 구성 및 설정 공유 -- Azure 서비스별 최적화 전략 문서화 -- 성능 튜닝 사례 및 트러블슈팅 가이드 제공 +전체 문서 목록은 파일로 중복 관리하지 않습니다. 올바른 폴더와 front matter를 사용하면 Pages 메뉴, 서비스 색인과 태그 색인에 자동으로 포함됩니다. -### 3. 협업 및 확장 -- 팀원들이 새로운 이슈 해결 사례를 지속적으로 추가 -- 코드 예제와 함께 상세한 설명 제공 -- 한국 고객 환경에 특화된 가이드 작성 +## 로컬 미리보기 -## 🧪 실습 랩 - -문서만 읽는 가이드와 달리, 실제 Azure 리소스를 배포해 직접 돌려 보는 실습입니다. 각 랩은 자체 `azure.yaml`을 가지고 있어 `azd up` 한 번으로 환경이 만들어집니다. - -| 랩 | 무엇을 확인하나 | 소요 시간 | 과금 | -|---|---|---|---| -| [Azure SRE Agent 이벤트 기반 장애 분석](monitor/sre-agent-event-lab/README.md) | 장애를 세 번 주입하고 Azure Monitor 경고를 받은 SRE Agent가 원인을 짚어내는지 | 배포 5분 + 시나리오당 30~40분 | 있음 (Container Apps, Log Analytics, Storage 등) | - -각 랩의 README가 사전 조건, 배포, 정리 절차를 안내합니다. **실습을 마치면 반드시 각 랩의 정리 절차를 따라 리소스를 삭제하세요.** - -새 랩을 추가할 때는 `azure.yaml`과 README를 갖춘 디렉터리를 만들고 위 표에 행을 추가합니다. - -## 📝 기여 방법 - -### 새로운 가이드 추가하기 - -1. 적절한 카테고리 폴더 선택 (aks, automation, cosmosdb, mysql, develop, architect) -2. 필요시 하위 폴더 생성 (예: `nodejs/`, `python/` 등) -3. Markdown 파일 작성 시 다음 항목 포함 권장: - - 문제 상황 설명 - - 원인 분석 - - 해결 방법 (코드 예제 포함) - - 참고 링크 - -### 가이드 작성 예시 구조 - -```markdown -# [제목]: 문제 및 해결 방법 간략 설명 - -## 문제 상황 -실제 발생한 이슈에 대한 설명 - -## 원인 분석 -문제의 근본 원인 파악 - -## 해결 방법 -상세한 해결 단계 및 코드 예제 - -## 참고 링크 -- 관련 Microsoft Learn 문서 -- Kubernetes/Azure 공식 문서 +```powershell +python -m venv .venv +.\.venv\Scripts\python -m pip install -r requirements-docs.txt +.\.venv\Scripts\mkdocs serve ``` -## 🎓 대상 독자 - -- Microsoft Customer Success Architect (CSA) -- Azure 기술 지원 엔지니어 -- Azure 클라우드 아키텍트 -- Azure 서비스를 사용하는 개발자 - -## 🔗 관련 리소스 - -- [Microsoft Learn](https://learn.microsoft.com/ko-kr/) -- [Azure Documentation](https://docs.microsoft.com/ko-kr/azure/) -- [Azure Architecture Center](https://learn.microsoft.com/ko-kr/azure/architecture/) -이 저장소의 내용은 Microsoft CSA 팀 내부 지식 공유를 목적으로 합니다. - ---- +자세한 작성·검증 규칙은 [CONTRIBUTING.md](CONTRIBUTING.md)를 확인하세요. -**Last Updated**: 2025-11-17 +> 이 저장소와 Pages는 공개되어 있습니다. 고객·사용자 식별자, 구독·테넌트 ID, 비밀, 내부 URL 또는 승인되지 않은 화면 캡처를 커밋하지 마세요. diff --git a/docs-taxonomy.yml b/docs-taxonomy.yml new file mode 100644 index 00000000..c9a7e400 --- /dev/null +++ b/docs-taxonomy.yml @@ -0,0 +1,95 @@ +collections: + case: + path: cases + title: 트러블슈팅 + description: 느린 응답, 막힌 I/O, 예상과 다른 동작. 관측한 증상에서 시작해 원인 후보를 하나씩 좁혀 간 기록입니다. + statuses: [unresolved, resolved, historical] + required_fields: [occurred_at] + guide: + path: guides + title: 구현 가이드 + description: 구성을 연결하고 동작을 확인하는 데 필요한 절차를 정리했습니다. 내 환경에 맞는 전제와 적용 범위부터 살펴보세요. + statuses: [current, needs-review, deprecated] + required_fields: [last_verified, review_cycle_days, applies_to] + lab: + path: labs + title: 실습 + description: 읽는 데서 끝내지 않고 직접 재현합니다. 배포할 구성, 확인할 결과, 비용과 정리 절차를 함께 살펴보세요. + statuses: [verified, needs-review, broken, archived] + required_fields: [last_verified, review_cycle_days, estimated_time, cost, cleanup_required] + research: + path: research + title: 비교·분석 + description: 어떤 선택이 더 나은지는 조건에 따라 달라집니다. 처리량과 비용, 품질과 복잡도를 비교하고 판단의 근거를 남깁니다. + statuses: [current, superseded, archived] + required_fields: [published_at] + +services: + application-development: Application Development + azure-application-gateway: Azure Application Gateway + azure-ai-search: Azure AI Search + azure-architecture: Azure Architecture + azure-automation: Azure Automation + azure-cosmos-db: Azure Cosmos DB + azure-database-for-mysql: Azure Database for MySQL + azure-hdinsight: Azure HDInsight + azure-kubernetes-service: Azure Kubernetes Service + azure-load-testing: Azure Load Testing + azure-managed-redis: Azure Managed Redis + azure-monitor: Azure Monitor + azure-openai: Azure OpenAI + azure-storage: Azure Storage + microsoft-foundry: Microsoft Foundry + +technologies: + azure-cli: Azure CLI + bicep: Bicep + cosmos-db: Azure Cosmos DB + docker: Docker + dotnet: .NET + gitops: GitOps + go: Go + grafana: Grafana + java: Java + kafka: Apache Kafka + kubernetes: Kubernetes + mysql: MySQL + nodejs: Node.js + open-telemetry: OpenTelemetry + powershell: PowerShell + prometheus: Prometheus + python: Python + rag: Retrieval-augmented generation + redis: Redis + rest-api: REST API + +tags: + ai-agents: AI agents + architecture: Architecture + authentication: Authentication + authorization: Authorization + benchmarking: Benchmarking + deployment: Deployment + development: Development + diagnostics: Diagnostics + disaster-recovery: Disaster recovery + high-availability: High availability + kubernetes: Kubernetes + latency: Latency + migration: Migration + monitoring: Monitoring + networking: Networking + observability: Observability + performance: Performance + reliability: Reliability + security: Security + storage: Storage + troubleshooting: Troubleshooting + +verification_statuses: [verified, needs-review] +required_source_host: learn.microsoft.com +official_source_hosts: + - learn.microsoft.com + - kubernetes.io + - www.cncf.io + - cncf.io diff --git a/docs/.nav.yml b/docs/.nav.yml new file mode 100644 index 00000000..d05f3b30 --- /dev/null +++ b/docs/.nav.yml @@ -0,0 +1,11 @@ +nav: + - 홈: index.md + - 트러블슈팅: cases + - 구현 가이드: guides + - 실습: labs + - 비교·분석: research + - 서비스별 찾기: services + - 태그: tags.md + - 기여하기: contributing + - glob: "*" + ignore_no_matches: true diff --git a/docs/assets/stylesheets/extra.css b/docs/assets/stylesheets/extra.css new file mode 100644 index 00000000..50106cef --- /dev/null +++ b/docs/assets/stylesheets/extra.css @@ -0,0 +1,910 @@ +:root, +[data-md-color-scheme] { + --dg-font-body: 0.8rem; + --dg-font-h1: 1.6rem; + --dg-font-h2: 1.2rem; + --dg-font-h3: 1rem; + --dg-font-code: 0.68rem; + --dg-bg: #f8fafc; + --dg-surface: #ffffff; + --dg-soft: #eef2f7; + --dg-text: #182230; + --dg-muted: #566578; + --dg-border: #dbe2eb; + --dg-accent: #1759c0; + --dg-accent-soft: #eaf1ff; + --dg-on-accent: #ffffff; + --dg-case: #b4522d; + --dg-guide: #1759c0; + --dg-lab: #087568; + --dg-research: #7450a3; + --md-default-bg-color: var(--dg-bg); + --md-default-fg-color: var(--dg-text); + --md-default-fg-color--light: var(--dg-muted); + --md-default-fg-color--lighter: #8693a5; + --md-default-fg-color--lightest: var(--dg-border); + --md-primary-fg-color: var(--dg-accent); + --md-primary-bg-color: var(--dg-on-accent); + --md-accent-fg-color: var(--dg-accent); + --md-accent-fg-color--transparent: var(--dg-accent-soft); + --md-typeset-a-color: var(--dg-accent); + --md-code-bg-color: #edf2f8; + --md-code-fg-color: #24354c; +} + +[data-md-color-scheme="slate"] { + --dg-bg: #111720; + --dg-surface: #18212d; + --dg-soft: #1e2a39; + --dg-text: #e7edf5; + --dg-muted: #a4b2c6; + --dg-border: #2c394b; + --dg-accent: #9bbcff; + --dg-accent-soft: #202f49; + --dg-on-accent: #101b2c; + --dg-case: #f1ad88; + --dg-guide: #9bbcff; + --dg-lab: #7dd6c3; + --dg-research: #c3acf0; + --md-default-fg-color--lighter: #7e90aa; + --md-code-bg-color: #1c2839; + --md-code-fg-color: #d5e3f7; +} + +body { + -webkit-font-smoothing: antialiased; +} + +.md-grid { + max-width: 74rem; +} + +.md-header { + background: var(--dg-bg); + color: var(--dg-text); + border-bottom: 1px solid var(--dg-border); +} + +.md-header { + box-shadow: none; +} + +.md-header__title { + font-weight: 700; + letter-spacing: -0.035em; +} + +.md-search__form { + background: var(--dg-soft); + border: 1px solid var(--dg-border); + border-radius: 0.35rem; +} + +.md-search__input, +.md-search__input + .md-search__icon { + color: var(--dg-text); +} + +.md-search__input::placeholder { + color: var(--dg-muted); +} + +.md-nav { + font-size: 0.65rem; + line-height: 1.7; +} + +.md-nav__link--active { + font-weight: 600; +} + +.md-nav--primary > .md-nav__list > .md-nav__item { + margin-bottom: 0.3rem; +} + +.md-nav--primary > .md-nav__list > .md-nav__item > .md-nav__link { + font-size: 0.7rem; + font-weight: 700; +} + +.md-nav--primary .md-nav[data-md-level="1"] > .md-nav__list > .md-nav__item > .md-nav__link { + font-size: 0.65rem; + font-weight: 600; +} + +.md-nav--primary .md-nav[data-md-level="2"] .md-nav__link:not(.md-nav__link--active) { + color: var(--dg-muted); + font-size: 0.65rem; + font-weight: 400; +} + +.md-nav--primary .md-nav__link { + border-radius: 0.2rem; + line-height: 1.6; +} + +.md-nav--primary .md-nav__link--active { + color: var(--dg-accent); + background: var(--dg-accent-soft); + box-shadow: inset 2px 0 var(--dg-accent); +} + +@media (min-width: 1220px) { + .md-nav--primary .md-nav[data-md-level="1"], + .md-nav--primary .md-nav[data-md-level="2"] { + border-left: 1px solid var(--dg-border); + margin-left: 0.3rem; + padding-left: 0.5rem; + } +} + +.md-main__inner { + margin-top: 1.25rem; +} + +.md-content { + min-width: 0; +} + +.md-content__inner { + max-width: 40rem; + margin-inline: auto; + padding-inline: 1rem; +} + +.md-content__inner:has(.dg-landing) { + max-width: none; + margin-inline: 0; + padding-inline: 1.25rem; +} + +.md-content__inner:has(.dg-landing) > .md-content__button { + display: none; +} + +.md-typeset { + font-size: var(--dg-font-body); + line-height: 1.5; + letter-spacing: -0.02em; + word-break: keep-all; + overflow-wrap: anywhere; +} + +.md-typeset h1, +.md-typeset h2, +.md-typeset h3 { + color: var(--dg-text); + font-weight: 600; + letter-spacing: -0.035em; +} + +.md-typeset h1 { + margin-bottom: 0.8rem; + font-size: var(--dg-font-h1); + line-height: 1.25; +} + +.md-typeset h2 { + margin-top: 1.5rem; + font-size: var(--dg-font-h2); + line-height: 1.25; +} + +.md-typeset h3 { + font-size: var(--dg-font-h3); + line-height: 1.25; +} + +.md-typeset :is(h1, h2, h3)[id] { + scroll-margin-top: 6rem; +} + +.md-typeset a { + text-underline-offset: 0.2em; +} + +.md-typeset a:focus-visible, +.md-header a:focus-visible { + outline: 2px solid var(--dg-accent); + outline-offset: 4px; + border-radius: 2px; +} + +.md-typeset code, +.md-typeset pre { + font-size: var(--dg-font-code); + letter-spacing: 0; + word-break: normal; +} + +.md-typeset pre { + line-height: 1.5; +} + +.md-typeset .highlight, +.md-typeset .highlighttable { + border: 1px solid var(--dg-border); + border-radius: 0.4rem; + overflow: hidden; +} + +.md-typeset .highlight > pre { + margin: 0; +} + +.md-typeset pre > code { + padding: 1rem; +} + +.md-typeset blockquote { + color: var(--dg-muted); + border-left-color: var(--dg-border); +} + +.md-typeset table:not([class]) { + background: var(--dg-surface); + border-color: var(--dg-border); + border-radius: 0.35rem; + font-size: 0.72rem; + line-height: 1.75; +} + +.md-typeset table:not([class]) th { + background: var(--dg-soft); + color: var(--dg-text); +} + +.md-typeset .admonition, +.md-typeset details { + font-size: 0.74rem; + line-height: 1.8; + box-shadow: none; +} + +.md-typeset .dg-article-lead { + color: var(--dg-muted); + font-size: var(--dg-font-body); + line-height: 1.6; + margin: 1rem 0 1.5rem; +} + +.md-typeset details.doc-sources { + background: var(--dg-surface); + border-color: var(--dg-border); + margin-top: 2rem; + font-size: 0.7rem; +} + +.md-typeset details.doc-sources > summary { + background: var(--dg-soft); + color: var(--dg-text); +} + +.dg-landing { + max-width: 60rem; + margin-inline: auto; + padding-block: 0.5rem 3rem; +} + +.md-typeset .dg-eyebrow { + color: var(--dg-muted); + font-family: var(--md-code-font-family); + font-size: 0.55rem; + font-weight: 500; + line-height: 1.8; + letter-spacing: 0.12em; + margin: 0 0 0.8rem; +} + +.md-typeset .dg-lead { + color: var(--dg-muted); + font-size: var(--dg-font-body); + line-height: 1.6; + max-width: 44rem; + margin: 1rem 0 0; +} + +.dg-hero { + display: grid; + grid-template-columns: 1.25fr 0.85fr; + gap: 2rem; + align-items: center; + padding-block: 1.2rem 1.4rem; +} + +.md-typeset .dg-hero h1 { + font-size: var(--dg-font-h1); + line-height: 1.25; + letter-spacing: -0.035em; + margin: 0.6rem 0 0.8rem; +} + +.dg-actions { + margin-top: 1.2rem; +} + +.dg-actions p { + display: flex; + flex-wrap: wrap; + gap: 0.55rem; + margin: 0; +} + +.md-typeset .dg-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 2.2rem; + padding: 0.55rem 0.9rem; + border: 1px solid var(--dg-border); + border-radius: 0.35rem; + font-size: 0.7rem; + font-weight: 600; + line-height: 1.6; + transition: background 160ms ease, border-color 160ms ease; +} + +.md-typeset .dg-button--primary { + background: var(--dg-accent); + border-color: var(--dg-accent); + color: var(--dg-on-accent); +} + +.md-typeset .dg-button--secondary { + background: var(--dg-surface); + color: var(--dg-text); +} + +.md-typeset .dg-button:hover { + border-color: var(--dg-accent); + filter: brightness(0.95); +} + +.dg-hero-topics { + border-left: 1px solid var(--dg-border); + padding-left: 1.6rem; +} + +.dg-hero-topics p { + margin: 0; +} + +.md-typeset .dg-topic-link { + display: block; + color: var(--dg-muted); + border-top: 1px solid var(--dg-border); + padding: 0.9rem 0; + font-size: 0.74rem; + line-height: 1.8; +} + +.dg-topic-link strong { + color: var(--dg-text); + font-size: var(--dg-font-body); +} + +.md-typeset .dg-topic-link:hover strong { + color: var(--dg-accent); +} + +.dg-stats { + display: flex; + flex-wrap: wrap; + gap: 1.5rem 3rem; + border-block: 1px solid var(--dg-border); + padding-block: 0.8rem; +} + +.dg-stat { + display: flex; + gap: 0.65rem; + align-items: baseline; +} + +.dg-stat strong { + font-family: var(--md-code-font-family); + font-size: 1.1rem; + line-height: 1.3; + font-weight: 500; + letter-spacing: -0.07em; +} + +.dg-stat span { + color: var(--dg-muted); + font-size: 0.65rem; +} + +.dg-section-heading { + margin-top: 2rem; + margin-bottom: 0.9rem; +} + +.md-typeset .dg-section-heading h2 { + margin: 0; + font-size: var(--dg-font-h2); +} + +.dg-section-heading > p:not(.dg-eyebrow) { + color: var(--dg-muted); + font-size: 0.78rem; + margin: 0.35rem 0 0; +} + +.dg-page-heading { + border-bottom: 1px solid var(--dg-border); + padding-block: 1.1rem 1.2rem; + margin-bottom: 1.4rem; +} + +.md-typeset .dg-page-heading h1 { + margin: 0; + font-size: var(--dg-font-h1); + line-height: 1.25; +} + +.dg-jump-links { + margin-block: 1.2rem 2rem; +} + +.dg-jump-links p { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + +.md-typeset .dg-jump-links a { + display: inline-flex; + align-items: center; + min-height: 1.9rem; + padding: 0.3rem 0.7rem; + background: var(--dg-surface); + color: var(--dg-muted); + border: 1px solid var(--dg-border); + border-radius: 2rem; + font-size: 0.65rem; + line-height: 1.5; +} + +.md-typeset .dg-jump-links a:hover { + background: var(--dg-accent-soft); + border-color: var(--dg-accent); + color: var(--dg-accent); +} + +.dg-doc-grid, +.dg-feature-grid, +.dg-collection-grid, +.dg-service-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.9rem; +} + +.dg-doc-card, +.dg-collection-card, +.dg-service-card { + --dg-card-accent: var(--dg-accent); + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + padding: 1rem; + background: var(--dg-surface); + border: 1px solid var(--dg-border); + border-radius: 0.5rem; + transition: border-color 160ms ease, transform 160ms ease; +} + +.dg-doc-card:hover, +.dg-collection-card:hover, +.dg-service-card:hover { + border-color: var(--dg-card-accent); + transform: translateY(-2px); +} + +.dg-doc-card--case, +.dg-collection-card--case { + --dg-card-accent: var(--dg-case); +} + +.dg-doc-card--guide, +.dg-collection-card--guide { + --dg-card-accent: var(--dg-guide); +} + +.dg-doc-card--lab, +.dg-collection-card--lab { + --dg-card-accent: var(--dg-lab); +} + +.dg-doc-card--research, +.dg-collection-card--research { + --dg-card-accent: var(--dg-research); +} + +.dg-doc-meta { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.35rem 0.75rem; + color: var(--dg-muted); + font-family: var(--md-code-font-family); + font-size: 0.6rem; + line-height: 1.8; +} + +.dg-doc-meta > :first-child, +.dg-collection-card .dg-eyebrow { + color: var(--dg-card-accent); +} + +.md-typeset :is(.dg-doc-card, .dg-collection-card, .dg-service-card) :is(h2, h3) { + font-size: 0.9rem; + line-height: 1.4; + margin: 0.65rem 0; +} + +.md-typeset :is(.dg-doc-card, .dg-collection-card, .dg-service-card) :is(h2, h3) a { + color: var(--dg-text); +} + +.md-typeset :is(.dg-doc-card, .dg-collection-card, .dg-service-card) :is(h2, h3) a::after { + content: ""; + position: absolute; + inset: 0; + border-radius: 0.5rem; +} + +.md-typeset :is(.dg-doc-card, .dg-collection-card, .dg-service-card) :is(h2, h3) .headerlink { + display: none; +} + +.md-typeset .dg-doc-summary { + color: var(--dg-muted); + font-size: 0.7rem; + line-height: 1.6; + margin: 0 0 0.95rem; +} + +.dg-doc-tags { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-top: 0.3rem; +} + +.dg-tag { + color: var(--dg-muted); + background: var(--dg-soft); + border-radius: 0.2rem; + padding: 0.13rem 0.4rem; + font-family: var(--md-code-font-family); + font-size: 0.6rem; + line-height: 1.75; + letter-spacing: 0; +} + +.dg-doc-details { + color: var(--dg-muted); + font-size: 0.62rem; + line-height: 1.85; + margin-top: auto; + padding-top: 0.8rem; +} + +.dg-doc-details > span { + display: inline-block; + margin-right: 0.65rem; +} + +.dg-feature-grid { + grid-template-columns: 1.15fr 1fr; +} + +.dg-feature-card:first-child { + grid-row: span 2; + padding: 1.2rem; + border-top: 3px solid var(--dg-card-accent); +} + +.md-typeset .dg-feature-card:first-child h3 { + font-size: var(--dg-font-h3); + line-height: 1.4; + margin-block: 0.7rem; +} + +.md-typeset .dg-feature-card:first-child .dg-doc-summary { + font-size: var(--dg-font-body); +} + +.dg-feature-card:first-child .dg-doc-tags { + margin-top: auto; +} + +.dg-feature-card:first-child .dg-doc-details { + margin-top: 0; +} + +.dg-collection-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.dg-collection-card { + padding: 1.1rem; +} + +.dg-card-count { + color: var(--dg-muted); + font-family: var(--md-code-font-family); + font-size: 0.65rem; + margin-top: auto; + padding-top: 0.6rem; +} + +.md-typeset .dg-collection > h2, +.md-typeset .dg-service > h2 { + font-size: var(--dg-font-h3); + margin-top: 2rem; +} + +.dg-collection--guide .dg-doc-grid { + grid-template-columns: minmax(0, 1fr); + gap: 0; +} + +.dg-collection--guide .dg-doc-card { + display: grid; + grid-template-columns: minmax(0, 1fr) 12rem; + gap: 0.3rem 1.6rem; + padding: 1.25rem 0; + background: transparent; + border-width: 0 0 1px; + border-radius: 0; +} + +.dg-collection--guide .dg-doc-card:hover { + transform: none; +} + +.dg-collection--guide .dg-doc-meta, +.dg-collection--guide .dg-doc-card h3, +.dg-collection--guide .dg-doc-summary, +.dg-collection--guide .dg-doc-details { + grid-column: 1; + margin-block: 0; +} + +.dg-collection--guide .dg-doc-tags { + grid-column: 2; + grid-row: 2 / span 2; + align-content: start; + justify-content: flex-end; +} + +.dg-collection--lab .dg-doc-details { + margin-top: 0.6rem; + padding: 0.65rem; + border-radius: 0.25rem; + background: var(--dg-soft); +} + +.dg-collection--research .dg-doc-grid, +.dg-service-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.dg-service-card { + padding: 1.1rem 1.3rem; +} + +.dg-service-card p, +.dg-service-card ul { + color: var(--dg-muted); + font-size: 0.65rem; +} + +.dg-service-card ul { + list-style: none; + padding: 0; + margin: 0; +} + +.dg-service-card li { + margin-left: 0; +} + +.dg-tags > h2 { + border-top: 1px solid var(--dg-border); + padding-top: 1.25rem; +} + +.dg-tags > ul { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.4rem 1.5rem; + list-style: none; +} + +.dg-tags > ul > li { + padding-block: 0.4rem; + margin-left: 0; + font-size: 0.76rem; +} + +.dg-contribute { + border-top: 1px solid var(--dg-border); + margin-top: 2.5rem; + padding-top: 1.6rem; +} + +.dg-contribute p, +.dg-contribution-steps { + font-size: 0.78rem; +} + +.dg-contribute p:not(:first-child) { + color: var(--dg-muted); + max-width: 40rem; +} + +.md-typeset .dg-text-link { + font-weight: 600; +} + +.dg-contribution-steps { + background: var(--dg-surface); + border: 1px solid var(--dg-border); + border-radius: 0.4rem; + padding: 0.5rem 1rem; + margin-block: 1.5rem; +} + +.dg-contribution-steps li { + padding-block: 0.25rem; +} + +.md-footer, +.md-footer-meta { + background: var(--dg-bg); + color: var(--dg-muted); +} + +.md-footer { + border-top: 1px solid var(--dg-border); +} + +.md-copyright, +.md-copyright__highlight { + color: var(--dg-muted); +} + +@media (max-width: 1219px) { + .md-nav--primary .md-nav__title[for="__drawer"] { + height: auto; + padding: 0.8rem; + background: var(--dg-surface); + color: var(--dg-text); + font-size: 0.8rem; + line-height: 1.5; + box-shadow: none; + } + + .md-nav--primary .md-nav__title .md-logo { + display: none; + } + + .md-nav--primary .md-nav__source { + background: var(--dg-soft); + color: var(--dg-text); + } +} + +@media (max-width: 1100px) { + .dg-hero { + grid-template-columns: 1.1fr 0.9fr; + gap: 1.5rem; + } + + .dg-collection-grid, + .dg-collection--research .dg-doc-grid, + .dg-service-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 760px) { + .md-content__inner, + .md-content__inner:has(.dg-landing) { + padding-inline: 0.8rem; + } + + .dg-landing { + padding-top: 0; + } + + .dg-hero { + grid-template-columns: minmax(0, 1fr); + gap: 1.5rem; + padding-block: 1.4rem 1.8rem; + } + + .dg-hero-topics { + display: none; + } + + .dg-stats { + gap: 0.8rem 1.25rem; + } + + .dg-stat { + gap: 0.4rem; + } + + .dg-doc-grid, + .dg-feature-grid, + .dg-collection--research .dg-doc-grid, + .dg-service-grid, + .dg-tags > ul { + grid-template-columns: minmax(0, 1fr); + } + + .dg-feature-card:first-child { + grid-row: auto; + padding: 1.3rem; + } + + .dg-collection--guide .dg-doc-card { + grid-template-columns: minmax(0, 1fr); + gap: 0.45rem; + } + + .dg-collection--guide .dg-doc-card > * { + grid-column: 1; + grid-row: auto; + } + + .dg-collection--guide .dg-doc-tags { + justify-content: flex-start; + } + + .dg-page-heading { + padding-top: 1rem; + } + +} + +@media (max-width: 480px) { + .dg-collection-grid { + grid-template-columns: minmax(0, 1fr); + } + + .dg-actions p { + flex-direction: column; + align-items: stretch; + } + + .dg-stats { + justify-content: space-between; + gap: 0.75rem; + } + + .dg-stat { + flex-direction: column; + gap: 0.1rem; + } +} + +@media (prefers-reduced-motion: reduce) { + .dg-doc-card, + .dg-collection-card, + .dg-service-card, + .md-typeset .dg-button { + transition: none; + } + + .dg-doc-card:hover, + .dg-collection-card:hover, + .dg-service-card:hover { + transform: none; + } +} diff --git a/aisearch/eventual_consistency_reindex.md b/docs/cases/azure-ai-search/eventual-consistency-reindex/index.md similarity index 93% rename from aisearch/eventual_consistency_reindex.md rename to docs/cases/azure-ai-search/eventual-consistency-reindex/index.md index 9ca93f61..ab12d5ed 100644 --- a/aisearch/eventual_consistency_reindex.md +++ b/docs/cases/azure-ai-search/eventual-consistency-reindex/index.md @@ -1,3 +1,23 @@ +--- +services: +- azure-ai-search +official_sources: +- title: Azure AI Search documentation + url: https://learn.microsoft.com/azure/search/ +document_type: case +status: resolved +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: 버전 기반 재색인의 Eventual Consistency 이슈 +description: Azure AI Search 버전 기반 재색인에서 관측한 eventual consistency 문제와 대응을 기록합니다. +technologies: +- python +tags: +- reliability +- troubleshooting +occurred_at: 2026-04-17 +--- + # Azure AI Search 버전 기반 재색인의 Eventual Consistency 이슈 ## 개요 @@ -151,7 +171,7 @@ Step 3가 완전히 결정적이 되고 replica lag의 영향을 받지 않습 ## 직접 재현해 보기 -자급식 재현 스크립트: [repro.py](./repro.py). Azure AI Search 서비스가 미리 준비되어 있어야 합니다. +자급식 재현 스크립트: [repro.py](https://github.com/hellices/devguidesample/blob/main/samples/azure-ai-search/source-material/repro.py). Azure AI Search 서비스가 미리 준비되어 있어야 합니다. ```bash # 1. Azure AI Search 서비스 생성 (35k 문서면 Basic 도 충분. diff --git a/aks/file_io_throttling.md b/docs/cases/azure-kubernetes-service/file-io-throttling/index.md similarity index 79% rename from aks/file_io_throttling.md rename to docs/cases/azure-kubernetes-service/file-io-throttling/index.md index dd94cb08..ad2c273d 100644 --- a/aks/file_io_throttling.md +++ b/docs/cases/azure-kubernetes-service/file-io-throttling/index.md @@ -1,3 +1,24 @@ +--- +services: +- azure-kubernetes-service +official_sources: +- title: Azure Kubernetes Service documentation + url: https://learn.microsoft.com/azure/aks/ +document_type: case +status: resolved +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: AKS Pod File I/O Throttling 분석 +description: AKS Pod에서 관측한 파일 I/O throttling의 증상과 분석 결과를 기록합니다. +technologies: +- kubernetes +tags: +- storage +- performance +- troubleshooting +occurred_at: 2025-11-05 +--- + # AKS Pod File I/O Throttling 분석 ## 개요 diff --git a/aks/file_io_throttling2.md b/docs/cases/azure-kubernetes-service/netapp-files-cpu-io-wait/index.md similarity index 96% rename from aks/file_io_throttling2.md rename to docs/cases/azure-kubernetes-service/netapp-files-cpu-io-wait/index.md index 3e8092f3..3fadfbf9 100644 --- a/aks/file_io_throttling2.md +++ b/docs/cases/azure-kubernetes-service/netapp-files-cpu-io-wait/index.md @@ -1,8 +1,29 @@ +--- +services: +- azure-kubernetes-service +official_sources: +- title: Azure Kubernetes Service documentation + url: https://learn.microsoft.com/azure/aks/ +document_type: case +status: resolved +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: Azure NetApp Files 환경 CPU 급증과 I/O 대기 +description: AKS와 Azure NetApp Files 환경의 CPU 급증 및 파일 I/O 대기 이력을 기록합니다. +technologies: +- kubernetes +tags: +- storage +- performance +- troubleshooting +occurred_at: 2025-11-07 +--- + # AKS NetApp Files 환경에서의 CPU 급증 및 File I/O 대기 이슈 ## 개요 -AKS 환경에서 NetApp Files를 사용하는 Node.js 애플리케이션의 트래픽 증가 시 CPU 급증과 File I/O 대기 현상을 분석한 사례입니다. 이전 용량 증설로 NFS write 지연 문제를 해결했으나([file_io_throttling.md](./file_io_throttling.md) 참조), 추가적인 병목 현상이 발견되었습니다. +AKS 환경에서 NetApp Files를 사용하는 Node.js 애플리케이션의 트래픽 증가 시 CPU 급증과 File I/O 대기 현상을 분석한 사례입니다. 이전 용량 증설로 NFS write 지연 문제를 해결했으나([파일 I/O 스로틀링 사례](../file-io-throttling/index.md) 참조), 추가적인 병목 현상이 발견되었습니다. ## 환경 구성 diff --git a/aks/pod_db_query_latency/01_query_latency.png b/docs/cases/azure-kubernetes-service/pod-database-query-latency/images/01_query_latency.png similarity index 100% rename from aks/pod_db_query_latency/01_query_latency.png rename to docs/cases/azure-kubernetes-service/pod-database-query-latency/images/01_query_latency.png diff --git a/aks/pod_db_query_latency/02_pod_runtime.png b/docs/cases/azure-kubernetes-service/pod-database-query-latency/images/02_pod_runtime.png similarity index 100% rename from aks/pod_db_query_latency/02_pod_runtime.png rename to docs/cases/azure-kubernetes-service/pod-database-query-latency/images/02_pod_runtime.png diff --git a/aks/pod_db_query_latency/pod_db_query_latency.png b/docs/cases/azure-kubernetes-service/pod-database-query-latency/images/pod_db_query_latency.png similarity index 100% rename from aks/pod_db_query_latency/pod_db_query_latency.png rename to docs/cases/azure-kubernetes-service/pod-database-query-latency/images/pod_db_query_latency.png diff --git a/aks/pod_db_query_latency/result_01.png b/docs/cases/azure-kubernetes-service/pod-database-query-latency/images/result_01.png similarity index 100% rename from aks/pod_db_query_latency/result_01.png rename to docs/cases/azure-kubernetes-service/pod-database-query-latency/images/result_01.png diff --git a/aks/pod_db_query_latency/result_02.png b/docs/cases/azure-kubernetes-service/pod-database-query-latency/images/result_02.png similarity index 100% rename from aks/pod_db_query_latency/result_02.png rename to docs/cases/azure-kubernetes-service/pod-database-query-latency/images/result_02.png diff --git a/aks/pod_db_query_latency/result_pod_runtime.png b/docs/cases/azure-kubernetes-service/pod-database-query-latency/images/result_pod_runtime.png similarity index 100% rename from aks/pod_db_query_latency/result_pod_runtime.png rename to docs/cases/azure-kubernetes-service/pod-database-query-latency/images/result_pod_runtime.png diff --git a/aks/pod_db_query_latency.md b/docs/cases/azure-kubernetes-service/pod-database-query-latency/index.md similarity index 94% rename from aks/pod_db_query_latency.md rename to docs/cases/azure-kubernetes-service/pod-database-query-latency/index.md index 4e711dda..9c9e17a5 100644 --- a/aks/pod_db_query_latency.md +++ b/docs/cases/azure-kubernetes-service/pod-database-query-latency/index.md @@ -1,15 +1,38 @@ +--- +services: +- azure-kubernetes-service +official_sources: +- title: Azure Kubernetes Service documentation + url: https://learn.microsoft.com/azure/aks/ +document_type: case +status: resolved +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: 'AKS 쿼리 지연: DB 밖에서 병목 찾기' +description: P95는 안정적인데 P99가 튀는 상황에서 시작합니다. CPU throttling, 커넥션 풀, 네트워크를 차례로 확인하며 원인을 좁혀 간 기록입니다. +featured: true +technologies: +- kubernetes +- nodejs +tags: +- latency +- networking +- troubleshooting +occurred_at: 2026-06-08 +--- + # AKS Pod → Database 쿼리 지연 디버깅 DB 쿼리 응답이 **30ms를 초과하면 fail 처리**되는 시나리오에서, 지연의 원인을 단계별로 좁혀가는 방법. -![Architecture](./pod_db_query_latency/pod_db_query_latency.png) +![Architecture](images/pod_db_query_latency.png) ## 현재 상황: 메트릭 분석 ### DB 쿼리 지연 -![MySQL query latency](./pod_db_query_latency/01_query_latency.png) +![MySQL query latency](images/01_query_latency.png) | 메트릭 | 관측 값 | 판단 | |---|---|---| @@ -21,7 +44,7 @@ P95는 SLA 내인데 P99부터 30ms를 넘고, max는 300ms까지 치솟는다. ### Pod 런타임 상태 -![Pod runtime metrics](./pod_db_query_latency/02_pod_runtime.png) +![Pod runtime metrics](images/02_pod_runtime.png) | 메트릭 | 관측 값 | |---|---| @@ -57,7 +80,7 @@ resources: - P99가 내려감 → throttling이 기여하고 있었음. limit 조정 또는 제거로 대응. - P99가 안 내려감 → throttling은 원인이 아님. **Step 2로.** -> CFS throttling이 평균 CPU가 낮은데도 왜 발생하는지, requests/limits 중 무엇을 조정해야 하는지에 대한 심층 분석은 [Appendix C](#appendix-c-cfs-throttling-심층-분석-nodejs-burst-모델)를 참고. +> CFS throttling이 평균 CPU가 낮은데도 왜 발생하는지, requests/limits 중 무엇을 조정해야 하는지에 대한 심층 분석은 [Appendix C](#appendix-c-cfs-throttling-nodejs-burst)를 참고. ### Step 2: 커넥션 풀 대기 배제 @@ -146,7 +169,7 @@ nohup kubectl logs -f -n -c debugger-XXXXX \ ## 적용 결과: CPU limit 2 core 상향 Step 1 가설 검증 — `limits.cpu`를 `1` → `2`로 상향하고 `requests.cpu`는 `1` 유지. -QoS는 Guaranteed → **Burstable** 로 강등. Appendix [C.5](#c5-권장-조치) 옵션 A(`req=lim=2`)와 옵션 B(`req=500m / lim=2`)의 중간 형태로, 노드 capacity 부담을 늘리지 않으면서 burst 천장만 2배 확보하는 선택. +QoS는 Guaranteed → **Burstable** 로 강등. Appendix [C.5](#c5) 옵션 A(`req=lim=2`)와 옵션 B(`req=500m / lim=2`)의 중간 형태로, 노드 capacity 부담을 늘리지 않으면서 burst 천장만 2배 확보하는 선택. ```yaml resources: @@ -158,13 +181,13 @@ resources: memory: 1Gi ``` -![Deployment YAML (cpu limit 2)](./pod_db_query_latency/result_01.png) +![Deployment YAML (cpu limit 2)](images/result_01.png) ### 변화 전후 비교 -![APM endpoint latency after change](./pod_db_query_latency/result_02.png) +![APM endpoint latency after change](images/result_02.png) -![Pod runtime after change](./pod_db_query_latency/result_pod_runtime.png) +![Pod runtime after change](images/result_pod_runtime.png) | 지표 | Before (limit 1 core) | After (limit 2 core) | 판정 | |---|---|---|---| @@ -176,7 +199,7 @@ resources: | GC pause P95 | 1~2 ms | 5~8 ms | **소폭 증가** (아래 참고) | | heap used | ~128 MB | ~110 MB | 변화 없음 | -P99가 SLA(30 ms) 내로 안정화되고 max도 ~28 ms로 떨어져 **fail 처리 구간이 사라짐**. throttle이 거의 사라졌으므로 Appendix [C.3.2 / C.3.4](#c32-수정된-가설)의 가설 — "burst가 1 core quota를 자주 초과해 큐가 누적되며 tail이 비선형으로 증폭된다" — 가 본 케이스의 지배적 원인이었음이 확인됨. +P99가 SLA(30 ms) 내로 안정화되고 max도 ~28 ms로 떨어져 **fail 처리 구간이 사라짐**. throttle이 거의 사라졌으므로 Appendix [C.3.2 / C.3.4](#c32)의 가설 — "burst가 1 core quota를 자주 초과해 큐가 누적되며 tail이 비선형으로 증폭된다" — 가 본 케이스의 지배적 원인이었음이 확인됨. ### 관전 포인트 diff --git a/redis/nodejs/cluster_failover_recovery.md b/docs/cases/azure-managed-redis/cluster-failover-recovery/index.md similarity index 92% rename from redis/nodejs/cluster_failover_recovery.md rename to docs/cases/azure-managed-redis/cluster-failover-recovery/index.md index ef6d9406..208a9bcb 100644 --- a/redis/nodejs/cluster_failover_recovery.md +++ b/docs/cases/azure-managed-redis/cluster-failover-recovery/index.md @@ -1,3 +1,25 @@ +--- +services: +- azure-managed-redis +official_sources: +- title: Azure Managed Redis documentation + url: https://learn.microsoft.com/azure/redis/ +document_type: case +status: resolved +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: Azure Managed Redis Cluster Failover 분석 +description: Azure Managed Redis cluster failover 장애의 분석과 복구 전략을 기록합니다. +technologies: +- nodejs +- redis +tags: +- disaster-recovery +- high-availability +- troubleshooting +occurred_at: 2026-02-22 +--- + # Azure Managed Redis Cluster Failover 장애 분석 및 복구 전략 ## 문제 정의 @@ -215,7 +237,7 @@ async function safeRedisGet(key) { ## 코드 샘플 -이 이슈를 검증하기 위한 예제 코드는 [`cluster_example/`](cluster_example/) 디렉토리에 포함되어 있습니다. +이 이슈를 검증하기 위한 예제 코드는 [`cluster_example/`](https://github.com/hellices/devguidesample/tree/main/samples/azure-managed-redis/source-material/nodejs/cluster_example) 디렉토리에 포함되어 있습니다. - OSS Cluster(`createCluster`) vs Enterprise(`createClient`) 연결 방식 비교 - Cluster topology 조회 및 모니터링 diff --git a/docs/contributing/case-template.md b/docs/contributing/case-template.md new file mode 100644 index 00000000..b9281345 --- /dev/null +++ b/docs/contributing/case-template.md @@ -0,0 +1,44 @@ +--- +title: 문제 해결 사례 템플릿 +description: 시점 고정 문제 해결 이력을 작성하는 형식입니다. +--- + +# 문제 해결 사례 템플릿 + +## Front matter + +```yaml +--- +title: 사례 제목 +description: 증상, 영향과 해결을 요약한 한 문장 +document_type: case +services: [azure-kubernetes-service] +technologies: [kubernetes] +tags: [troubleshooting] +status: resolved +verification_status: verified +sources_checked_at: 2026-09-12 +official_sources: + - title: 관련 Microsoft Learn 원문 제목 + url: https://learn.microsoft.com/azure/aks/ +occurred_at: 2026-09-01 +resolved_at: 2026-09-02 +related_guides: [] +--- +``` + +## 본문 순서 + +```markdown +# 사례 제목 +## 요약 +## 발생 환경과 영향 +## 증상과 관측 데이터 +## 조사 과정과 타임라인 +## 근본 원인 +## 해결 방법 +## 검증 결과 +## 재발 방지와 교훈 +## 관련 최신 가이드 +## 참고 자료 +``` diff --git a/docs/contributing/guide-template.md b/docs/contributing/guide-template.md new file mode 100644 index 00000000..f952bf91 --- /dev/null +++ b/docs/contributing/guide-template.md @@ -0,0 +1,45 @@ +--- +title: 일반 가이드 템플릿 +description: 계속 검증하고 갱신하는 기술 가이드 형식입니다. +--- + +# 일반 가이드 템플릿 + +## Front matter + +```yaml +--- +title: 가이드 제목 +description: 목표와 적용 범위를 요약한 한 문장 +document_type: guide +services: [azure-kubernetes-service] +technologies: [kubernetes] +tags: [diagnostics] +status: current +verification_status: verified +sources_checked_at: 2026-09-12 +official_sources: + - title: 관련 Microsoft Learn 원문 제목 + url: https://learn.microsoft.com/azure/aks/ +last_verified: 2026-09-12 +review_cycle_days: 180 +applies_to: [AKS 1.34+] +related_cases: [] +--- +``` + +## 본문 순서 + +```markdown +# 가이드 제목 +## 목표 +## 적용 범위와 지원 버전 +## 사전 조건 +## 권장 아키텍처와 선택 근거 +## 구성과 운영 절차 +## 검증 방법 +## 롤백과 트러블슈팅 +## 제약 사항과 버전별 차이 +## 관련 사례 +## 공식 참고 자료 +``` diff --git a/docs/contributing/index.md b/docs/contributing/index.md new file mode 100644 index 00000000..a96d3ed0 --- /dev/null +++ b/docs/contributing/index.md @@ -0,0 +1,126 @@ +--- +title: 기여하기 +description: 재현할 수 있는 환경과 코드, 판단의 근거를 갖춘 기술 기록을 기여하는 방법 +--- + +

CONTRIBUTING

+ +# 코드와 근거를 함께 남겨주세요 + +어떤 환경에서 무엇을 관측했고, 왜 그 방법을 선택했는지까지 적어주세요. 다음 사람이 같은 조건을 재현하고, 자신의 환경에도 적용할 수 있는지 판단하는 데 도움이 됩니다. + +
+ +1. **글의 성격을 고릅니다.** 특정 시점의 문제 해결, 재사용할 절차, 직접 해보는 실습, 선택지를 비교한 분석 중 어디에 해당하는지 정합니다. +2. **환경과 근거를 남깁니다.** 코드·관측값·공식 출처를 정리하고, 확인하지 못한 부분은 구분합니다. +3. **공개 전에 검증합니다.** 링크와 빌드 결과를 확인하고 실제 환경의 식별자나 비밀이 남아 있지 않은지 살펴봅니다. + +
+ +이 페이지가 공개 문서 작성 규칙의 기준입니다. 루트 `CONTRIBUTING.md`와 `AGENTS.md`는 핵심 규칙과 실행할 검증 명령을 안내합니다. + +## 문서 유형 선택 + +| 질문 | 모음 | 수명주기 | +|---|---|---| +| 특정 시점에 무엇이 발생했고 어떻게 해결했나? | 트러블슈팅 (`cases`) | 당시 사실을 보존하고 필요하면 `historical`로 표시 | +| 지금 같은 문제를 어떻게 진단·구성해야 하나? | 구현 가이드 (`guides`) | 제품 변화에 맞춰 같은 문서를 갱신 | +| 독자가 리소스를 배포해 재현할 수 있나? | 실습 (`labs`) | 비용, 검증, 정리 절차까지 재검증 | +| 선택지·성능·아키텍처를 어떤 기준으로 비교했나? | 비교·분석 (`research`) | 조사 기준일과 한계를 보존 | + +실제 장애 이력과 재사용 가능한 절차가 섞여 있으면 두 문서로 분리합니다. 사례에는 당시 환경·관측·결과를, 가이드에는 현재 지원 범위·권장 절차·검증·롤백을 두고 `related_cases`와 `related_guides`로 연결합니다. + +## 폴더만으로 자동 게시하기 + +```text +docs//// +├── index.md +└── images/ +``` + +URL에 쓰이는 폴더 이름은 소문자 kebab-case 영문으로 작성합니다. Azure 제품의 ``는 `azure-`와 공식 전체 제품명을 조합합니다(예: `azure-kubernetes-service`, `azure-database-for-mysql`). Microsoft 제품은 공식 명칭을 보존하므로 이름이 `Microsoft`로 시작하면 `microsoft-` slug를 사용합니다(예: `microsoft-foundry`). 오픈소스와 그 밖의 제품은 임의의 공급자 접두사를 붙이지 않고 공식 프로젝트·제품명을 사용합니다. + +문서를 위 경로에 넣고 유효한 front matter를 작성하면 다음 빌드에서 자동으로 Pages에 포함됩니다. + +- Awesome Nav가 폴더에서 메뉴를 발견합니다. +- 생성기가 문서 유형별·서비스별 색인에 추가합니다. +- Material tags가 태그를 페이지와 태그 색인에 표시하고 검색 미리보기에 포함합니다. +- Material search가 한국어와 영어 제목·본문·섹션을 전체 텍스트로 색인합니다. + +메뉴는 **문서 유형 → 서비스 → 문서** 순서의 트리로 만들어집니다. 문서 이름은 front matter의 `title`을 사용하므로, 폴더 이름을 바꾸지 않고도 읽기 좋은 제목을 표시할 수 있습니다. 각 그룹은 접고 펼칠 수 있고, 현재 읽는 문서의 경로만 기본으로 열립니다. 메뉴 데이터는 빌드 결과에 포함되므로 브라우저가 Markdown을 다시 읽어 구성하지 않습니다. + +개별 문서 추가를 위해 `mkdocs.yml`, `docs/.nav.yml`, README 또는 수동 목록을 수정하지 않습니다. 새 서비스·기술·태그 식별자가 필요할 때만 `docs-taxonomy.yml`을 같은 PR에서 한 번 갱신합니다. + +| 추가하거나 바꾸려는 것 | 작성자가 수정할 곳 | +|---|---| +| 기존 서비스·태그를 사용하는 새 글 | 해당 bundle의 `index.md` | +| 글에서 보여줄 이미지 | 해당 bundle의 `images/` | +| 처음 사용하는 서비스·기술·태그 | 새 글과 `docs-taxonomy.yml` | +| 메뉴·목록·검색에 새 글 노출 | 별도 수정 없음 — 빌드가 처리 | + +홈에 소개할 글은 해당 문서의 front matter에 `featured: true`를 지정합니다. 홈의 대표 글은 이 메타데이터에서 최대 세 편을 고르며, 별도 문서 목록은 관리하지 않습니다. 지정한 글이 없으면 문서 유형별로 글을 골라 보여줍니다. + +## 공통 front matter + +```yaml +--- +title: AKS 네트워크 진단 +description: AKS 네트워크 문제를 진단하는 현재 절차 +document_type: guide +services: [azure-kubernetes-service] +technologies: [kubernetes] +tags: [networking, troubleshooting] +status: current +verification_status: verified +sources_checked_at: 2026-09-12 +official_sources: + - title: Azure Kubernetes Service documentation + url: https://learn.microsoft.com/azure/aks/ +last_verified: 2026-09-12 +review_cycle_days: 180 +applies_to: [AKS 1.34+] +--- +``` + +`services`, `technologies`, `tags`와 유형별 `status`는 `docs-taxonomy.yml`의 값을 사용합니다. 제목은 한국어여도 제품명, API, 명령과 오류 메시지는 공식 영문 표기를 함께 써 검색 가능하게 합니다. + +유형별 전체 형식은 [사례 템플릿](case-template.md), [가이드 템플릿](guide-template.md), [실습 템플릿](lab-template.md), [리서치 템플릿](research-template.md)을 사용합니다. + +## Microsoft Learn MCP로 기술 주장 검증하기 + +워크스페이스의 `.vscode/mcp.json`은 공식 Microsoft Learn MCP 서버를 연결합니다. Microsoft 또는 Azure의 제품 동작, 지원 상태, 제한, 버전, 구성 단계나 CLI/API 사용법을 작성할 때 repository skill `verify-with-microsoft-learn`을 사용합니다. + +1. 변경한 문장에서 검증 가능한 주장을 나눕니다. +2. MCP가 현재 제공하는 도구 설명을 조회하고 Microsoft Learn을 검색합니다. +3. 검색 요약으로 끝내지 않고 관련 원문 전체를 가져옵니다. +4. 적용 범위, 전제 조건, 버전, 지원 상태와 제한을 본문의 주장과 비교합니다. +5. 실제 사용한 원문의 제목과 정규 URL을 `official_sources`에 기록하고 `sources_checked_at`을 확인일로 갱신합니다. +6. 모든 중요한 주장이 맞을 때만 `verification_status: verified`로 표시합니다. + +Kubernetes나 CNCF 프로젝트 구현처럼 Microsoft Learn이 최종 권위가 아닌 세부 사항은 `kubernetes.io`, `cncf.io` 또는 해당 프로젝트 공식 원문도 추가합니다. 검증을 끝내지 못한 문서는 `verification_status: needs-review`로 표시합니다. 자동 CI는 필드와 도메인을 검사하지만 본문의 의미를 대신 판단하지 않습니다. + +## 본문과 파일 규칙 + +- 페이지마다 하나의 H1을 사용하고 H2/H3 순서를 건너뛰지 않습니다. +- 코드 블록에는 언어를 지정하고 비밀·실제 식별자를 넣지 않습니다. +- 문서 간 링크는 `.md` 소스 상대 경로를, 페이지 이미지는 `images/` 상대 경로를 사용합니다. +- 모든 이미지에 내용을 설명하는 대체 텍스트를 작성합니다. +- 실행 코드와 배포 파일은 `samples///`에 두고 GitHub 파일 링크로 연결합니다. +- 원시 로그, 캡처 타임라인과 생성 결과는 문서가 아니라 sample/evidence로 보관합니다. + +## 공개 안전성 + +고객·조직·사용자 실명, 구독·테넌트 ID, 키·토큰·인증서·연결 문자열, 내부 URL·IP·호스트명, 개인정보가 포함된 로그와 승인되지 않은 고객 화면을 게시하지 않습니다. `validate_public_safety.py`가 환경 고유 식별자의 대표 패턴을 검사하지만, 자동 검사와 별개로 사람이 원문과 이미지를 확인해야 합니다. + +## 로컬 검증 + +```powershell +python scripts/docs/validate_metadata.py +python scripts/docs/validate_sources.py +python scripts/docs/validate_links.py +python scripts/docs/validate_public_safety.py +mkdocs build --strict +python scripts/docs/validate_search_index.py +``` + +`main`에 병합되면 GitHub Actions가 같은 검사를 다시 실행하고 생성된 `site/`을 Pages artifact로 배포합니다. 생성 HTML과 검색 인덱스는 Git에 커밋하지 않습니다. diff --git a/docs/contributing/lab-template.md b/docs/contributing/lab-template.md new file mode 100644 index 00000000..f1bd5304 --- /dev/null +++ b/docs/contributing/lab-template.md @@ -0,0 +1,46 @@ +--- +title: 실습 템플릿 +description: 재현 가능한 배포·검증·정리 실습 형식입니다. +--- + +# 실습 템플릿 + +## Front matter + +```yaml +--- +title: 실습 제목 +description: 배포하고 확인할 동작을 요약한 한 문장 +document_type: lab +services: [azure-monitor] +technologies: [bicep] +tags: [deployment, monitoring] +status: verified +verification_status: verified +sources_checked_at: 2026-09-12 +official_sources: + - title: 관련 Microsoft Learn 원문 제목 + url: https://learn.microsoft.com/azure/azure-monitor/ +last_verified: 2026-09-12 +review_cycle_days: 90 +estimated_time: 45m +cost: paid +cleanup_required: true +--- +``` + +## 본문 순서 + +```markdown +# 실습 제목 +## 목표 +## 사전 조건 +## 비용과 안전 경계 +## 배포 +## 시나리오 실행 +## 예상 결과 +## 검증 +## 정리 +## 문제 해결 +## 공식 참고 자료 +``` diff --git a/docs/contributing/research-template.md b/docs/contributing/research-template.md new file mode 100644 index 00000000..ecd9285f --- /dev/null +++ b/docs/contributing/research-template.md @@ -0,0 +1,39 @@ +--- +title: 리서치 템플릿 +description: 비교·벤치마크·아키텍처 조사 형식입니다. +--- + +# 리서치 템플릿 + +## Front matter + +```yaml +--- +title: 리서치 제목 +description: 질문, 비교 대상과 결론을 요약한 한 문장 +document_type: research +services: [azure-ai-search] +technologies: [rag] +tags: [benchmarking] +status: current +verification_status: verified +sources_checked_at: 2026-09-12 +official_sources: + - title: 관련 Microsoft Learn 원문 제목 + url: https://learn.microsoft.com/azure/search/ +published_at: 2026-09-12 +--- +``` + +## 본문 순서 + +```markdown +# 리서치 제목 +## 질문 +## 조사 범위와 기준일 +## 조사 방법 +## 결과 +## 권고 +## 한계 +## 공식 출처 +``` diff --git a/develop/nodejs/fileio_issue_cpu_overhead.md b/docs/guides/application-development/nodejs-file-io-cpu/index.md similarity index 82% rename from develop/nodejs/fileio_issue_cpu_overhead.md rename to docs/guides/application-development/nodejs-file-io-cpu/index.md index 113f4af3..60327b2c 100644 --- a/develop/nodejs/fileio_issue_cpu_overhead.md +++ b/docs/guides/application-development/nodejs-file-io-cpu/index.md @@ -1,3 +1,27 @@ +--- +services: +- application-development +official_sources: +- title: JavaScript on Azure documentation + url: https://learn.microsoft.com/azure/developer/javascript/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Node.js 파일 I/O CPU 오버헤드 분석 +description: Node.js 서버의 파일 I/O로 인한 CPU 오버헤드를 분석하고 개선합니다. +technologies: +- nodejs +tags: +- performance +- diagnostics +- development +--- + # Node.js 서버 CPU 오버헤드 분석 및 개선 방안 ## **문제 상황** diff --git a/aisearch/custom_vectorization/images/indexer_vs_pushapi.png b/docs/guides/azure-ai-search/custom-embedding-ingestion/images/indexer_vs_pushapi.png similarity index 100% rename from aisearch/custom_vectorization/images/indexer_vs_pushapi.png rename to docs/guides/azure-ai-search/custom-embedding-ingestion/images/indexer_vs_pushapi.png diff --git a/aisearch/custom_vectorization/01_custom_embedding_guide.md b/docs/guides/azure-ai-search/custom-embedding-ingestion/index.md similarity index 95% rename from aisearch/custom_vectorization/01_custom_embedding_guide.md rename to docs/guides/azure-ai-search/custom-embedding-ingestion/index.md index 64bc97e2..a5969582 100644 --- a/aisearch/custom_vectorization/01_custom_embedding_guide.md +++ b/docs/guides/azure-ai-search/custom-embedding-ingestion/index.md @@ -1,8 +1,33 @@ +--- +services: +- azure-ai-search +official_sources: +- title: Azure AI Search documentation + url: https://learn.microsoft.com/azure/search/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Custom 임베딩 적재 설계 +description: Azure AI Search의 SKU, 적재 방식과 GPU 임베딩 서빙 엔진 선택 기준을 설명합니다. +technologies: +- python +- rag +tags: +- architecture +- performance +- deployment +--- + # Custom 임베딩 적재 가이드: SKU 선택부터 서빙 엔진까지 **Azure AI Search에 벡터를 적재하는 두 가지 경로(Indexer Pull vs Push API)와, GPU 임베딩 서빙 엔진(TEI vs vLLM) 선택 기준** -> 관련 문서: [기본 가이드 (CPU, Indexer)](02_custom_vectorization.md) | [GPU vLLM RAG 가이드](03_gpu_vllm_rag_guide.md) | [청킹 전략 리서치](ref_chunking_strategies_research.md) +> 관련 문서: [기본 가이드 (CPU, Indexer)](../custom-web-api-vectorization/index.md) | [GPU vLLM RAG 가이드](../gpu-vllm-rag/index.md) | [청킹 전략 리서치](../../../research/azure-ai-search/rag-chunking-strategies/index.md) > > 작성일: 2026-05-22 @@ -133,7 +158,7 @@ AI Search의 [Push API](https://learn.microsoft.com/en-us/azure/search/search-wh #### 아키텍처 비교 -![Indexer vs Push API](indexer_vs_pushapi.png) +![Indexer vs Push API](images/indexer_vs_pushapi.png) #### Push API 성능이 높은 이유 @@ -155,7 +180,7 @@ Indexer: Push Pipeline: 4. **네트워크 오버헤드 감소**: 동일 클러스터 내부 통신 시 네트워크 hop이 줄어 지연 감소 (TLS 적용 여부는 보안/서비스 메시 구성에 따라 달라진다) 5. **실행 시간 제한 없음**: 인덱서의 실행 시간 제한(공용 2h / 전용 24h)이 없어 대량 적재 가능 -> Push API 파이프라인 코드 예시는 [Appendix A](#appendix-a-push-api-파이프라인-코드-예시) 참고. +> Push API 파이프라인 코드 예시는 [Appendix A](#appendix-a-push-api) 참고. #### 쿼리 시점: Custom Vectorizer (변경 없음) @@ -268,7 +293,7 @@ TEI / vLLM (추론 서버) (어댑터 필요: 쿼리 시점) ``` -> 어댑터 구현 예시: [tei-adapter](tei-adapter/) 디렉토리 참고 +> 어댑터 구현 예시: [tei-adapter](https://github.com/hellices/devguidesample/tree/main/samples/azure-ai-search/source-material/custom_vectorization/tei-adapter) 디렉토리 참고 --- diff --git a/aisearch/custom_vectorization/images/architecture.png b/docs/guides/azure-ai-search/custom-web-api-vectorization/images/architecture.png similarity index 100% rename from aisearch/custom_vectorization/images/architecture.png rename to docs/guides/azure-ai-search/custom-web-api-vectorization/images/architecture.png diff --git a/aisearch/custom_vectorization/02_custom_vectorization.md b/docs/guides/azure-ai-search/custom-web-api-vectorization/index.md similarity index 91% rename from aisearch/custom_vectorization/02_custom_vectorization.md rename to docs/guides/azure-ai-search/custom-web-api-vectorization/index.md index 34b48a9e..8a9d486e 100644 --- a/aisearch/custom_vectorization/02_custom_vectorization.md +++ b/docs/guides/azure-ai-search/custom-web-api-vectorization/index.md @@ -1,3 +1,28 @@ +--- +services: +- azure-ai-search +official_sources: +- title: Azure AI Search documentation + url: https://learn.microsoft.com/azure/search/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Custom Web API 기반 외부 임베딩 통합 +description: Azure AI Search Custom Web API skill로 외부 임베딩 모델을 연결하는 방법을 설명합니다. +technologies: +- python +- rest-api +- rag +tags: +- deployment +- architecture +--- + # Azure AI Search Custom Web API를 활용한 외부 임베딩 모델 통합 벡터화 가이드 **Azure OpenAI 없이, 자체 호스팅한 오픈소스 임베딩 모델을 Custom Web API Skill/Vectorizer로 AI Search Integrated Vectorization에 연동하는 방법** @@ -12,7 +37,7 @@ | 검증 모델 | [BAAI/bge-m3](https://huggingface.co/BAAI/bge-m3), [Qwen/Qwen3-Embedding-0.6B](https://huggingface.co/Qwen/Qwen3-Embedding-0.6B) | | 호스팅 | FastAPI 서버를 Container Apps (TLS 자동) 또는 AKS + cert-manager (Let's Encrypt)로 HTTPS 노출 | | 핵심 검증 | Custom Web API Skill(인덱싱) + Custom Web API Vectorizer(쿼리) 동작 확인 | -| 모델 비교 | [BGE-M3 vs Qwen3-Embedding-0.6B](ref_bge_m3_vs_qwen3_comparison.md) | +| 모델 비교 | [BGE-M3 vs Qwen3-Embedding-0.6B](../../../research/azure-ai-search/bge-m3-vs-qwen3-embedding/index.md) | | 인덱서 결과 | 5건 처리 / 0건 실패 | --- @@ -54,12 +79,12 @@ Azure Blob Storage → Indexer → Custom Web API Skill → BG | 구성 요소 | 리소스 | 세부 사항 | |-----------|--------|-----------| -| AI Search | `ais-aiplay-krc-01` (Standard) | 인덱스, 스킬셋, 인덱서, 벡터라이저 | +| AI Search | `search-example-koreacentral-01` (Standard) | 인덱스, 스킬셋, 인덱서, 벡터라이저 | | 임베딩 API | FastAPI (`embedding-api/app.py`) | Custom Web API Skill 계약 준수 | -| HTTPS 엔드포인트 (A) | Container Apps `ca-bge-m3-embed` | TLS 자동 적용, 설정 최소 | +| HTTPS 엔드포인트 (A) | Container Apps `ca-embedding-example-01` | TLS 자동 적용, 설정 최소 | | HTTPS 엔드포인트 (B) | AKS Ingress `embed.{IP}.nip.io` | ingress-nginx + cert-manager + Let's Encrypt | -| 컨테이너 레지스트리 | `acrcustomvec01` (Basic) | Docker 이미지 저장소 | -| 스토리지 | `sacustomvecsrc01` | Blob 컨테이너 `sample-docs` (샘플 문서) | +| 컨테이너 레지스트리 | `acrexamplekrc01` (Basic) | Docker 이미지 저장소 | +| 스토리지 | `stexamplekrc01` | Blob 컨테이너 `sample-docs` (샘플 문서) | | Region | koreacentral | 모든 리소스 동일 리전 | --- @@ -71,7 +96,7 @@ Azure Blob Storage → Indexer → Custom Web API Skill → BG ACR Tasks를 사용하여 로컬 Docker 없이 클라우드에서 빌드한다. BGE-M3 모델 가중치(~2.3GB)를 빌드 타임에 다운로드하여 Cold start를 방지한다. ```bash -az acr build --registry acrcustomvec01 \ +az acr build --registry acrexamplekrc01 \ --image bge-m3-embedding:latest \ --file embedding-api/Dockerfile \ embedding-api/ @@ -92,24 +117,24 @@ Container Apps는 이미지를 배포하면 HTTPS가 자동 적용된다. ```bash # Container Apps 환경 생성 -az containerapp env create --name cae-customvec \ - --resource-group rg-aiplay-krc-01 --location koreacentral +az containerapp env create --name cae-example-koreacentral-01 \ + --resource-group rg-example-koreacentral-01 --location koreacentral # 임베딩 API 배포 (외부 HTTPS ingress) -az containerapp create --name ca-bge-m3-embed \ - --resource-group rg-aiplay-krc-01 --environment cae-customvec \ - --image acrcustomvec01.azurecr.io/bge-m3-embedding:latest \ - --registry-server acrcustomvec01.azurecr.io \ +az containerapp create --name ca-embedding-example-01 \ + --resource-group rg-example-koreacentral-01 --environment cae-example-koreacentral-01 \ + --image acrexamplekrc01.azurecr.io/bge-m3-embedding:latest \ + --registry-server acrexamplekrc01.azurecr.io \ --target-port 8000 --ingress external \ --cpu 2 --memory 4Gi --min-replicas 1 --max-replicas 1 ``` ```bash # FQDN 확인 -az containerapp show --name ca-bge-m3-embed \ - --resource-group rg-aiplay-krc-01 \ +az containerapp show --name ca-embedding-example-01 \ + --resource-group rg-example-koreacentral-01 \ --query properties.configuration.ingress.fqdn -o tsv -# ca-bge-m3-embed.icycliff-31a3d588.koreacentral.azurecontainerapps.io +# ca-embedding-example-01.example.koreacentral.azurecontainerapps.io ``` #### Option B. AKS + ingress-nginx + cert-manager (Let's Encrypt) @@ -149,7 +174,7 @@ Ingress LoadBalancer IP를 확인하여 nip.io 도메인을 구성한다. nip.io INGRESS_IP=$(kubectl get svc -n ingress-nginx ingress-nginx-controller \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}') echo "Domain: embed.${INGRESS_IP}.nip.io" -# embed.20.249.162.81.nip.io +# Domain: embed..nip.io ``` `k8s/ingress.yaml`의 host를 위 도메인으로 설정한 뒤 적용한다. @@ -173,10 +198,10 @@ kubectl get certificate ```bash # Option A (Container Apps) -curl -s https://ca-bge-m3-embed.icycliff-31a3d588.koreacentral.azurecontainerapps.io/health +curl -s https://ca-embedding-example-01.example.koreacentral.azurecontainerapps.io/health # Option B (AKS + Let's Encrypt) -curl -s https://embed.20.249.162.81.nip.io/health +curl -s "https://embed.${INGRESS_IP}.nip.io/health" # {"status":"ok","model":"BAAI/bge-m3"} ``` @@ -207,11 +232,11 @@ KEY="" # AI Search MI에 Storage Blob Data Reader 역할 부여 az role assignment create \ --role "Storage Blob Data Reader" \ - --assignee-object-id $(az search service show --name ais-aiplay-krc-01 \ - --resource-group rg-aiplay-krc-01 --query identity.principalId -o tsv) \ + --assignee-object-id $(az search service show --name search-example-koreacentral-01 \ + --resource-group rg-example-koreacentral-01 --query identity.principalId -o tsv) \ --assignee-principal-type ServicePrincipal \ - --scope $(az storage account show --name sacustomvecsrc01 \ - --resource-group rg-aiplay-krc-01 --query id -o tsv) + --scope $(az storage account show --name stexamplekrc01 \ + --resource-group rg-example-koreacentral-01 --query id -o tsv) ``` ```bash @@ -222,7 +247,7 @@ curl -X POST "$SEARCH_URL/datasources?api-version=2024-07-01" \ "name": "sample-docs-ds", "type": "azureblob", "credentials": { - "connectionString": "ResourceId=/subscriptions/{sub}/resourceGroups/rg-aiplay-krc-01/providers/Microsoft.Storage/storageAccounts/sacustomvecsrc01/;" + "connectionString": "ResourceId=/subscriptions/{sub}/resourceGroups/rg-example-koreacentral-01/providers/Microsoft.Storage/storageAccounts/stexamplekrc01/;" }, "container": {"name": "sample-docs"} }' @@ -239,9 +264,10 @@ curl -X POST "$SEARCH_URL/datasources?api-version=2024-07-01" \ ```bash # Option A: Container Apps FQDN -# EMBED_URL="https://ca-bge-m3-embed.icycliff-31a3d588.koreacentral.azurecontainerapps.io" +# EMBED_URL="https://ca-embedding-example-01.example.koreacentral.azurecontainerapps.io" # Option B: AKS Ingress -EMBED_URL="https://embed.20.249.162.81.nip.io" +: "${INGRESS_IP:?Set INGRESS_IP to the Ingress LoadBalancer IP}" +EMBED_URL="https://embed.${INGRESS_IP}.nip.io" curl -X PUT "$SEARCH_URL/indexes/sample-vector-idx?api-version=2024-07-01" \ -H "Content-Type: application/json" -H "api-key: $KEY" \ @@ -492,7 +518,7 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] 다른 모델로 빌드하려면 `--build-arg`를 사용한다: ```bash -az acr build --registry acrcustomvec01 \ +az acr build --registry acrexamplekrc01 \ --image qwen3-embedding:latest \ --build-arg EMBEDDING_MODEL=Qwen/Qwen3-Embedding-0.6B \ --file embedding-api/Dockerfile embedding-api/ @@ -794,7 +820,7 @@ curl -X POST "$SEARCH_URL/indexes/sample-chunk-idx/docs/search?api-version=2024- ### sentence-transformers → TEI 전환 시 차이 -TEI가 sentence-transformers보다 유리한 구조적 이유(dynamic batching, Rust 네이티브 동시성, GPU 최적화)는 [Custom 임베딩 적재 가이드 — 서빙 엔진 비교](01_custom_embedding_guide.md#3-gpu-임베딩-서빙-엔진-tei-vs-vllm) 참고. 아래는 이 가이드(CPU 기본 구성) 기준의 실용적 차이: +TEI가 sentence-transformers보다 유리한 구조적 이유(dynamic batching, Rust 네이티브 동시성, GPU 최적화)는 [Custom 임베딩 적재 가이드 — 서빙 엔진 비교](../custom-embedding-ingestion/index.md#3-gpu-tei-vs-vllm) 참고. 아래는 이 가이드(CPU 기본 구성) 기준의 실용적 차이: | | sentence-transformers (현재) | TEI | |---|---|---| @@ -819,7 +845,7 @@ Adapter와 TEI를 **같은 Pod의 sidecar**로 구성하면 네트워크 홉 없 어댑터는 모델을 포함하지 않으므로 빌드가 빠르다 (~30초). ```bash -az acr build --registry acrcustomvec01 \ +az acr build --registry acrexamplekrc01 \ --image tei-adapter:latest \ --file tei-adapter/Dockerfile \ tei-adapter/ @@ -897,10 +923,10 @@ Ingress의 backend 서비스를 `bge-m3-embedding`으로 되돌리면 즉시 sen --- ## 📋 관련 문서 -- [GPU vLLM RAG 가이드](03_gpu_vllm_rag_guide.md) — T4 GPU에서 vLLM + Push API 대량 적재 + 하이브리드 검색 -- [BGE-M3 vs Qwen3-Embedding-0.6B 비교](ref_bge_m3_vs_qwen3_comparison.md) — 동일 파이프라인에서의 검색 품질 비교 결과 -- [Custom 임베딩 적재 가이드](01_custom_embedding_guide.md) — SKU 선택부터 서빙 엔진까지 의사결정 가이드 -- [청킹 전략 리서치](ref_chunking_strategies_research.md) — 7가지 최신 RAG 청킹 전략 비교 분석 +- [GPU vLLM RAG 가이드](../gpu-vllm-rag/index.md) — T4 GPU에서 vLLM + Push API 대량 적재 + 하이브리드 검색 +- [BGE-M3 vs Qwen3-Embedding-0.6B 비교](../../../research/azure-ai-search/bge-m3-vs-qwen3-embedding/index.md) — 동일 파이프라인에서의 검색 품질 비교 결과 +- [Custom 임베딩 적재 가이드](../custom-embedding-ingestion/index.md) — SKU 선택부터 서빙 엔진까지 의사결정 가이드 +- [청킹 전략 리서치](../../../research/azure-ai-search/rag-chunking-strategies/index.md) — 7가지 최신 RAG 청킹 전략 비교 분석 ## 📋 참고 diff --git a/aisearch/custom_vectorization/images/gpu_vllm_rag_architecture.png b/docs/guides/azure-ai-search/gpu-vllm-rag/images/gpu_vllm_rag_architecture.png similarity index 100% rename from aisearch/custom_vectorization/images/gpu_vllm_rag_architecture.png rename to docs/guides/azure-ai-search/gpu-vllm-rag/images/gpu_vllm_rag_architecture.png diff --git a/aisearch/custom_vectorization/03_gpu_vllm_rag_guide.md b/docs/guides/azure-ai-search/gpu-vllm-rag/index.md similarity index 90% rename from aisearch/custom_vectorization/03_gpu_vllm_rag_guide.md rename to docs/guides/azure-ai-search/gpu-vllm-rag/index.md index 390ff3cc..53336852 100644 --- a/aisearch/custom_vectorization/03_gpu_vllm_rag_guide.md +++ b/docs/guides/azure-ai-search/gpu-vllm-rag/index.md @@ -1,8 +1,34 @@ +--- +services: +- azure-ai-search +official_sources: +- title: Azure AI Search documentation + url: https://learn.microsoft.com/azure/search/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: GPU vLLM 기반 RAG 적재와 검색 +description: GPU vLLM 임베딩, Push API 대량 적재와 실시간 하이브리드 검색 구성을 설명합니다. +technologies: +- python +- kubernetes +- rag +tags: +- performance +- deployment +- architecture +--- + # GPU vLLM RAG 가이드: Push API 대량 적재 + 실시간 하이브리드 검색 **T4 GPU에서 vLLM 임베딩 + PIC 청킹을 통합 서빙하고, Push API로 대량 적재 + Custom Vectorizer로 실시간 하이브리드 검색을 수행하는 구성 가이드** -> 관련 문서: [기본 가이드 (CPU, Indexer)](02_custom_vectorization.md) | [Custom 임베딩 적재 가이드](01_custom_embedding_guide.md) | [청킹 전략 리서치](ref_chunking_strategies_research.md) +> 관련 문서: [기본 가이드 (CPU, Indexer)](../custom-web-api-vectorization/index.md) | [Custom 임베딩 적재 가이드](../custom-embedding-ingestion/index.md) | [청킹 전략 리서치](../../../research/azure-ai-search/rag-chunking-strategies/index.md) > > 작성일: 2026-05-22 @@ -21,7 +47,7 @@ | 청킹 전략 | PIC (Pseudo-Instruction Chunking) | | 전환 방식 | Ingress backend 교체 — AI Search URI 변경 불필요 | -> 모델·엔진·청킹 전략의 선택 근거는 [Appendix A](#appendix-a-모델엔진청킹-선택-근거), 벤치마크 결과는 [Appendix B](#appendix-b-성능-벤치마크) 참고. +> 모델·엔진·청킹 전략의 선택 근거는 [Appendix A](#appendix-a), 벤치마크 결과는 [Appendix B](#appendix-b) 참고. --- @@ -83,7 +109,7 @@ Qwen3-Embedding-4B의 출력 차원은 **2560**이다 (BGE-M3는 1024). Custom V 인덱스 생성 curl ```bash -SEARCH_URL="https://ais-aiplay-krc-01.search.windows.net" +SEARCH_URL="https://search-example-koreacentral-01.search.windows.net" KEY="" curl -X PUT "$SEARCH_URL/indexes/prod-chunk-idx?api-version=2024-07-01" \ @@ -125,7 +151,7 @@ curl -X PUT "$SEARCH_URL/indexes/prod-chunk-idx?api-version=2024-07-01" \ ## 2. Push API 파이프라인 -인덱서 대신 Push API로 적재한다. 인덱서의 구조적 한계(deg≤10, 순차 배치, 공용 2h/전용 24h 제한)를 우회하여 GPU 활용률을 극대화한다. ([상세 비교](01_custom_embedding_guide.md)) +인덱서 대신 Push API로 적재한다. 인덱서의 구조적 한계(deg≤10, 순차 배치, 공용 2h/전용 24h 제한)를 우회하여 GPU 활용률을 극대화한다. ([상세 비교](../custom-embedding-ingestion/index.md)) 파이프라인 흐름: **데이터 소스 → PIC 청킹(vLLM-chunk) → 임베딩(vLLM-embed) → Push API(AI Search)** @@ -134,13 +160,13 @@ curl -X PUT "$SEARCH_URL/indexes/prod-chunk-idx?api-version=2024-07-01" \ - 적재: Push API 배치 제한 (요청당 최대 1000건 또는 16MB) - Fallback: PIC 실패 시 원본 텍스트를 단일 청크로 적재 -> Indexer vs Push API의 개념적 비교와 하이브리드 운영 패턴은 [Custom 임베딩 적재 가이드 — 적재 방식](01_custom_embedding_guide.md#2-적재-방식-indexer-vs-push-api) 참고. 실측치(T4 Spot 3대, vLLM 3 replica): 500 docs 적재 147.9s (embed 37.71 c/s). 상세 결과는 [Appendix B](#appendix-b-성능-벤치마크) 참고. +> Indexer vs Push API의 개념적 비교와 하이브리드 운영 패턴은 [Custom 임베딩 적재 가이드 — 적재 방식](../custom-embedding-ingestion/index.md#2-indexer-vs-push-api) 참고. 실측치(T4 Spot 3대, vLLM 3 replica): 500 docs 적재 147.9s (embed 37.71 c/s). 상세 결과는 [Appendix B](#appendix-b) 참고. --- ## 3. AKS GPU 노드풀 -실제 구성: `aks-customvec-krc` 클러스터, `rg-aiplay-krc-01` 리소스 그룹. +실제 구성: `aks-example-koreacentral-01` 클러스터, `rg-example-koreacentral-01` 리소스 그룹.
노드풀 생성 CLI @@ -148,8 +174,8 @@ curl -X PUT "$SEARCH_URL/indexes/prod-chunk-idx?api-version=2024-07-01" \ ```bash # GPU Spot 노드풀 (T4 × 1) az aks nodepool add \ - --resource-group rg-aiplay-krc-01 \ - --cluster-name aks-customvec-krc \ + --resource-group rg-example-koreacentral-01 \ + --cluster-name aks-example-koreacentral-01 \ --name gpuspot \ --node-count 1 \ --node-vm-size Standard_NC16as_T4_v3 \ @@ -329,7 +355,7 @@ spec: # Adapter — 쿼리 시점 Custom Vectorizer용 (AI Search 계약 변환) - name: adapter - image: acrcustomvec01.azurecr.io/vllm-adapter:latest + image: acrexamplekrc01.azurecr.io/vllm-adapter:latest env: - name: VLLM_URL value: "http://localhost:8081" @@ -560,9 +586,9 @@ curl localhost:8081/metrics | grep -E "vllm:(num_requests|gpu_cache)" - [vLLM Embedding Models](https://docs.vllm.ai/en/latest/models/pooling_models.html) - [Qwen3-Embedding 모델 라인업](https://huggingface.co/collections/Qwen/qwen3-embedding-67e5253e22bb421b4fa0ed10) - [PIC — Pseudo-Instruction Chunking](https://aclanthology.org/2025.findings-acl.422/) (ACL-Findings 2025) -- [기본 가이드 (CPU, Indexer)](02_custom_vectorization.md) -- [Custom 임베딩 적재 가이드](01_custom_embedding_guide.md) -- [청킹 전략 리서치](ref_chunking_strategies_research.md) +- [기본 가이드 (CPU, Indexer)](../custom-web-api-vectorization/index.md) +- [Custom 임베딩 적재 가이드](../custom-embedding-ingestion/index.md) +- [청킹 전략 리서치](../../../research/azure-ai-search/rag-chunking-strategies/index.md) --- @@ -576,13 +602,13 @@ curl localhost:8081/metrics | grep -E "vllm:(num_requests|gpu_cache)" | vLLM + BGE-M3 | 0.57B 모델에서 PyTorch overhead가 크다 (23.85 c/s) | | TEI + BGE-M3 | **51.11 c/s로 가장 빠르지만**, MTEB 성능이 Qwen3-4B 대비 낮아 품질·속도 트레이드오프 | | vLLM + Qwen3-Embedding-8B | 8B(16GB)이면 T4 VRAM 전체를 소비, 청킹 LLM 공간 없음 | -| sentence-transformers | 서버 레벨 dynamic batching 미지원, Python 단일 프로세스 동시성 제한 ([상세](01_custom_embedding_guide.md)) | +| sentence-transformers | 서버 레벨 dynamic batching 미지원, Python 단일 프로세스 동시성 제한 ([상세](../custom-embedding-ingestion/index.md)) | -vLLM은 PyTorch 기반이라 **Qwen3 아키텍처를 HuggingFace 가중치에서 직접 로딩** 가능하다. TEI(Rust/candle)는 모델별 커널을 별도 구현해야 하므로 신규 아키텍처 지원이 늦다. 단, BERT 계열(BGE-M3)에서는 TEI가 2.1배 빠르다 ([Appendix B](#appendix-b-성능-벤치마크) 참고). +vLLM은 PyTorch 기반이라 **Qwen3 아키텍처를 HuggingFace 가중치에서 직접 로딩** 가능하다. TEI(Rust/candle)는 모델별 커널을 별도 구현해야 하므로 신규 아키텍처 지원이 늦다. 단, BERT 계열(BGE-M3)에서는 TEI가 2.1배 빠르다 ([Appendix B](#appendix-b) 참고). ### 청킹: PIC (Pseudo-Instruction Chunking) -[청킹 전략 리서치](ref_chunking_strategies_research.md)에서 분석한 7가지 전략 중 PIC를 선택한 이유: +[청킹 전략 리서치](../../../research/azure-ai-search/rag-chunking-strategies/index.md)에서 분석한 7가지 전략 중 PIC를 선택한 이유: | 전략 | LLM 호출 | GPU 적합도 | 검색 품질 | 인덱싱 속도 | 선택 | |------|---------|-----------|----------|-----------|------| @@ -602,7 +628,7 @@ vLLM은 PyTorch 기반이라 **Qwen3 아키텍처를 HuggingFace 가중치에서 ## Appendix B. 성능 벤치마크 -> **테스트 환경**: AKS (`aks-customvec-krc`, koreacentral), GPU 노드풀 Standard_NC16as_T4_v3 Spot × 3, AI Search Standard tier. 500개 Azure Docs .md 파일 (평균 ~8KB). Push API는 bench_runner의 section-based chunking, Indexer는 AI Search SplitSkill(pages, 2000자)을 사용하여 **chunk 수가 다를 수 있다**. +> **테스트 환경**: AKS (`aks-example-koreacentral-01`, koreacentral), GPU 노드풀 Standard_NC16as_T4_v3 Spot × 3, AI Search Standard tier. 500개 Azure Docs .md 파일 (평균 ~8KB). Push API는 bench_runner의 section-based chunking, Indexer는 AI Search SplitSkill(pages, 2000자)을 사용하여 **chunk 수가 다를 수 있다**. > > **벤치마크 조건**: vLLM `--gpu-memory-utilization=0.95 --max-num-seqs=256 --max-model-len=2048`, GPU당 단독 실행. TEI `turing-latest` (Candle backend). diff --git a/aisearch-v2/images/portal_ko_microsoft.png b/docs/guides/azure-ai-search/korean-analyzer-comparison/images/portal_ko_microsoft.png similarity index 100% rename from aisearch-v2/images/portal_ko_microsoft.png rename to docs/guides/azure-ai-search/korean-analyzer-comparison/images/portal_ko_microsoft.png diff --git a/aisearch-v2/images/portal_standard_lucene.png b/docs/guides/azure-ai-search/korean-analyzer-comparison/images/portal_standard_lucene.png similarity index 100% rename from aisearch-v2/images/portal_standard_lucene.png rename to docs/guides/azure-ai-search/korean-analyzer-comparison/images/portal_standard_lucene.png diff --git a/aisearch-v2/README.md b/docs/guides/azure-ai-search/korean-analyzer-comparison/index.md similarity index 91% rename from aisearch-v2/README.md rename to docs/guides/azure-ai-search/korean-analyzer-comparison/index.md index 270033ca..3178adbb 100644 --- a/aisearch-v2/README.md +++ b/docs/guides/azure-ai-search/korean-analyzer-comparison/index.md @@ -1,8 +1,31 @@ +--- +services: +- azure-ai-search +official_sources: +- title: Azure AI Search documentation + url: https://learn.microsoft.com/azure/search/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Azure AI Search 한국어 분석기 비교 +description: Microsoft 한국어 분석기와 표준 Lucene 분석기의 동작을 비교합니다. +technologies: +- python +tags: +- diagnostics +- performance +--- + # Azure AI Search 한국어 분석기(Analyzer) 비교 가이드 **필드에 지정하는 분석기(Analyzer)에 따라 한국어 복합명사/조사 검색 성능(recall)이 어떻게 달라지는지 실제 리소스로 재현하고, `ko.microsoft`가 가장 잘 검색되는 것을 정답/오답 데이터로 검증한 가이드** -> **고객 증상 예시**: "특정 한국어 단어(예: `온누리상품권`)가 AI Search에서 검색되지 않는다", "인덱스 필드의 analyzer 설정에 따라 같은 검색어가 되기도 하고 안 되기도 한다". → 원인·해결·검증 절차는 [🛠️ 고객 적용 가이드](#️-고객-적용-가이드-진단--조치--검증) 참고. +> **고객 증상 예시**: "특정 한국어 단어(예: `온누리상품권`)가 AI Search에서 검색되지 않는다", "인덱스 필드의 analyzer 설정에 따라 같은 검색어가 되기도 하고 안 되기도 한다". → 원인·해결·검증 절차는 [🛠️ 고객 적용 가이드](#_9) 참고. --- @@ -14,7 +37,7 @@ | 결론 | **`ko.microsoft`만** 모든 쿼리 변형에서 기대정답을 완벽 회수(recall 5/5) | | 근본 원인 | 기본값 `standard.lucene`은 복합명사 미분해 + 조사(은/는/도/과) 미제거 | | 해결 | 한국어 필드에 `ko.microsoft` 분석기 지정 후 **재색인** | -| 검증 리소스 | RG `aisearchtest` / AI Search `onnuri-search-59018` (Free, koreacentral) | +| 검증 리소스 | RG `rg-search-example-koreacentral` / AI Search `search-example-koreacentral-02` (Free, koreacentral) | | 검증 방법 | 정답 6건 + distractor 4건 데이터로 분석기별 recall/precision 비교 | > `온누리상품권`은 분석기 동작을 보여주기 위한 **샘플 검색 키워드**일 뿐이며, 이 가이드는 AI Search 분석기 기능 자체에 초점을 둔다. @@ -47,8 +70,8 @@ | 구성 요소 | 리소스 | 세부 사항 | |-----------|--------|-----------| -| Resource Group | `aisearchtest` | koreacentral | -| AI Search | `onnuri-search-59018` | **Free** tier (비용 0) | +| Resource Group | `rg-search-example-koreacentral` | koreacentral | +| AI Search | `search-example-koreacentral-02` | **Free** tier (비용 0) | | Index | `analyzer-compare-idx` | 동일 텍스트를 4개 분석기 필드로 색인 (모두 retrievable) | | 샘플 데이터 | `sample_data.json` | 정답 6건 + distractor 4건 = 10건 | | 테스트 스크립트 | `analyzer_test.py` | 인덱스 생성 → 업로드 → Analyze 비교 → 검색/ recall 비교 | @@ -143,7 +166,7 @@ distractor(7~10)는 **어떤 분석기에서도 오탐되지 않았다**(precisi ## 🖥️ 포탈에서 확인하기 -Azure Portal → `onnuri-search-59018` → **Indexes → `analyzer-compare-idx` → Search explorer** → 우측 상단 **View → `JSON`** 으로 전환하면 JSON query editor가 열린다. +Azure Portal → `search-example-koreacentral-02` → **Indexes → `analyzer-compare-idx` → Search explorer** → 우측 상단 **View → `JSON`** 으로 전환하면 JSON query editor가 열린다. Search explorer는 기본적으로 모든 searchable 필드를 검색하므로, **`searchFields`로 분석기 필드를 지정**해 비교한다. 결과 본문(`content`)까지 보려면 `select`에 해당 필드를 넣는데, **`select`에는 retrievable(=검색결과 반환 허용) 필드만** 넣을 수 있다. @@ -350,16 +373,17 @@ curl -s -X POST "https://{service}.search.windows.net/indexes/{index}-v2/docs/se ① 동의어(Synonym Map)로 표기 변형 흡수, ② 벡터/하이브리드 검색으로 의미 기반 보완, ③ 필요한 도메인 용어는 사용자 사전이 필요한 경우 커스텀 분석기 설계를 검토합니다. **Q. 동의어 맵을 쓰면 분석기는 안 바꿔도 되나요?** -아니요. 동의어는 분석기를 **대체하지 않고 보완**합니다([재현 3](#-재현-3--동의어synonym-map로-정확도-더-높이기) 참고). 조사를 못 떼는 `standard.lucene` 위에서는 동의어를 붙여도 회수에 실패할 수 있고, `ko.microsoft` 위에서 가장 잘 동작합니다. 또한 동의어 맵은 **쿼리 시점 적용**이라 필드에 연결하면 재색인 없이 반영되지만, 별칭이 형태소로 분해되면(예: `컬처랜드`→`컬처`/`랜드`) 발동하지 않을 수 있어 **분해되지 않는 단일 명사 형태**로 등록하는 것이 안전합니다. +아니요. 동의어는 분석기를 **대체하지 않고 보완**합니다([재현 3](#3-synonym-map) 참고). 조사를 못 떼는 `standard.lucene` 위에서는 동의어를 붙여도 회수에 실패할 수 있고, `ko.microsoft` 위에서 가장 잘 동작합니다. 또한 동의어 맵은 **쿼리 시점 적용**이라 필드에 연결하면 재색인 없이 반영되지만, 별칭이 형태소로 분해되면(예: `컬처랜드`→`컬처`/`랜드`) 발동하지 않을 수 있어 **분해되지 않는 단일 명사 형태**로 등록하는 것이 안전합니다. --- ## ▶️ 재현 실행 방법 ```powershell -# 리소스는 이미 생성됨 (RG aisearchtest / onnuri-search-59018) +# 아래 리소스 이름은 예시입니다. 사용할 리소스 그룹과 Azure AI Search 서비스를 +# 먼저 준비한 뒤 실제 이름으로 바꾸십시오. # .env.example 을 .env 로 복사하고 SEARCH_ADMIN_KEY 채우기 -# az search admin-key show --service-name onnuri-search-59018 -g aisearchtest --query primaryKey -o tsv +# az search admin-key show --service-name search-example-koreacentral-02 -g rg-search-example-koreacentral --query primaryKey -o tsv python -m venv .venv .\.venv\Scripts\python.exe -m pip install -r requirements.txt @@ -372,7 +396,7 @@ python -m venv .venv ## 🧹 리소스 정리 ```powershell -az group delete -n aisearchtest --yes --no-wait +az group delete -n rg-search-example-koreacentral --yes --no-wait ``` --- diff --git a/appgw/sse_response_buffer_disable.md b/docs/guides/azure-application-gateway/sse-response-buffering/index.md similarity index 88% rename from appgw/sse_response_buffer_disable.md rename to docs/guides/azure-application-gateway/sse-response-buffering/index.md index 5c1f3bf1..70d8ffb9 100644 --- a/appgw/sse_response_buffer_disable.md +++ b/docs/guides/azure-application-gateway/sse-response-buffering/index.md @@ -1,3 +1,26 @@ +--- +services: +- azure-application-gateway +official_sources: +- title: Azure Application Gateway documentation + url: https://learn.microsoft.com/azure/application-gateway/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Application Gateway SSE 응답 버퍼링 비활성화 +description: Server-Sent Events 통신을 위해 Application Gateway 응답 버퍼링을 조정합니다. +technologies: +- rest-api +tags: +- networking +- performance +--- + # Application Gateway SSE 통신 시 Response Buffer 비활성화 가이드 ## 문제 상황 diff --git a/appgw/waf_custom_rule_ip_whitelist.md b/docs/guides/azure-application-gateway/waf-path-ip-allowlist/index.md similarity index 95% rename from appgw/waf_custom_rule_ip_whitelist.md rename to docs/guides/azure-application-gateway/waf-path-ip-allowlist/index.md index 35e965dd..63d6d14d 100644 --- a/appgw/waf_custom_rule_ip_whitelist.md +++ b/docs/guides/azure-application-gateway/waf-path-ip-allowlist/index.md @@ -1,3 +1,26 @@ +--- +services: +- azure-application-gateway +official_sources: +- title: Azure Application Gateway documentation + url: https://learn.microsoft.com/azure/application-gateway/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Application Gateway WAF 경로 기반 IP 허용 +description: Application Gateway WAF custom rule로 특정 경로의 IP 허용 목록을 구성합니다. +technologies: +- rest-api +tags: +- security +- networking +--- + # Application Gateway WAF: 경로 기반 IP 화이트리스트 설정 가이드 > Azure Application Gateway WAF v2에서 커스텀 룰을 사용하여 특정 경로에 대해 허용된 IP만 접근 가능하도록 설정하는 방법을 정리한 문서. AGW 레벨(전체 적용)과 리스너별 적용 두 가지 방식을 모두 다룬다. diff --git a/architect/request_in_second.md b/docs/guides/azure-architecture/response-time-optimization/index.md similarity index 73% rename from architect/request_in_second.md rename to docs/guides/azure-architecture/response-time-optimization/index.md index c45df76a..1221d37b 100644 --- a/architect/request_in_second.md +++ b/docs/guides/azure-architecture/response-time-optimization/index.md @@ -1,3 +1,27 @@ +--- +services: +- azure-architecture +official_sources: +- title: Azure Architecture Center + url: https://learn.microsoft.com/azure/architecture/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Azure 응답 시간 최적화 설계 고려사항 +description: 초 단위 응답 시간을 줄이기 위한 Azure 아키텍처 고려사항을 정리합니다. +technologies: +- rest-api +tags: +- architecture +- latency +- performance +--- + # 응답속도 최적화를 위한 azure architect 고려사항 > 모든 azure의 제품을 고려할 때 네트워크의 홉이 증가한다는 사실을 고려해야 함 diff --git a/automation/portal-cli-limitations.md b/docs/guides/azure-automation/portal-cli-limitations/index.md similarity index 90% rename from automation/portal-cli-limitations.md rename to docs/guides/azure-automation/portal-cli-limitations/index.md index d996736a..07cc0db3 100644 --- a/automation/portal-cli-limitations.md +++ b/docs/guides/azure-automation/portal-cli-limitations/index.md @@ -1,7 +1,31 @@ +--- +services: +- azure-automation +official_sources: +- title: Azure Automation documentation + url: https://learn.microsoft.com/azure/automation/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Azure Automation 포털과 CLI 제한사항 +description: Azure Automation의 포털 및 CLI 제약과 우회 방법을 정리합니다. +technologies: +- azure-cli +- powershell +tags: +- troubleshooting +- development +--- + # Azure Automation - 포탈/CLI 제한사항 및 우회 방법 > 작성일: 2026-03-08 -> 대상: Automation Account `autorecovery` (RG: `rg-hellices-krc-01`) +> 대상: Automation Account `` (RG: `rg-example-koreacentral-01`) --- diff --git a/cosmosdb/nodejs/client.md b/docs/guides/azure-cosmos-db/nodejs-client-optimization/index.md similarity index 93% rename from cosmosdb/nodejs/client.md rename to docs/guides/azure-cosmos-db/nodejs-client-optimization/index.md index e41ea323..f7aa0e11 100644 --- a/cosmosdb/nodejs/client.md +++ b/docs/guides/azure-cosmos-db/nodejs-client-optimization/index.md @@ -1,3 +1,27 @@ +--- +services: +- azure-cosmos-db +official_sources: +- title: Azure Cosmos DB documentation + url: https://learn.microsoft.com/azure/cosmos-db/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Azure Cosmos DB Node.js Client 최적화 +description: Node.js 애플리케이션에서 Cosmos DB Client 연결과 사용을 최적화합니다. +technologies: +- nodejs +- cosmos-db +tags: +- performance +- development +--- + # CosmosDB Client 최적화 방안 ## 개요 diff --git a/cosmosdb/nodejs/query_thread_with_low_core.md b/docs/guides/azure-cosmos-db/nodejs-dns-lookup-bottleneck/index.md similarity index 82% rename from cosmosdb/nodejs/query_thread_with_low_core.md rename to docs/guides/azure-cosmos-db/nodejs-dns-lookup-bottleneck/index.md index d7885e94..f864ad5f 100644 --- a/cosmosdb/nodejs/query_thread_with_low_core.md +++ b/docs/guides/azure-cosmos-db/nodejs-dns-lookup-bottleneck/index.md @@ -1,3 +1,28 @@ +--- +services: +- azure-cosmos-db +official_sources: +- title: Azure Cosmos DB documentation + url: https://learn.microsoft.com/azure/cosmos-db/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Cosmos DB 연속 쿼리 DNS Lookup 병목 진단 +description: 낮은 CPU 환경의 연속 쿼리에서 발생하는 DNS lookup 병목을 진단하고 완화합니다. +technologies: +- nodejs +- cosmos-db +tags: +- networking +- performance +- troubleshooting +--- + # **Cosmos DB 연속 쿼리 시 DNS Lookup 병목 최적화 가이드** ## ✅ **문제 상황** @@ -35,7 +60,7 @@ const CacheableLookup = require('cacheable-lookup'); const cacheable = new CacheableLookup(); cacheable.install(http); -http.get('http://db-neu-prd-cosmos-northeurope.documents.azure.com', (res) => { +http.get('http://cosmos-example-northeurope.documents.azure.com', (res) => { console.log('Status:', res.statusCode); }); ``` diff --git a/cosmosdb/nodejs/point_read.md b/docs/guides/azure-cosmos-db/point-read-optimization/index.md similarity index 95% rename from cosmosdb/nodejs/point_read.md rename to docs/guides/azure-cosmos-db/point-read-optimization/index.md index fb63019f..bec7471f 100644 --- a/cosmosdb/nodejs/point_read.md +++ b/docs/guides/azure-cosmos-db/point-read-optimization/index.md @@ -1,3 +1,27 @@ +--- +services: +- azure-cosmos-db +official_sources: +- title: Azure Cosmos DB documentation + url: https://learn.microsoft.com/azure/cosmos-db/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Cosmos DB Point Read 최적화 +description: Point Read를 활용해 Azure Cosmos DB 요청량과 지연 시간을 줄입니다. +technologies: +- nodejs +- cosmos-db +tags: +- performance +- latency +--- + # CosmosDB Point Read를 활용한 트래픽 및 Latency 최적화 전략 ## 문제 정의 diff --git a/mysql/nodejs/db_upgrade_zero_downtime.md b/docs/guides/azure-database-for-mysql/blue-green-upgrade/index.md similarity index 97% rename from mysql/nodejs/db_upgrade_zero_downtime.md rename to docs/guides/azure-database-for-mysql/blue-green-upgrade/index.md index d2b88ff8..94b0420d 100644 --- a/mysql/nodejs/db_upgrade_zero_downtime.md +++ b/docs/guides/azure-database-for-mysql/blue-green-upgrade/index.md @@ -1,3 +1,29 @@ +--- +services: +- azure-database-for-mysql +official_sources: +- title: Azure Database for MySQL documentation + url: https://learn.microsoft.com/azure/mysql/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Azure MySQL Blue/Green 무중단 업그레이드 +description: AKS와 Node.js mysql2 PoolCluster를 이용한 Azure MySQL Blue/Green 전환을 설명합니다. +technologies: +- nodejs +- mysql +- kubernetes +tags: +- migration +- high-availability +- reliability +--- + # Azure MySQL 무중단 업그레이드: Blue/Green 전략 구현 (AKS + Node.js mysql2 PoolCluster) ## 개요 diff --git a/mysql/nodejs/read_write_routing.md b/docs/guides/azure-database-for-mysql/nodejs-read-write-routing/index.md similarity index 88% rename from mysql/nodejs/read_write_routing.md rename to docs/guides/azure-database-for-mysql/nodejs-read-write-routing/index.md index e4261de1..217cde12 100644 --- a/mysql/nodejs/read_write_routing.md +++ b/docs/guides/azure-database-for-mysql/nodejs-read-write-routing/index.md @@ -1,3 +1,28 @@ +--- +services: +- azure-database-for-mysql +official_sources: +- title: Azure Database for MySQL documentation + url: https://learn.microsoft.com/azure/mysql/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Azure MySQL Node.js 읽기/쓰기 라우팅 +description: MySQL Flexible Server와 Node.js mysql2의 읽기/쓰기 연결 라우팅을 설명합니다. +technologies: +- nodejs +- mysql +tags: +- high-availability +- performance +- development +--- + 아래는 **Node.js에서 mysql2를 사용해 Azure Database for MySQL Flexible Server를 Primary(쓰기) + Replica(읽기) 구성 시 적용할 수 있는 한글 가이드**입니다. Microsoft Learn 공식 문서 링크도 포함했습니다. *** diff --git a/aks/argocd_image_updater.md b/docs/guides/azure-kubernetes-service/argocd-image-updater-acr/index.md similarity index 96% rename from aks/argocd_image_updater.md rename to docs/guides/azure-kubernetes-service/argocd-image-updater-acr/index.md index 1bdaa838..4560d148 100644 --- a/aks/argocd_image_updater.md +++ b/docs/guides/azure-kubernetes-service/argocd-image-updater-acr/index.md @@ -1,3 +1,27 @@ +--- +services: +- azure-kubernetes-service +official_sources: +- title: Azure Kubernetes Service documentation + url: https://learn.microsoft.com/azure/aks/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Argo CD Image Updater와 ACR 연동 +description: AKS에서 Argo CD Image Updater가 Azure Container Registry 이미지를 갱신하도록 구성합니다. +technologies: +- kubernetes +- gitops +tags: +- deployment +- authentication +--- + # ✅ ArgoCD Image Updater + ACR on AKS 설정 가이드 **AKS에서 ArgoCD Image Updater v2를 사용하여 Azure Container Registry(ACR)의 이미지 태그를 자동으로 감지하고 업데이트하는 전체 과정을 정리한 문서.** @@ -124,8 +148,8 @@ az identity create \ --location # 출력에서 clientId, principalId 기록 -# clientId: 예) 4baa8b8b-e2db-4c10-9043-168edc8466a0 -# principalId: 예) 8eb24516-2c4d-4217-9239-353ec59ae24a +# clientId: 예) +# principalId: 예) # 2. ACR에 AcrPull 역할 부여 ACR_ID=$(az acr show --name --query id -o tsv) @@ -515,8 +539,8 @@ level=debug msg="Image already on latest allowed version" **해결**: `imageName`에서 태그 제거. ```yaml -imageName: acrrubiconkrc01.azurecr.io/test-app:1.0.0 # ❌ -imageName: acrrubiconkrc01.azurecr.io/test-app # ✅ +imageName: acrexamplekrc01.azurecr.io/test-app:1.0.0 # ❌ +imageName: acrexamplekrc01.azurecr.io/test-app # ✅ ``` ### 오류 5: AZURE 환경변수 미주입 diff --git a/aks/authorization-troubleshooting.md b/docs/guides/azure-kubernetes-service/authorization-troubleshooting/index.md similarity index 94% rename from aks/authorization-troubleshooting.md rename to docs/guides/azure-kubernetes-service/authorization-troubleshooting/index.md index 0fc01328..5a7f340b 100644 --- a/aks/authorization-troubleshooting.md +++ b/docs/guides/azure-kubernetes-service/authorization-troubleshooting/index.md @@ -1,3 +1,27 @@ +--- +services: +- azure-kubernetes-service +official_sources: +- title: Azure Kubernetes Service documentation + url: https://learn.microsoft.com/azure/aks/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: AKS 배포 Authorization 오류 진단 +description: AKS 배포 과정의 Azure 권한 오류를 분류하고 진단하는 절차를 설명합니다. +technologies: +- kubernetes +- azure-cli +tags: +- authorization +- troubleshooting +--- + # ✅ AKS 배포 시 Authorization 오류 트러블슈팅 가이드 **Pod 배포 시 LinkedAuthorizationFailed 오류 디버깅 및 해결 방법** diff --git a/aks/cni-overlay-nsg.md b/docs/guides/azure-kubernetes-service/cni-overlay-nsg/index.md similarity index 93% rename from aks/cni-overlay-nsg.md rename to docs/guides/azure-kubernetes-service/cni-overlay-nsg/index.md index f2545303..50f75c21 100644 --- a/aks/cni-overlay-nsg.md +++ b/docs/guides/azure-kubernetes-service/cni-overlay-nsg/index.md @@ -1,3 +1,26 @@ +--- +services: +- azure-kubernetes-service +official_sources: +- title: Azure Kubernetes Service documentation + url: https://learn.microsoft.com/azure/aks/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: AKS CNI Overlay와 NSG 구성 +description: AKS CNI Overlay를 사용할 때 NSG 규칙과 트래픽 경로의 주의점을 설명합니다. +technologies: +- kubernetes +tags: +- networking +- security +--- + # ✅ AKS CNI Overlay 사용 시 NSG 설정 주의사항 **CNI Overlay에서 Pod IP 대역을 NSG에 허용해야 하는 이유** diff --git a/aks/kaito_opensource_model.md b/docs/guides/azure-kubernetes-service/kaito-open-source-model/index.md similarity index 93% rename from aks/kaito_opensource_model.md rename to docs/guides/azure-kubernetes-service/kaito-open-source-model/index.md index a7f07ab3..70e83614 100644 --- a/aks/kaito_opensource_model.md +++ b/docs/guides/azure-kubernetes-service/kaito-open-source-model/index.md @@ -1,3 +1,26 @@ +--- +services: +- azure-kubernetes-service +official_sources: +- title: Azure Kubernetes Service documentation + url: https://learn.microsoft.com/azure/aks/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: AKS와 KAITO로 오픈소스 LLM 서빙 +description: AKS에서 KAITO를 이용해 오픈소스 언어 모델을 배포하고 서빙합니다. +technologies: +- kubernetes +tags: +- ai-agents +- deployment +--- + # AKS + KAITO로 오픈소스 LLM 서빙하기 KAITO(Kubernetes AI Toolchain Operator)를 사용해 AKS 위에 오픈소스 LLM을 배포하는 전체 과정을 정리한다. diff --git a/aks/affinity.md b/docs/guides/azure-kubernetes-service/pod-affinity-distribution/index.md similarity index 82% rename from aks/affinity.md rename to docs/guides/azure-kubernetes-service/pod-affinity-distribution/index.md index 62aa927c..4e37e14a 100644 --- a/aks/affinity.md +++ b/docs/guides/azure-kubernetes-service/pod-affinity-distribution/index.md @@ -1,3 +1,26 @@ +--- +services: +- azure-kubernetes-service +official_sources: +- title: Azure Kubernetes Service documentation + url: https://learn.microsoft.com/azure/aks/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Pod Affinity를 이용한 AKS 워크로드 분산 +description: AKS에서 Pod affinity와 anti-affinity로 워크로드를 분산하는 예제를 설명합니다. +technologies: +- kubernetes +tags: +- kubernetes +- reliability +--- + # ✅ AKS에서 Pod 분산 예제 **PodAntiAffinity + TopologySpreadConstraints + HPA 예시** diff --git a/aks/agent-pool.md b/docs/guides/azure-kubernetes-service/pod-scheduling-agent-pools/index.md similarity index 82% rename from aks/agent-pool.md rename to docs/guides/azure-kubernetes-service/pod-scheduling-agent-pools/index.md index 8f5120f3..9efd8e39 100644 --- a/aks/agent-pool.md +++ b/docs/guides/azure-kubernetes-service/pod-scheduling-agent-pools/index.md @@ -1,3 +1,26 @@ +--- +services: +- azure-kubernetes-service +official_sources: +- title: Azure Kubernetes Service documentation + url: https://learn.microsoft.com/azure/aks/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: AKS Agent Pool 기반 Pod 스케줄링 +description: AKS agent pool, label, taint와 affinity를 이용한 스케줄링 패턴을 설명합니다. +technologies: +- kubernetes +tags: +- kubernetes +- deployment +--- + # ✅ AKS에서 Pod 스케줄링 제어 Best Practice **taint + toleration, affinity, nodeSelector 비교 및 조합** diff --git a/aks/pyroscope-chat-profiling/images/anf-create-bucket.png b/docs/guides/azure-kubernetes-service/pyroscope-anf-s3/images/anf-create-bucket.png similarity index 100% rename from aks/pyroscope-chat-profiling/images/anf-create-bucket.png rename to docs/guides/azure-kubernetes-service/pyroscope-anf-s3/images/anf-create-bucket.png diff --git a/aks/pyroscope-chat-profiling/images/anf-create-certificate.png b/docs/guides/azure-kubernetes-service/pyroscope-anf-s3/images/anf-create-certificate.png similarity index 100% rename from aks/pyroscope-chat-profiling/images/anf-create-certificate.png rename to docs/guides/azure-kubernetes-service/pyroscope-anf-s3/images/anf-create-certificate.png diff --git a/aks/pyroscope-chat-profiling/images/architecture.png b/docs/guides/azure-kubernetes-service/pyroscope-anf-s3/images/architecture.png similarity index 100% rename from aks/pyroscope-chat-profiling/images/architecture.png rename to docs/guides/azure-kubernetes-service/pyroscope-anf-s3/images/architecture.png diff --git a/aks/pyroscope-chat-profiling/images/pyroscope_ui.png b/docs/guides/azure-kubernetes-service/pyroscope-anf-s3/images/pyroscope_ui.png similarity index 100% rename from aks/pyroscope-chat-profiling/images/pyroscope_ui.png rename to docs/guides/azure-kubernetes-service/pyroscope-anf-s3/images/pyroscope_ui.png diff --git a/aks/pyroscope_anf_s3_deployment.md b/docs/guides/azure-kubernetes-service/pyroscope-anf-s3/index.md similarity index 90% rename from aks/pyroscope_anf_s3_deployment.md rename to docs/guides/azure-kubernetes-service/pyroscope-anf-s3/index.md index c42f6025..9c943a7d 100644 --- a/aks/pyroscope_anf_s3_deployment.md +++ b/docs/guides/azure-kubernetes-service/pyroscope-anf-s3/index.md @@ -1,3 +1,28 @@ +--- +services: +- azure-kubernetes-service +official_sources: +- title: Azure Kubernetes Service documentation + url: https://learn.microsoft.com/azure/aks/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: AKS Pyroscope와 Azure NetApp Files S3 배포 +description: Pyroscope을 AKS에 배포하고 Azure NetApp Files S3를 백엔드로 구성합니다. +technologies: +- kubernetes +- python +tags: +- observability +- deployment +- storage +--- + # Pyroscope을 AKS에 배포하기 (S3 백엔드를 Azure NetApp Files로) > **Pyroscope란?** Grafana의 오픈소스 지속프로파일링(continuous profiling) 플랫폼입니다. @@ -10,16 +35,16 @@ > S3 호환 API**로 구현하는 것입니다. (프로파일링 대상 예시로 Python pod를 사용) ## 목차 -0. [목표 & 시나리오](#목표--시나리오) -1. [아키텍처 개요](#아키텍처-개요) -2. [필수 사전 조건](#필수-사전-조건) -3. [1단계: ANF 구성 (S3 백엔드)](#1단계-anf-구성-s3-백엔드) -4. [2단계: ANF S3 Bucket 설정](#2단계-anf-s3-bucket-설정) -5. [3단계: Pyroscope 배포](#3단계-pyroscope-배포) -6. [4단계: 검증](#4단계-검증) -7. [5단계: 애플리케이션 프로파일링 (예제)](#5단계-애플리케이션-프로파일링-예제) -8. [프로파일 수집 방식 & 언어별 지원](#프로파일-수집-방식--언어별-지원) -9. [설정값 정리](#설정값-정리) +0. [목표 & 시나리오](#_2) +1. [아키텍처 개요](#_3) +2. [필수 사전 조건](#_4) +3. [1단계: ANF 구성 (S3 백엔드)](#1-anf-s3) +4. [2단계: ANF S3 Bucket 설정](#2-anf-s3-bucket) +5. [3단계: Pyroscope 배포](#3-pyroscope) +6. [4단계: 검증](#4) +7. [5단계: 애플리케이션 프로파일링 (예제)](#5) +8. [프로파일 수집 방식 & 언어별 지원](#_5) +9. [설정값 정리](#_7) --- @@ -40,24 +65,24 @@ Python Pod (앱 + SDK) ──push──▶ Pyroscope Server ──S3 API─ ``` > **왜 ANF인가**: MinIO 운영 불필요, ANF 용량으로 자동 스케일, 엔터프라이즈 SLA·스냅샷·3-way replica. -> **이미 떠있는 앱 프로파일링**은 이미지 재빌드 없이 SDK 초기화 몇 줄만 추가 → [5단계](#5단계-애플리케이션-프로파일링-예제). +> **이미 떠있는 앱 프로파일링**은 이미지 재빌드 없이 SDK 초기화 몇 줄만 추가 → [5단계](#5). --- ## 아키텍처 개요 -![Pyroscope 프로파일링 아키텍처 (Python Pod → Pyroscope → ANF)](./pyroscope-chat-profiling/images/architecture.png) +![Pyroscope 프로파일링 아키텍처 (Python Pod → Pyroscope → ANF)](images/architecture.png) **핵심 포인트:** - MinIO 제거: `enabled: false` - S3 저장소: Azure NetApp Files S3-compatible API - 네트워킹: Private HTTPS (self-signed cert) - 자격증명: AWS SigV4 (AccessKey + SecretKey) -- 애플리케이션 프로파일: SDK가 Pyroscope로 직접 push (Python 예제는 [5단계](#5단계-애플리케이션-프로파일링-예제) 참고) +- 애플리케이션 프로파일: SDK가 Pyroscope로 직접 push (Python 예제는 [5단계](#5) 참고) Pyroscope UI 플레임그래프 예시: -![Pyroscope UI - ai-foundry-chat flame graph](./pyroscope-chat-profiling/images/pyroscope_ui.png) +![Pyroscope UI - ai-foundry-chat flame graph](images/pyroscope_ui.png) --- @@ -169,18 +194,18 @@ ANF는 버킷을 NFS/SMB 볼륨 위에 얹고 S3 접근에 self-signed 인증서 인증서 생성 옵션: -![Create certificate options (Microsoft Learn)](./pyroscope-chat-profiling/images/anf-create-certificate.png) +![Create certificate options (Microsoft Learn)](images/anf-create-certificate.png) 버킷 생성 메뉴 (Name / Path / UID·GID / Permissions / Certificate): -![Create a bucket menu (Microsoft Learn)](./pyroscope-chat-profiling/images/anf-create-bucket.png) +![Create a bucket menu (Microsoft Learn)](images/anf-create-bucket.png) > 위 스크린샷은 Microsoft Learn 문서 > [Configure object REST API in Azure NetApp Files](https://learn.microsoft.com/en-us/azure/azure-netapp-files/object-rest-api-access-configure) > (최종 갱신 2026-05-20)에서 인용했습니다. 최신 UI는 원문을 참고하세요. > 포털 가이드: [Configure object REST API in Azure NetApp Files](https://learn.microsoft.com/en-us/azure/azure-netapp-files/object-rest-api-access-configure) -> 여기서 얻은 `endpoint`(`https://`), `accessKey`, `secretKey`를 [3단계](#3단계-pyroscope-배포)에 사용합니다. +> 여기서 얻은 `endpoint`(`https://`), `accessKey`, `secretKey`를 [3단계](#3-pyroscope)에 사용합니다. ### 2.B ARM REST API로 생성 (자동화용 대안) @@ -378,8 +403,8 @@ kubectl port-forward -n observability svc/pyroscope 4040:4040 아래는 동작을 바로 확인할 수 있는 **데모**입니다. Azure AI Foundry를 호출하는 FastAPI 앱에 Pyroscope Python SDK를 붙여 콜스택을 수집합니다. -> 예제 코드: [pyroscope-chat-profiling/](pyroscope-chat-profiling/) -> ([app.py](pyroscope-chat-profiling/app.py) · [deployment.yaml](pyroscope-chat-profiling/deployment.yaml) · [deploy.sh](pyroscope-chat-profiling/deploy.sh) · [test.py](pyroscope-chat-profiling/test.py)) +> 예제 코드: [pyroscope-chat-profiling/](https://github.com/hellices/devguidesample/tree/main/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling) +> ([app.py](https://github.com/hellices/devguidesample/blob/main/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/app.py) · [deployment.yaml](https://github.com/hellices/devguidesample/blob/main/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/deployment.yaml) · [deploy.sh](https://github.com/hellices/devguidesample/blob/main/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/deploy.sh) · [test.py](https://github.com/hellices/devguidesample/blob/main/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/test.py))
5.1 핵심 동작 (펼치기) @@ -430,7 +455,7 @@ kubectl port-forward -n observability svc/pyroscope 4040:4040 # → http://localhost:4040 (service: ai-foundry-chat) ``` -![Pyroscope UI - ai-foundry-chat flame graph](./pyroscope-chat-profiling/images/pyroscope_ui.png) +![Pyroscope UI - ai-foundry-chat flame graph](images/pyroscope_ui.png) **플레임그래프 읽는 법 (분석):** - 가로 폭 = 해당 함수가 점유한 CPU 시간 비율. **넓은 박스 = bottleneck 후보**. diff --git a/aks/memray-leak-profiling/reports/leak-leaks.png b/docs/guides/azure-kubernetes-service/python-memory-leak-memray/images/leak-leaks.png similarity index 100% rename from aks/memray-leak-profiling/reports/leak-leaks.png rename to docs/guides/azure-kubernetes-service/python-memory-leak-memray/images/leak-leaks.png diff --git a/aks/memray_leak_profiling.md b/docs/guides/azure-kubernetes-service/python-memory-leak-memray/index.md similarity index 88% rename from aks/memray_leak_profiling.md rename to docs/guides/azure-kubernetes-service/python-memory-leak-memray/index.md index 8c1cd137..103ed0a5 100644 --- a/aks/memray_leak_profiling.md +++ b/docs/guides/azure-kubernetes-service/python-memory-leak-memray/index.md @@ -1,3 +1,28 @@ +--- +services: +- azure-kubernetes-service +official_sources: +- title: Azure Kubernetes Service documentation + url: https://learn.microsoft.com/azure/aks/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: AKS Python Pod 메모리 누수 분석 +description: kubectl debug와 Memray attach를 이용해 Python Pod 메모리 누수를 분석합니다. +technologies: +- kubernetes +- python +tags: +- diagnostics +- performance +- troubleshooting +--- + # AKS Python Pod 메모리 누수 — `kubectl debug` + Memray attach 앱 이미지를 건드리지 않고, 재시작 없이, **memray가 설치된 디버그 이미지를 @@ -21,7 +46,7 @@ ## 사전 준비: 디버그 이미지 -gdb + procps + memray를 담은 이미지를 레지스트리에 상비 ([debug/Dockerfile](./memray-leak-profiling/debug/Dockerfile)): +gdb + procps + memray를 담은 이미지를 레지스트리에 상비 ([debug/Dockerfile](https://github.com/hellices/devguidesample/blob/main/samples/azure-kubernetes-service/source-material/memray-leak-profiling/debug/Dockerfile)): ```dockerfile # ⚠️ 베이스는 앱과 동일 Python 버전 — memray attach 는 "타깃 프로세스 안"에서 @@ -143,9 +168,9 @@ kubectl cp memray-demo/$POD:/tmp/leak-leaks.html reports/leak-leaks.html `--leaks` 플레임그래프 — 프레임 hover 시 파일/라인/크기/횟수 표시 (`_remember_turn_context`, app.py:50 — 5.4MB / 2,544 allocations): -![memray --leaks 플레임그래프: chat → _remember_turn_context (app.py:50) 와 _archive_tool_trace (app.py:65) 가 미해제 메모리의 대부분을 차지](./memray-leak-profiling/reports/leak-leaks.png) +![memray --leaks 플레임그래프: chat → _remember_turn_context (app.py:50) 와 _archive_tool_trace (app.py:65) 가 미해제 메모리의 대부분을 차지](images/leak-leaks.png) -- 원본 HTML(인터랙티브): [`reports/leak-leaks.html`](./memray-leak-profiling/reports/leak-leaks.html) · [`reports/leak-flamegraph.html`](./memray-leak-profiling/reports/leak-flamegraph.html) +- 원본 HTML 파일(내려받아 브라우저에서 열기): [`reports/leak-leaks.html`](https://github.com/hellices/devguidesample/blob/main/samples/azure-kubernetes-service/source-material/memray-leak-profiling/reports/leak-leaks.html) · [`reports/leak-flamegraph.html`](https://github.com/hellices/devguidesample/blob/main/samples/azure-kubernetes-service/source-material/memray-leak-profiling/reports/leak-flamegraph.html) - 절대 수치는 캡처 시점/부하에 따라 상이 (tree와 스크린샷은 별도 60s 캡처) — 핵심은 **비율 + 콜스택** - 대조군 `_synthesize_answer`(요청 종료 시 해제)는 leaks 리포트 **미등장** → 오탐 없음 - 상단 노란 배너 = pymalloc 안내: 소형 객체는 풀 잔류 노이즈 가능 → @@ -176,7 +201,7 @@ kubectl rollout restart deploy/leaky-agent -n memray-demo | attach 권한 | 앱 파드에 SYS_PTRACE 불필요. 단 RBAC `pods/ephemeralcontainers` + privileged 허용 정책 필요 | | 멀티워커 (gunicorn) | `ps`로 워커 PID 확인 → 워커별 attach (`--follow-fork`는 run 모드 전용) | | 오버헤드 | attach 추적 중 수 배 감속 가능 — 카나리아/단일 파드에 짧게 | -| 상시 프로파일링 | memray = 스팟 조사용. 상시는 [Pyroscope](./pyroscope_anf_s3_deployment.md) 등 continuous profiler 병행 | +| 상시 프로파일링 | memray = 스팟 조사용. 상시는 [Pyroscope](../pyroscope-anf-s3/index.md) 등 continuous profiler 병행 | - **잘 잡는 것**: Python 힙 상주 객체 증가형 누수, C 확장 malloc 누수(`--native`) - **한계**: macOS는 allocator 패치 불가 → 사실상 Linux 전용 (로컬 재현도 컨테이너에서), attach는 attach 이후 할당만 추적 @@ -185,7 +210,7 @@ kubectl rollout restart deploy/leaky-agent -n memray-demo ## Appendix -재현 환경 전체 코드: [`memray-leak-profiling/`](./memray-leak-profiling/) +재현 환경 전체 코드: [`memray-leak-profiling/`](https://github.com/hellices/devguidesample/tree/main/samples/azure-kubernetes-service/source-material/memray-leak-profiling) ``` memray-leak-profiling/ @@ -215,7 +240,7 @@ memray-leak-profiling/ (agent 서비스에서 대화 메모리/툴 로그 무제한 축적 시 동일 증상) - `/stats`로 RSS/세션 수/히스토리 수 노출 → 관찰용 -앱 이미지는 clean 유지 ([Dockerfile](./memray-leak-profiling/Dockerfile)): +앱 이미지는 clean 유지 ([Dockerfile](https://github.com/hellices/devguidesample/blob/main/samples/azure-kubernetes-service/source-material/memray-leak-profiling/Dockerfile)): ```dockerfile FROM python:3.12-slim @@ -244,7 +269,7 @@ az aks get-credentials -g rg-memray-demo -n aks-memray-demo 이미지 빌드 — 로컬 docker 불필요, ACR 클라우드 빌드 (Apple Silicon 크로스빌드 이슈도 회피): ```bash -cd aks/memray-leak-profiling +cd samples/azure-kubernetes-service/source-material/memray-leak-profiling az acr build --registry --image leaky-agent:v2 --platform linux/amd64 . # 24s az acr build --registry --image memray-debug:v1 --platform linux/amd64 ./debug # 33s ``` diff --git a/aks/local_dev_loop.md b/docs/guides/azure-kubernetes-service/remote-cluster-local-development/index.md similarity index 88% rename from aks/local_dev_loop.md rename to docs/guides/azure-kubernetes-service/remote-cluster-local-development/index.md index 14ebec7b..a9bf9325 100644 --- a/aks/local_dev_loop.md +++ b/docs/guides/azure-kubernetes-service/remote-cluster-local-development/index.md @@ -1,3 +1,27 @@ +--- +services: +- azure-kubernetes-service +official_sources: +- title: Azure Kubernetes Service documentation + url: https://learn.microsoft.com/azure/aks/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: AKS 원격 클러스터와 로컬 개발 루프 +description: Telepresence와 mirrord를 이용해 AKS 트래픽을 로컬 개발 환경으로 연결합니다. +technologies: +- kubernetes +- python +tags: +- development +- networking +--- + # AKS 원격 클러스터 + 로컬 개발 루프 (Telepresence / mirrord) AKS에 배포된 서비스를 대상으로, **코드를 이미지로 빌드/배포하지 않고** @@ -12,7 +36,7 @@ AKS에 배포된 서비스를 대상으로, **코드를 이미지로 빌드/배 - [Use Telepresence to develop and test microservices locally — MS Learn](https://learn.microsoft.com/en-us/azure/aks/use-telepresence-aks) - [Local Development on AKS with mirrord — AKS Engineering Blog](https://blog.aks.azure.com/2024/12/04/mirrord-on-aks) -예제 파일: [`local_dev_loop/`](./local_dev_loop/) +예제 파일: [`local_dev_loop/`](https://github.com/hellices/devguidesample/tree/main/samples/azure-kubernetes-service/source-material/local_dev_loop) ``` local_dev_loop/ @@ -26,7 +50,7 @@ local_dev_loop/ ## 📌 사전 요구사항 -- Azure CLI, `kubectl`, AKS 클러스터 (예시: `aks-customvec-krc` @ `rg-aiplay-krc-01`) +- Azure CLI, `kubectl`, AKS 클러스터 (예시: `aks-example-koreacentral-01` @ `rg-example-koreacentral-01`) - Telepresence: `brew install telepresenceio/telepresence/telepresence-oss` - mirrord: `brew install metalbear-co/mirrord/mirrord` - (UI 사용 시) VS Code + `code --install-extension MetalBear.mirrord` @@ -36,8 +60,8 @@ local_dev_loop/ ## 1. 대상 서비스 배포 ```bash -az aks get-credentials -g rg-aiplay-krc-01 -n aks-customvec-krc -kubectl apply -f local_dev_loop/k8s/echo-service.yaml +az aks get-credentials -g rg-example-koreacentral-01 -n aks-example-koreacentral-01 +kubectl apply -f samples/azure-kubernetes-service/source-material/local_dev_loop/k8s/echo-service.yaml kubectl -n tp-demo rollout status deploy/echo-server ``` @@ -180,7 +204,7 @@ F5(Run/Debug)만으로 mirrord가 적용된다. **브레이크포인트를 찍 "name": "mirrord demo: local-server (echo-server steal)", "type": "debugpy", "request": "launch", - "program": "${workspaceFolder}/aks/local_dev_loop/local/local-server.py", + "program": "${workspaceFolder}/samples/azure-kubernetes-service/source-material/local_dev_loop/local/local-server.py", "console": "integratedTerminal", "env": { "MIRRORD_ACTIVE": "1", @@ -234,10 +258,10 @@ telepresence helm uninstall # Traffic Manager 제거 # mirrord — agent pod는 세션 종료 시 자동 삭제됨 # 샘플 서비스 제거 -kubectl delete -f local_dev_loop/k8s/echo-service.yaml +kubectl delete -f samples/azure-kubernetes-service/source-material/local_dev_loop/k8s/echo-service.yaml # 비용 절약: 클러스터 중지 -az aks stop -g rg-aiplay-krc-01 -n aks-customvec-krc +az aks stop -g rg-example-koreacentral-01 -n aks-example-koreacentral-01 ``` > ⚠️ 예시 클러스터는 시작 시 `gpuspot` 노드풀(NC16as_T4_v3 spot ×3)에 diff --git a/aks/spot_gpu_kaito_llm.md b/docs/guides/azure-kubernetes-service/spot-h100-kaito/index.md similarity index 95% rename from aks/spot_gpu_kaito_llm.md rename to docs/guides/azure-kubernetes-service/spot-h100-kaito/index.md index 22633311..3828c5e6 100644 --- a/aks/spot_gpu_kaito_llm.md +++ b/docs/guides/azure-kubernetes-service/spot-h100-kaito/index.md @@ -1,3 +1,28 @@ +--- +services: +- azure-kubernetes-service +official_sources: +- title: Azure Kubernetes Service documentation + url: https://learn.microsoft.com/azure/aks/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: AKS Spot H100 GPU와 KAITO LLM 서빙 +description: AKS Spot H100 GPU 노드에서 KAITO 기반 모델을 배포하는 절차를 설명합니다. +technologies: +- kubernetes +- azure-cli +tags: +- ai-agents +- deployment +- performance +--- + # ✅ AKS Spot H100 GPU + KAITO LLM 서빙 가이드 **AKS에서 Azure Spot H100 GPU 노드와 KAITO(AI Toolchain Operator)로 `phi-4-mini-instruct`를 서빙한 End-to-End 검증 사례** @@ -232,7 +257,7 @@ AKS에서 Azure Spot H100 GPU 노드를 사용하고, KAITO(AI Toolchain Operato | GPU 노드풀 (Spot) | `gpuspot`, `Standard_NC40ads_H100_v5` × 1 (H100 94GB, Spot, max-price -1), autoscaler 0-1 | | GPU 노드 Taint | `sku=gpu:NoSchedule` + AKS 자동 `kubernetes.azure.com/scalesetpriority=spot:NoSchedule` | | 모델 | `phi-4-mini-instruct` (KAITO preset, vLLM runtime) | -| Workspace CR | [workspace-phi4-mini.yaml](workspace-phi4-mini.yaml) | +| Workspace CR | [workspace-phi4-mini.yaml](#4-kaito-workspace) | | Service | `workspace-phi-4-mini` ClusterIP:80 (OpenAI 호환 API) | > Spot VM은 Spot Core 쿼터가 설정되어 있지 않아도 배포 가능 (정규 H100 쿼터와 무관). diff --git a/aks/workload_identity_databricks_keyless.md b/docs/guides/azure-kubernetes-service/workload-identity-databricks/index.md similarity index 87% rename from aks/workload_identity_databricks_keyless.md rename to docs/guides/azure-kubernetes-service/workload-identity-databricks/index.md index c8ecce48..2871c68e 100644 --- a/aks/workload_identity_databricks_keyless.md +++ b/docs/guides/azure-kubernetes-service/workload-identity-databricks/index.md @@ -1,8 +1,34 @@ +--- +services: +- azure-kubernetes-service +official_sources: +- title: Azure Kubernetes Service documentation + url: https://learn.microsoft.com/azure/aks/ +- title: Authenticate with managed identity + url: https://learn.microsoft.com/azure/api-management/authentication-managed-identity-policy +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: AKS Workload Identity로 Databricks 키리스 호출 +description: AKS Workload Identity 토큰으로 Databricks Model Serving을 비밀 없이 호출합니다. +technologies: +- kubernetes +- nodejs +tags: +- authentication +- security +--- + # AKS Workload Identity로 Databricks Model Serving 키리스(Keyless) 호출 > 시나리오: AKS Pod → Databricks Foundation Model API (`databricks-meta-llama-3-1-8b-instruct`) > 어떤 PAT/시크릿/Databricks OAuth secret도 사용하지 않고, **AKS Workload Identity**가 발급한 Entra ID(AAD) 액세스 토큰을 그대로 Databricks Bearer 토큰으로 사용합니다. -> APIM 가이드(`apim/databricks_keyless_managed_identity.md`)와 같은 인증 모델을 Pod-측에서 구현한 패턴. +> [APIM `authentication-managed-identity` 정책](https://learn.microsoft.com/azure/api-management/authentication-managed-identity-policy)과 같은 인증 모델을 Pod 측에서 구현한 패턴입니다. --- @@ -195,7 +221,7 @@ POST {DBX_HOST}/serving-endpoints/{ENDPOINT}/invocations → 200 + predictions - HTTP keep-alive / HTTP/2 풀 (`undici Pool`) - 타임아웃 / AbortSignal / 재시도 -> 인증 모델은 [apim/databricks_keyless_managed_identity.md](../apim/databricks_keyless_managed_identity.md) 의 APIM 정책(`authentication-managed-identity`)과 동일. APIM에서는 게이트웨이가 토큰을 받아 붙여주지만, 여기서는 앱 코드가 같은 일을 두 줄로 합니다. +> 인증 모델은 [APIM `authentication-managed-identity` 정책](https://learn.microsoft.com/azure/api-management/authentication-managed-identity-policy)과 동일합니다. APIM에서는 게이트웨이가 토큰을 받아 붙여주지만, 여기서는 앱 코드가 같은 일을 두 줄로 합니다. --- @@ -357,7 +383,7 @@ Client → Application Gateway (WAF, TLS) → Internal Service / Ingress → Pod - Pod 자체 노출 금지: Service를 `ClusterIP`로 바꾸고 AGIC/NGINX Ingress를 Application Gateway 백엔드로. - AppGW WAF_v2로 외부 인입 보호, 클라이언트는 자체 도메인(`api.contoso.com`)으로 호출. -- 필요하면 Pod 앞에 APIM(Internal VNet)을 추가해 `apim/databricks_keyless_managed_identity.md` 의 4계층 패턴(Client → AppGW → APIM → 백엔드) 적용 가능. 단, 이 경우 인증 부착 책임을 APIM 정책으로 옮길지(Pod에서 제거), 양쪽 다 둘지(이중 keyless) 결정 필요. +- 필요하면 Pod 앞에 APIM(Internal VNet)을 추가해 Client → AppGW → APIM → 백엔드의 4계층 패턴을 적용할 수 있습니다. 이 경우 인증 부착 책임을 [APIM `authentication-managed-identity` 정책](https://learn.microsoft.com/azure/api-management/authentication-managed-identity-policy)으로 옮길지(Pod에서 제거), 양쪽에 둘지 결정해야 합니다. --- @@ -381,4 +407,4 @@ Client → Application Gateway (WAF, TLS) → Internal Service / Ingress → Pod - AKS Workload Identity: https://learn.microsoft.com/azure/aks/workload-identity-overview - Databricks AAD audience: https://learn.microsoft.com/azure/databricks/dev-tools/api/latest/aad/service-prin-aad-token - Databricks Serving Endpoints REST: https://docs.databricks.com/api/workspace/servingendpoints -- 자매 가이드 (게이트웨이 측 keyless): `apim/databricks_keyless_managed_identity.md` +- 게이트웨이 측 keyless 참고: [APIM `authentication-managed-identity` 정책](https://learn.microsoft.com/azure/api-management/authentication-managed-identity-policy) diff --git a/loadtest/images/01-test-list.png b/docs/guides/azure-load-testing/locust-appgw-aks-private/images/01-test-list.png similarity index 100% rename from loadtest/images/01-test-list.png rename to docs/guides/azure-load-testing/locust-appgw-aks-private/images/01-test-list.png diff --git a/loadtest/images/02-run-summary.png b/docs/guides/azure-load-testing/locust-appgw-aks-private/images/02-run-summary.png similarity index 100% rename from loadtest/images/02-run-summary.png rename to docs/guides/azure-load-testing/locust-appgw-aks-private/images/02-run-summary.png diff --git a/loadtest/images/03-request-statistics.png b/docs/guides/azure-load-testing/locust-appgw-aks-private/images/03-request-statistics.png similarity index 100% rename from loadtest/images/03-request-statistics.png rename to docs/guides/azure-load-testing/locust-appgw-aks-private/images/03-request-statistics.png diff --git a/loadtest/images/04-client-side-metrics.png b/docs/guides/azure-load-testing/locust-appgw-aks-private/images/04-client-side-metrics.png similarity index 100% rename from loadtest/images/04-client-side-metrics.png rename to docs/guides/azure-load-testing/locust-appgw-aks-private/images/04-client-side-metrics.png diff --git a/loadtest/images/05-server-side-metrics.png b/docs/guides/azure-load-testing/locust-appgw-aks-private/images/05-server-side-metrics.png similarity index 100% rename from loadtest/images/05-server-side-metrics.png rename to docs/guides/azure-load-testing/locust-appgw-aks-private/images/05-server-side-metrics.png diff --git a/loadtest/images/06-engine-health.png b/docs/guides/azure-load-testing/locust-appgw-aks-private/images/06-engine-health.png similarity index 100% rename from loadtest/images/06-engine-health.png rename to docs/guides/azure-load-testing/locust-appgw-aks-private/images/06-engine-health.png diff --git a/loadtest/images/07-compare-select.png b/docs/guides/azure-load-testing/locust-appgw-aks-private/images/07-compare-select.png similarity index 100% rename from loadtest/images/07-compare-select.png rename to docs/guides/azure-load-testing/locust-appgw-aks-private/images/07-compare-select.png diff --git a/loadtest/images/08-compare-result.png b/docs/guides/azure-load-testing/locust-appgw-aks-private/images/08-compare-result.png similarity index 100% rename from loadtest/images/08-compare-result.png rename to docs/guides/azure-load-testing/locust-appgw-aks-private/images/08-compare-result.png diff --git a/loadtest/loadtest_locust_appgw_aks.md b/docs/guides/azure-load-testing/locust-appgw-aks-private/index.md similarity index 96% rename from loadtest/loadtest_locust_appgw_aks.md rename to docs/guides/azure-load-testing/locust-appgw-aks-private/index.md index bcf569cf..802a2716 100644 --- a/loadtest/loadtest_locust_appgw_aks.md +++ b/docs/guides/azure-load-testing/locust-appgw-aks-private/index.md @@ -1,3 +1,28 @@ +--- +services: +- azure-load-testing +official_sources: +- title: Azure Load Testing documentation + url: https://learn.microsoft.com/azure/load-testing/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Private 환경 Locust 부하 테스트 구성 +description: Application Gateway와 AKS를 대상으로 Private 환경에서 Locust 부하 테스트를 구성합니다. +technologies: +- python +- kubernetes +tags: +- benchmarking +- networking +- performance +--- + # Korea Central Private 환경의 Locust 부하테스트 구성 가이드 ## 📌 문제 상황 @@ -7,7 +32,7 @@ Azure Load Testing(현 Azure App Testing)은 Korea Central을 지원하지 않 > ⚠️ **Azure Load Testing은 2026년 기준 Korea Central / Korea South에서 사용 불가**하며, 가장 가까운 지원 리전은 **Japan East**이다. -> 📌 이 가이드는 **AKS + AGIC + Private AppGW가 이미 구성**되어 있다고 가정한다. 테스트 타깃이 없는 경우 [부록 A](#-부록-a-테스트-타깃-구성-aks--agic--backend)를 먼저 수행한다. +> 📌 이 가이드는 **AKS + AGIC + Private AppGW가 이미 구성**되어 있다고 가정한다. 테스트 타깃이 없는 경우 [부록 A](#a-aks-agic-backend)를 먼저 수행한다. ## 🔍 구성 개요 @@ -502,7 +527,7 @@ backend-app-ingress * 10.0.1.10 80 16s | AppGW 404 Not Found | AGIC가 ARM으로 AppGW를 비동기 생성 | 권한 부여 → pod 재시작 → 5~10분 대기 | | Ingress ADDRESS 비어 있음 | Private frontend IP 추가 전 상태 | Private IP 추가 후 Ingress 재생성 | -부록 A 완료 후 본문 [Step 1](#step-1-부하테스트용-vnet-생성-japan-east)부터 진행한다. +부록 A 완료 후 본문 [Step 1](#step-1-vnet-japan-east)부터 진행한다. --- diff --git a/monitor/otel-monitor-setup.md b/docs/guides/azure-monitor/aks-private-opentelemetry/index.md similarity index 84% rename from monitor/otel-monitor-setup.md rename to docs/guides/azure-monitor/aks-private-opentelemetry/index.md index ab660142..329d34de 100644 --- a/monitor/otel-monitor-setup.md +++ b/docs/guides/azure-monitor/aks-private-opentelemetry/index.md @@ -1,21 +1,46 @@ +--- +services: +- azure-monitor +official_sources: +- title: Azure Monitor documentation + url: https://learn.microsoft.com/azure/azure-monitor/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: AKS 폐쇄망 OpenTelemetry와 Azure Monitor 구성 +description: 폐쇄망 AKS에서 OpenTelemetry와 Azure Monitor 메트릭 파이프라인을 구축합니다. +technologies: +- kubernetes +- open-telemetry +tags: +- observability +- monitoring +- networking +--- + # AKS 폐쇄망 환경 OpenTelemetry + Azure Monitor 메트릭 파이프라인 구축 ## 1. 환경 개요 | 항목 | 값 | |------|-----| -| AKS 클러스터 | `aks-rubicon-koreacentral-01` (Private Cluster) | -| 리소스 그룹 | `rg-rubicon-koreacentral-01` | -| PRD 리소스 그룹 | `rg-rubicon-koreacentral-prd-01` | -| 구독 | `f752aff6-b20c-4973-b32b-0a60ba2c6764` | +| AKS 클러스터 | `aks-example-koreacentral-01` (Private Cluster) | +| 리소스 그룹 | `rg-example-koreacentral-01` | +| PRD 리소스 그룹 | `rg-example-koreacentral-prd-01` | +| 구독 | `` | | 리전 | Korea Central | -| VNet (기존) | `vnet-rubicon-koreacentral-01` (`10.0.0.0/10`) | -| VNet (PRD) | `vnet-rubicon-koreacentral-prd-01` (`10.100.0.0/16`) | +| VNet (기존) | `vnet-example-koreacentral-01` (`10.0.0.0/10`) | +| VNet (PRD) | `vnet-example-koreacentral-prd-01` (`10.100.0.0/16`) | | AKS 서브넷 | `aks-subnet` (`10.1.0.0/24`) | | PE 서브넷 | `pe-subnet` (`10.2.0.0/22`) | -| Azure Firewall | `fw-rubicon-koreacentral-prd-01` (Private IP: `10.100.0.4`) | -| Azure Monitor Workspace | `azuremonitor-workspace-rubicon-krc-01` | -| Managed Grafana | `grafanaworkspacerubicon` | +| Azure Firewall | `fw-example-koreacentral-prd-01` (Private IP: `10.100.0.4`) | +| Azure Monitor Workspace | `azuremonitor-workspace-example-krc-01` | +| Managed Grafana | `grafanaworkspaceexample` | | 노드 Taint | `workload=general:NoSchedule`, `CriticalAddonsOnly=true:NoSchedule` | | 네트워크 특성 | **폐쇄망** (방화벽 TLS Inspection, 아웃바운드 제한) | | DNS 구성 | 양쪽 VNet → Firewall DNS Proxy (`10.100.0.4`) → Azure DNS | @@ -51,11 +76,11 @@ flowchart TB end subgraph AZURE["Azure (Private Link)"] - subgraph AMPLS_SCOPE["AMPLS: ampls-rubicon-krc-01"] - DCE["DCE
MSProm-koreacentral-
aks-rubicon-koreacentral"] - DCE2["DCE
azuremonitor-workspace-
rubicon-krc-01"] + subgraph AMPLS_SCOPE["AMPLS: ampls-example-krc-01"] + DCE["DCE
MSProm-koreacentral-
aks-example-koreacentral"] + DCE2["DCE
azuremonitor-workspace-
example-krc-01"] end - PE["Private Endpoint
pe-ampls-rubicon-krc-01
(pe-subnet)"] + PE["Private Endpoint
pe-ampls-example-krc-01
(pe-subnet)"] AMW["Azure Monitor
Workspace"] GRAFANA["Managed Grafana"] end @@ -75,9 +100,9 @@ flowchart TB DNS -.->|"PRD VNet에 링크됨"| AKS subgraph NET["네트워크 / DNS"] - EXISTING_VNET["vnet-rubicon-koreacentral-01
10.0.0.0/10"] - PRD_VNET["vnet-rubicon-koreacentral-prd-01
10.100.0.0/16"] - FIREWALL["Azure Firewall
fw-rubicon-koreacentral-prd-01
DNS Proxy: 10.100.0.4"] + EXISTING_VNET["vnet-example-koreacentral-01
10.0.0.0/10"] + PRD_VNET["vnet-example-koreacentral-prd-01
10.100.0.0/16"] + FIREWALL["Azure Firewall
fw-example-koreacentral-prd-01
DNS Proxy: 10.100.0.4"] EXISTING_VNET <-->|"VNet Peering"| PRD_VNET PRD_VNET --- FIREWALL FIREWALL -->|"DNS Proxy → Azure DNS"| DNS @@ -239,12 +264,12 @@ kubectl apply -f otel-collector.yaml **포털에서 생성:** 1. Azure Portal → **Azure Monitor Private Link Scope** → 만들기 -2. 이름: `ampls-rubicon-krc-01` +2. 이름: `ampls-example-krc-01` 3. 연결된 리소스 (DCE 2개 추가): - - `MSProm-koreacentral-aks-rubicon-koreacentral` (RG: `rg-rubicon-koreacentral-01`) - - `azuremonitor-workspace-rubicon-krc-01` (RG: `MA_azuremonitor-workspace-rubicon-krc-01_koreacentral_managed`) + - `MSProm-koreacentral-aks-example-koreacentral` (RG: `rg-example-koreacentral-01`) + - `azuremonitor-workspace-example-krc-01` (RG: `MA_azuremonitor-workspace-example-krc-01_koreacentral_managed`) 4. Private Endpoint 생성: - - 이름: `pe-ampls-rubicon-krc-01` + - 이름: `pe-ampls-example-krc-01` - 서브넷: `pe-subnet` - Private DNS Zone 통합: 사용 @@ -258,13 +283,13 @@ AMPLS 생성 시 자동으로 5개 DNS Zone이 생성됨. | Private DNS Zone | 용도 | VNet 링크 | |---|---|---| -| `privatelink.monitor.azure.com` | 메트릭 수집 엔드포인트 | `vnet-rubicon-koreacentral-prd-01` | -| `privatelink.ods.opinsights.azure.com` | ODS 데이터 채널 | `vnet-rubicon-koreacentral-prd-01` | -| `privatelink.oms.opinsights.azure.com` | OMS 포털 | `vnet-rubicon-koreacentral-prd-01` | -| `privatelink.agentsvc.azure-automation.net` | 에이전트 서비스 | `vnet-rubicon-koreacentral-prd-01` | -| `privatelink.blob.core.windows.net` | Blob 스토리지 | `vnet-rubicon-koreacentral-prd-01` | -| `*.privatelink.koreacentral.azmk8s.io` | AKS API Server | `vnet-rubicon-koreacentral-prd-01` | -| `privatelink.redis.azure.net` | Redis Cache | `vnet-rubicon-koreacentral-prd-01` | +| `privatelink.monitor.azure.com` | 메트릭 수집 엔드포인트 | `vnet-example-koreacentral-prd-01` | +| `privatelink.ods.opinsights.azure.com` | ODS 데이터 채널 | `vnet-example-koreacentral-prd-01` | +| `privatelink.oms.opinsights.azure.com` | OMS 포털 | `vnet-example-koreacentral-prd-01` | +| `privatelink.agentsvc.azure-automation.net` | 에이전트 서비스 | `vnet-example-koreacentral-prd-01` | +| `privatelink.blob.core.windows.net` | Blob 스토리지 | `vnet-example-koreacentral-prd-01` | +| `*.privatelink.koreacentral.azmk8s.io` | AKS API Server | `vnet-example-koreacentral-prd-01` | +| `privatelink.redis.azure.net` | Redis Cache | `vnet-example-koreacentral-prd-01` | **DNS 해석 경로:** ``` @@ -278,7 +303,7 @@ VM/AKS Pod → Firewall DNS Proxy (10.100.0.4) → Azure DNS (168.63.129.16) **DNS 해석 확인:** ```bash # AKS 파드 내부에서 -nslookup msprom-koreacentral-aks-rubicon-koreacentral-ruja.koreacentral-1.metrics.ingest.monitor.azure.com +nslookup msprom-koreacentral-aks-example-koreacentral-ruja.koreacentral-1.metrics.ingest.monitor.azure.com # → 10.2.0.6 (Private IP ✅) nslookup global.handler.control.monitor.azure.com @@ -296,8 +321,8 @@ nslookup global.handler.control.monitor.azure.com | DCR 이름 | 리소스 그룹 | 연결할 DCE | |---|---|---| -| `MSProm-koreacentral-aks-rubicon-koreacentral-01` | `rg-rubicon-koreacentral-01` | `MSProm-koreacentral-aks-rubicon-koreacentral` | -| `azuremonitor-workspace-rubicon-krc-01` | `MA_..._managed` | `azuremonitor-workspace-rubicon-krc-01` | +| `MSProm-koreacentral-aks-example-koreacentral-01` | `rg-example-koreacentral-01` | `MSProm-koreacentral-aks-example-koreacentral` | +| `azuremonitor-workspace-example-krc-01` | `MA_..._managed` | `azuremonitor-workspace-example-krc-01` | ### 3.6 ⭐ configurationAccessEndpoint DCRA 생성 (핵심!) @@ -329,7 +354,7 @@ nslookup global.handler.control.monitor.azure.com #### 방법 1: 포털에서 설정 1. Azure Portal → **Monitor** → **데이터 수집 규칙 (Data Collection Rules)** -2. Prometheus DCR 선택 (예: `MSProm-koreacentral-aks-rubicon-koreacentral-01`) +2. Prometheus DCR 선택 (예: `MSProm-koreacentral-aks-example-koreacentral-01`) 3. 왼쪽 메뉴 → **리소스 (Resources)** 탭 4. AKS 클러스터 리소스의 **데이터 수집 엔드포인트** 열에서 DCE 선택 5. 저장 → `configurationAccessEndpoint` DCRA가 자동 생성됨 @@ -338,14 +363,14 @@ nslookup global.handler.control.monitor.azure.com ```bash az rest --method put \ - --url "/subscriptions/f752aff6-b20c-4973-b32b-0a60ba2c6764/resourceGroups/rg-rubicon-koreacentral-01/providers/Microsoft.ContainerService/managedClusters/aks-rubicon-koreacentral-01/providers/Microsoft.Insights/dataCollectionRuleAssociations/configurationAccessEndpoint?api-version=2023-03-11" \ - --body '{"properties":{"dataCollectionEndpointId":"/subscriptions/f752aff6-b20c-4973-b32b-0a60ba2c6764/resourceGroups/rg-rubicon-koreacentral-01/providers/Microsoft.Insights/dataCollectionEndpoints/MSProm-koreacentral-aks-rubicon-koreacentral"}}' + --url "/subscriptions//resourceGroups/rg-example-koreacentral-01/providers/Microsoft.ContainerService/managedClusters/aks-example-koreacentral-01/providers/Microsoft.Insights/dataCollectionRuleAssociations/configurationAccessEndpoint?api-version=2023-03-11" \ + --body '{"properties":{"dataCollectionEndpointId":"/subscriptions//resourceGroups/rg-example-koreacentral-01/providers/Microsoft.Insights/dataCollectionEndpoints/MSProm-koreacentral-aks-example-koreacentral"}}' ``` **확인:** ```bash az rest --method get \ - --url "/subscriptions/f752aff6-b20c-4973-b32b-0a60ba2c6764/resourceGroups/rg-rubicon-koreacentral-01/providers/Microsoft.ContainerService/managedClusters/aks-rubicon-koreacentral-01/providers/Microsoft.Insights/dataCollectionRuleAssociations?api-version=2023-03-11" \ + --url "/subscriptions//resourceGroups/rg-example-koreacentral-01/providers/Microsoft.ContainerService/managedClusters/aks-example-koreacentral-01/providers/Microsoft.Insights/dataCollectionRuleAssociations?api-version=2023-03-11" \ | python3 -c " import sys,json data=json.load(sys.stdin) @@ -363,7 +388,7 @@ for a in data.get('value',[]): |---|---|---| | `ContainerInsightsExtension` | `MSCI-koreacentral-...` | N/A | | `ContainerInsightsMetricsExtension` | `MSProm-koreacentral-...` | N/A | -| **`configurationAccessEndpoint`** | N/A | **`MSProm-koreacentral-aks-rubicon-koreacentral`** ✅ | +| **`configurationAccessEndpoint`** | N/A | **`MSProm-koreacentral-aks-example-koreacentral`** ✅ | --- @@ -495,13 +520,13 @@ DNS를 PRD 환경에서 중앙 관리하기 위해 별도의 PRD VNet을 생성 | 리소스 | 이름 | 설정 | |---|---|---| -| Resource Group | `rg-rubicon-koreacentral-prd-01` | Korea Central | -| VNet | `vnet-rubicon-koreacentral-prd-01` | `10.100.0.0/16` | +| Resource Group | `rg-example-koreacentral-prd-01` | Korea Central | +| VNet | `vnet-example-koreacentral-prd-01` | `10.100.0.0/16` | | Subnet (Firewall) | `AzureFirewallSubnet` | `10.100.0.0/26` | | Subnet (Default) | `default` | `10.100.1.0/24` | -| Azure Firewall | `fw-rubicon-koreacentral-prd-01` | Standard SKU, DNS Proxy 활성화 | +| Azure Firewall | `fw-example-koreacentral-prd-01` | Standard SKU, DNS Proxy 활성화 | | Firewall Private IP | `10.100.0.4` | DNS Proxy 엔드포인트 | -| Public IP | `pip-fw-rubicon-koreacentral-prd-01` | Standard SKU | +| Public IP | `pip-fw-example-koreacentral-prd-01` | Standard SKU | ### 6.3 네트워크 구성 @@ -527,8 +552,8 @@ Custom DNS Server: 10.100.0.4 (Firewall DNS Proxy) ```mermaid flowchart TB - VM["VM / AKS Pod
(vnet-rubicon-koreacentral-01)"] -->|"DNS Query"| RESOLVER["systemd-resolved
127.0.0.53"] - RESOLVER -->|"Forward to
Custom DNS: 10.100.0.4"| FW["Azure Firewall
DNS Proxy
(vnet-rubicon-koreacentral-prd-01)"] + VM["VM / AKS Pod
(vnet-example-koreacentral-01)"] -->|"DNS Query"| RESOLVER["systemd-resolved
127.0.0.53"] + RESOLVER -->|"Forward to
Custom DNS: 10.100.0.4"| FW["Azure Firewall
DNS Proxy
(vnet-example-koreacentral-prd-01)"] FW -->|"Forward"| AZURE_DNS["Azure DNS
168.63.129.16"] AZURE_DNS -->|"Private DNS Zone
(PRD VNet에 링크됨)"| PRIVATE_IP["Private IP 반환
10.2.0.x / 10.1.0.4"] @@ -539,7 +564,7 @@ flowchart TB ### 6.5 Private DNS Zone VNet 링크 이전 -기존 VNet(`vnet-rubicon-koreacentral-01`)에 연결되어 있던 모든 Private DNS Zone 링크를 PRD VNet으로 이전. +기존 VNet(`vnet-example-koreacentral-01`)에 연결되어 있던 모든 Private DNS Zone 링크를 PRD VNet으로 이전. **이전 대상:** - AMPLS 관련 5개 Zone (`privatelink.monitor.azure.com` 등) @@ -553,7 +578,7 @@ az network private-dns link vnet create \ --name "link-to-prd-vnet" \ --zone-name "" \ --resource-group "" \ - --virtual-network "/subscriptions/.../vnet-rubicon-koreacentral-prd-01" \ + --virtual-network "/subscriptions/.../vnet-example-koreacentral-prd-01" \ --registration-enabled false # 기존 VNet 링크 제거 @@ -567,7 +592,7 @@ az network private-dns link vnet delete \ ### 6.6 AMPLS/DCE 영향 Firewall DNS Proxy 구성 후에도 AMPLS/DCE는 영향 없음: -- Private Endpoint(`pe-ampls-rubicon-krc-01`)는 기존 VNet의 `pe-subnet`에 그대로 위치 +- Private Endpoint(`pe-ampls-example-krc-01`)는 기존 VNet의 `pe-subnet`에 그대로 위치 - DNS 해석 경로만 변경: VM → Firewall(10.100.0.4) → Azure DNS → Private DNS Zone(PRD VNet 링크) → Private IP(10.2.0.x) - 데이터 경로(ama-metrics → PE → Azure Monitor)는 변경 없음 diff --git a/monitor/sre-agent-event-lab/assets/official/agent-reasoning-flow.svg b/docs/guides/azure-monitor/azure-sre-agent-overview/images/agent-reasoning-flow.svg similarity index 97% rename from monitor/sre-agent-event-lab/assets/official/agent-reasoning-flow.svg rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/agent-reasoning-flow.svg index 4de43d3e..2b0b0b00 100644 --- a/monitor/sre-agent-event-lab/assets/official/agent-reasoning-flow.svg +++ b/docs/guides/azure-monitor/azure-sre-agent-overview/images/agent-reasoning-flow.svg @@ -1,75 +1,75 @@ - - - - - - - - - - - Your question - or incident alert - - - - - - - Understand - parse request - - - - - - - Gather context - parallel queries - - - - - - - Reason - analyze patterns - - - - - - - - - Execute - safe actions run immediately - - - - Ask for approval - destructive actions wait - - - - Respond - present findings - - - - iterate (up to 10×) - - - Your agent adjusts reasoning depth automatically — simple questions get quick answers, complex incidents get multistep analysis. - + + + + + + + + + + + Your question + or incident alert + + + + + + + Understand + parse request + + + + + + + Gather context + parallel queries + + + + + + + Reason + analyze patterns + + + + + + + + + Execute + safe actions run immediately + + + + Ask for approval + destructive actions wait + + + + Respond + present findings + + + + iterate (up to 10×) + + + Your agent adjusts reasoning depth automatically — simple questions get quick answers, complex incidents get multistep analysis. + diff --git a/monitor/sre-agent-event-lab/assets/official/azure-sre-agent-networking-vnet.png b/docs/guides/azure-monitor/azure-sre-agent-overview/images/azure-sre-agent-networking-vnet.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/official/azure-sre-agent-networking-vnet.png rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/azure-sre-agent-networking-vnet.png diff --git a/monitor/sre-agent-event-lab/assets/official/custom-skill-flow.svg b/docs/guides/azure-monitor/azure-sre-agent-overview/images/custom-skill-flow.svg similarity index 96% rename from monitor/sre-agent-event-lab/assets/official/custom-skill-flow.svg rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/custom-skill-flow.svg index e0cd0dd6..136ef56d 100644 --- a/monitor/sre-agent-event-lab/assets/official/custom-skill-flow.svg +++ b/docs/guides/azure-monitor/azure-sre-agent-overview/images/custom-skill-flow.svg @@ -1,44 +1,44 @@ - - - - - - - - - - - - - - - Agent - - - - - - - Custom Skill - - - - - - - Your API - - - - - - - Returns data - - - - - - - → - + + + + + + + + + + + + + + + Agent + + + + + + + Custom Skill + + + + + + + Your API + + + + + + + Returns data + + + + + + + → + diff --git a/monitor/sre-agent-event-lab/assets/official/diagnose-azure-services.svg b/docs/guides/azure-monitor/azure-sre-agent-overview/images/diagnose-azure-services.svg similarity index 98% rename from monitor/sre-agent-event-lab/assets/official/diagnose-azure-services.svg rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/diagnose-azure-services.svg index e5b51c5b..31cad4ac 100644 --- a/monitor/sre-agent-event-lab/assets/official/diagnose-azure-services.svg +++ b/docs/guides/azure-monitor/azure-sre-agent-overview/images/diagnose-azure-services.svg @@ -1,82 +1,82 @@ - - - - - - - - - - - - - - - - - How your agent diagnoses Azure services - - - - Service - issue - - - - - - - SRE Agent - queries all sources - correlates evidence - explains root cause - - - - - - Application Insights - KQL queries, traces, dependencies - - - - - Log Analytics - Workspace logs, custom events - - - - - Azure Monitor Metrics - CPU, memory, requests, analysis - - - - - Resource Graph & Activity - Topology, changes, deployment logs - - - - - Built-in Skills & Az CLI - Deep diagnostics, connectivity, - resource-specific checks, remediation - - - - - - - - - - - - Correlated - diagnosis - Evidence from all sources - - - No connectors needed — works through managed identity and Azure RBAC - + + + + + + + + + + + + + + + + + How your agent diagnoses Azure services + + + + Service + issue + + + + + + + SRE Agent + queries all sources + correlates evidence + explains root cause + + + + + + Application Insights + KQL queries, traces, dependencies + + + + + Log Analytics + Workspace logs, custom events + + + + + Azure Monitor Metrics + CPU, memory, requests, analysis + + + + + Resource Graph & Activity + Topology, changes, deployment logs + + + + + Built-in Skills & Az CLI + Deep diagnostics, connectivity, + resource-specific checks, remediation + + + + + + + + + + + + Correlated + diagnosis + Evidence from all sources + + + No connectors needed — works through managed identity and Azure RBAC + diff --git a/monitor/sre-agent-event-lab/assets/notifications/github-issue.png b/docs/guides/azure-monitor/azure-sre-agent-overview/images/github-issue.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/notifications/github-issue.png rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/github-issue.png diff --git a/monitor/sre-agent-event-lab/assets/official/incident-platform-flow.svg b/docs/guides/azure-monitor/azure-sre-agent-overview/images/incident-platform-flow.svg similarity index 97% rename from monitor/sre-agent-event-lab/assets/official/incident-platform-flow.svg rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/incident-platform-flow.svg index 022541a4..d2960e1f 100644 --- a/monitor/sre-agent-event-lab/assets/official/incident-platform-flow.svg +++ b/docs/guides/azure-monitor/azure-sre-agent-overview/images/incident-platform-flow.svg @@ -1,59 +1,59 @@ - - - - - - - - - - - - - - - Incident Platform - PagerDuty - Azure Monitor - - - - Alert - - - - Response - Plan - Filter + Mode - - - - Match - - - - Agent - Investigates - Automatically - - - - Findings - - - - Take Action - Mitigate issue - Update incident - - - - - - - Track - Metrics - - - Connect your incident platform → Agent responds automatically → Track effectiveness - + + + + + + + + + + + + + + + Incident Platform + PagerDuty + Azure Monitor + + + + Alert + + + + Response + Plan + Filter + Mode + + + + Match + + + + Agent + Investigates + Automatically + + + + Findings + + + + Take Action + Mitigate issue + Update incident + + + + + + + Track + Metrics + + + Connect your incident platform → Agent responds automatically → Track effectiveness + diff --git a/monitor/sre-agent-event-lab/assets/official/incident-response-flow.svg b/docs/guides/azure-monitor/azure-sre-agent-overview/images/incident-response-flow.svg similarity index 97% rename from monitor/sre-agent-event-lab/assets/official/incident-response-flow.svg rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/incident-response-flow.svg index b6102ccc..9d75e04f 100644 --- a/monitor/sre-agent-event-lab/assets/official/incident-response-flow.svg +++ b/docs/guides/azure-monitor/azure-sre-agent-overview/images/incident-response-flow.svg @@ -1,80 +1,80 @@ - - - - - - - - - - - - - - From alert to resolution in minutes, not hours - - - - Alert Fires - PagerDuty - Azure Monitor - ServiceNow - - - - 30s - - - - Agent - Acknowledges - Notifies team - Starts investigation - - - - 1 min - - - - Investigates - Queries logs - Checks metrics - Correlates deployments - - - - 2 min - - - - Root Cause - Found - Proposes fix - or acts autonomously - - - - - - - Resolved - Ticket updated - Team notified - - - - - - - - - - 0:00 - 0:30 - 1:30 - 3:00 - 5:00 - - Minutes elapsed - + + + + + + + + + + + + + + From alert to resolution in minutes, not hours + + + + Alert Fires + PagerDuty + Azure Monitor + ServiceNow + + + + 30s + + + + Agent + Acknowledges + Notifies team + Starts investigation + + + + 1 min + + + + Investigates + Queries logs + Checks metrics + Correlates deployments + + + + 2 min + + + + Root Cause + Found + Proposes fix + or acts autonomously + + + + + + + Resolved + Ticket updated + Team notified + + + + + + + + + + 0:00 + 0:30 + 1:30 + 3:00 + 5:00 + + Minutes elapsed + diff --git a/monitor/sre-agent-event-lab/assets/official/knowledge-sources.svg b/docs/guides/azure-monitor/azure-sre-agent-overview/images/knowledge-sources.svg similarity index 98% rename from monitor/sre-agent-event-lab/assets/official/knowledge-sources.svg rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/knowledge-sources.svg index a9d35f47..ef2bb279 100644 --- a/monitor/sre-agent-event-lab/assets/official/knowledge-sources.svg +++ b/docs/guides/azure-monitor/azure-sre-agent-overview/images/knowledge-sources.svg @@ -1,48 +1,48 @@ - - - - - - - - - - - - - - Knowledge Base Sources - - - - Upload Documents - Builder → Knowledge base - - 📄 Runbooks (.md, .txt) - 📄 Architecture guides - 📄 On-call playbooks - 📄 API documentation - 📄 Team procedures - - - - MCP Connectors - Settings → Connectors - - 🔗 Azure DevOps wikis - 🔗 GitHub repos and wikis - 🔗 Microsoft Learn docs - 🔗 Confluence pages - 🔗 Custom MCP servers - - - - - - - Agent's - Knowledge Base - Searched via - SearchMemory - + citations - + + + + + + + + + + + + + + Knowledge Base Sources + + + + Upload Documents + Builder → Knowledge base + + 📄 Runbooks (.md, .txt) + 📄 Architecture guides + 📄 On-call playbooks + 📄 API documentation + 📄 Team procedures + + + + MCP Connectors + Settings → Connectors + + 🔗 Azure DevOps wikis + 🔗 GitHub repos and wikis + 🔗 Microsoft Learn docs + 🔗 Confluence pages + 🔗 Custom MCP servers + + + + + + + Agent's + Knowledge Base + Searched via + SearchMemory + + citations + diff --git a/monitor/sre-agent-event-lab/assets/official/managed-connectors-icon-grid.png b/docs/guides/azure-monitor/azure-sre-agent-overview/images/managed-connectors-icon-grid.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/official/managed-connectors-icon-grid.png rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/managed-connectors-icon-grid.png diff --git a/monitor/sre-agent-event-lab/assets/official/memory-auto-learning.svg b/docs/guides/azure-monitor/azure-sre-agent-overview/images/memory-auto-learning.svg similarity index 97% rename from monitor/sre-agent-event-lab/assets/official/memory-auto-learning.svg rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/memory-auto-learning.svg index 39980ddb..c8e1821f 100644 --- a/monitor/sre-agent-event-lab/assets/official/memory-auto-learning.svg +++ b/docs/guides/azure-monitor/azure-sre-agent-overview/images/memory-auto-learning.svg @@ -1,54 +1,54 @@ - - - - - - - - - - - - - - Your agent learns from every session - - - - Thread - Completes - Sync or Async - - - - 30 min - - - - Session - Evaluator - Auto-extracts learnings - - - - - - - Learns: - • Symptoms observed - • Steps that worked - • Root cause found - • Pitfalls to avoid - - - - - - - Indexed - Memory - For future use - - - Next time a similar incident occurs, your agent recalls what worked - + + + + + + + + + + + + + + Your agent learns from every session + + + + Thread + Completes + Sync or Async + + + + 30 min + + + + Session + Evaluator + Auto-extracts learnings + + + + + + + Learns: + • Symptoms observed + • Steps that worked + • Root cause found + • Pitfalls to avoid + + + + + + + Indexed + Memory + For future use + + + Next time a similar incident occurs, your agent recalls what worked + diff --git a/monitor/sre-agent-event-lab/assets/official/memory-unified-search.svg b/docs/guides/azure-monitor/azure-sre-agent-overview/images/memory-unified-search.svg similarity index 98% rename from monitor/sre-agent-event-lab/assets/official/memory-unified-search.svg rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/memory-unified-search.svg index ec9d4c2f..c370461b 100644 --- a/monitor/sre-agent-event-lab/assets/official/memory-unified-search.svg +++ b/docs/guides/azure-monitor/azure-sre-agent-overview/images/memory-unified-search.svg @@ -1,72 +1,72 @@ - - - - - - - - - - - - - - - - Sync Chat - Your questions - - - - Async - Auto-triggered - - - - - - - - SearchMemory - Searches all sources - simultaneously - - - - - - Past Sessions - Symptoms, Root Cause - Steps, Pitfalls - Auto-learned - - - - - User Memories - #remember facts - Explicit saves - - - - - Knowledge Base - Runbooks, Docs - Architecture guides - Uploaded + Connectors - - - - - - - - - Grounded - Response - With clickable citations - - - - Learnings saved back to memory (30 min after thread ends) - + + + + + + + + + + + + + + + + Sync Chat + Your questions + + + + Async + Auto-triggered + + + + + + + + SearchMemory + Searches all sources + simultaneously + + + + + + Past Sessions + Symptoms, Root Cause + Steps, Pitfalls + Auto-learned + + + + + User Memories + #remember facts + Explicit saves + + + + + Knowledge Base + Runbooks, Docs + Architecture guides + Uploaded + Connectors + + + + + + + + + Grounded + Response + With clickable citations + + + + Learnings saved back to memory (30 min after thread ends) + diff --git a/monitor/sre-agent-event-lab/assets/official/notification-paths.svg b/docs/guides/azure-monitor/azure-sre-agent-overview/images/notification-paths.svg similarity index 98% rename from monitor/sre-agent-event-lab/assets/official/notification-paths.svg rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/notification-paths.svg index 188c09bc..45a7ed07 100644 --- a/monitor/sre-agent-event-lab/assets/official/notification-paths.svg +++ b/docs/guides/azure-monitor/azure-sre-agent-overview/images/notification-paths.svg @@ -1,68 +1,68 @@ - - - - - - - - - - - - - - - - - - - - Agent - investigates - and recommends - - - - - - Response paths - - - - Execute fix - Scale, restart, rollback - - - - Create work item - ADO, Jira, GitHub - - - - Send notification - Teams, Outlook, Slack - - - - - - - - - Azure resources - Container Apps, VMs, AKS - - - Tracking systems - With context + evidence - - - Team channels - Formatted summary - - - - - - - + + + + + + + + + + + + + + + + + + + + Agent + investigates + and recommends + + + + + + Response paths + + + + Execute fix + Scale, restart, rollback + + + + Create work item + ADO, Jira, GitHub + + + + Send notification + Teams, Outlook, Slack + + + + + + + + + Azure resources + Container Apps, VMs, AKS + + + Tracking systems + With context + evidence + + + Team channels + Formatted summary + + + + + + + diff --git a/monitor/sre-agent-event-lab/assets/official/operations-hub-overview-tab.png b/docs/guides/azure-monitor/azure-sre-agent-overview/images/operations-hub-overview-tab.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/official/operations-hub-overview-tab.png rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/operations-hub-overview-tab.png diff --git a/monitor/sre-agent-event-lab/assets/official/permission-flow.svg b/docs/guides/azure-monitor/azure-sre-agent-overview/images/permission-flow.svg similarity index 98% rename from monitor/sre-agent-event-lab/assets/official/permission-flow.svg rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/permission-flow.svg index f030d35a..f3ef5db3 100644 --- a/monitor/sre-agent-event-lab/assets/official/permission-flow.svg +++ b/docs/guides/azure-monitor/azure-sre-agent-overview/images/permission-flow.svg @@ -1,61 +1,61 @@ - - - - - - - - - - - - - - - - - - Agent needs to - perform action - - - - - - - Managed identity - has RBAC role? - - - - YES - - - Use managed - identity - - - - - - - NO - - - Request user - permissions (OBO) - - - - - - - Action - performed - - Restart service - Scale resources - Query logs - Modify config - Read metrics - + + + + + + + + + + + + + + + + + + Agent needs to + perform action + + + + + + + Managed identity + has RBAC role? + + + + YES + + + Use managed + identity + + + + + + + NO + + + Request user + permissions (OBO) + + + + + + + Action + performed + + Restart service + Scale resources + Query logs + Modify config + Read metrics + diff --git a/monitor/sre-agent-event-lab/assets/official/portal-sub-agent-canvas-full.png b/docs/guides/azure-monitor/azure-sre-agent-overview/images/portal-sub-agent-canvas-full.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/official/portal-sub-agent-canvas-full.png rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/portal-sub-agent-canvas-full.png diff --git a/monitor/sre-agent-event-lab/assets/official/root-cause-analysis.svg b/docs/guides/azure-monitor/azure-sre-agent-overview/images/root-cause-analysis.svg similarity index 97% rename from monitor/sre-agent-event-lab/assets/official/root-cause-analysis.svg rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/root-cause-analysis.svg index f1975f41..fe24fa57 100644 --- a/monitor/sre-agent-event-lab/assets/official/root-cause-analysis.svg +++ b/docs/guides/azure-monitor/azure-sre-agent-overview/images/root-cause-analysis.svg @@ -1,71 +1,71 @@ - - - - - - - - - - - - - - - Symptoms - errors, alerts - - - - - - Gather context - logs, metrics, deploys - - - - - - Form hypotheses - test & validate - - - - - - Root cause - with evidence - - - - - - Fix - resolve - - - - App Insights - - - Azure Monitor - - - Deployments - - - Source code - - - Memory - - - - - - - - - - Agent queries multiple data sources and correlates evidence to identify root cause - + + + + + + + + + + + + + + + Symptoms + errors, alerts + + + + + + Gather context + logs, metrics, deploys + + + + + + Form hypotheses + test & validate + + + + + + Root cause + with evidence + + + + + + Fix + resolve + + + + App Insights + + + Azure Monitor + + + Deployments + + + Source code + + + Memory + + + + + + + + + + Agent queries multiple data sources and correlates evidence to identify root cause + diff --git a/monitor/sre-agent-event-lab/assets/official/run-modes-comparison.svg b/docs/guides/azure-monitor/azure-sre-agent-overview/images/run-modes-comparison.svg similarity index 97% rename from monitor/sre-agent-event-lab/assets/official/run-modes-comparison.svg rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/run-modes-comparison.svg index 98f89736..680042a8 100644 --- a/monitor/sre-agent-event-lab/assets/official/run-modes-comparison.svg +++ b/docs/guides/azure-monitor/azure-sre-agent-overview/images/run-modes-comparison.svg @@ -1,57 +1,57 @@ - - - - - - - - - - - - - - Review Mode - - - - Agent - - - - - Proposes - action - - - - - User - - - Autonomous Mode - - - - Agent - - - - - Executes - immediately - - - - - Done - - - - User approves or denies - before agent acts - - - Agent completes task - independently - + + + + + + + + + + + + + + Review Mode + + + + Agent + + + + + Proposes + action + + + + + User + + + Autonomous Mode + + + + Agent + + + + + Executes + immediately + + + + + Done + + + + User approves or denies + before agent acts + + + Agent completes task + independently + diff --git a/monitor/sre-agent-event-lab/assets/briefing/s1-agent-conclusion.png b/docs/guides/azure-monitor/azure-sre-agent-overview/images/s1-agent-conclusion.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/briefing/s1-agent-conclusion.png rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/s1-agent-conclusion.png diff --git a/monitor/sre-agent-event-lab/assets/notifications/s1-email-preview.png b/docs/guides/azure-monitor/azure-sre-agent-overview/images/s1-email-preview.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/notifications/s1-email-preview.png rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/s1-email-preview.png diff --git a/monitor/sre-agent-event-lab/assets/briefing/s1-three-panel.png b/docs/guides/azure-monitor/azure-sre-agent-overview/images/s1-three-panel.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/briefing/s1-three-panel.png rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/s1-three-panel.png diff --git a/monitor/sre-agent-event-lab/assets/briefing/sre-agent-process.png b/docs/guides/azure-monitor/azure-sre-agent-overview/images/sre-agent-process.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/briefing/sre-agent-process.png rename to docs/guides/azure-monitor/azure-sre-agent-overview/images/sre-agent-process.png diff --git a/monitor/azure-sre-agent.md b/docs/guides/azure-monitor/azure-sre-agent-overview/index.md similarity index 93% rename from monitor/azure-sre-agent.md rename to docs/guides/azure-monitor/azure-sre-agent-overview/index.md index 2d9b6036..3dba48fa 100644 --- a/monitor/azure-sre-agent.md +++ b/docs/guides/azure-monitor/azure-sre-agent-overview/index.md @@ -1,3 +1,27 @@ +--- +services: +- azure-monitor +official_sources: +- title: Azure Monitor documentation + url: https://learn.microsoft.com/azure/azure-monitor/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Azure SRE Agent 개요 +description: Azure SRE Agent의 기능, 구성 요소와 운영 흐름을 설명합니다. +technologies: +- rest-api +tags: +- ai-agents +- monitoring +- troubleshooting +--- + # Azure SRE Agent 소개 Azure SRE Agent는 Azure 운영 환경에서 발생한 인시던트를 자동으로 조사하고, 관련 근거를 바탕으로 근본 원인과 조치 방안을 제안하는 AI 기반 운영 도우미입니다. @@ -20,7 +44,7 @@ Azure SRE Agent는 단순히 오류 로그를 나열하지 않습니다. 어떤 ## 인시던트가 발생하면 어떻게 조사하나요? -![경고 발생부터 해결까지 5분 안에 이어지는 흐름을 시간축으로 보여 주는 다이어그램입니다. 0:00에 PagerDuty와 Azure Monitor, ServiceNow에서 경고가 발생하고, 0:30에 에이전트가 경고를 접수해 팀에 알린 뒤 조사를 시작합니다. 1:30에는 로그 조회와 메트릭 확인, 배포 이력 연관 분석을 수행하고, 3:00에 근본 원인을 찾아 수정 방안을 제안하거나 자율 모드로 직접 조치합니다. 5:00에는 해결 상태가 되어 작업 항목이 갱신되고 팀에 알림이 전달됩니다.](sre-agent-event-lab/assets/official/incident-response-flow.svg) +![경고 발생부터 해결까지 5분 안에 이어지는 흐름을 시간축으로 보여 주는 다이어그램입니다. 0:00에 PagerDuty와 Azure Monitor, ServiceNow에서 경고가 발생하고, 0:30에 에이전트가 경고를 접수해 팀에 알린 뒤 조사를 시작합니다. 1:30에는 로그 조회와 메트릭 확인, 배포 이력 연관 분석을 수행하고, 3:00에 근본 원인을 찾아 수정 방안을 제안하거나 자율 모드로 직접 조치합니다. 5:00에는 해결 상태가 되어 작업 항목이 갱신되고 팀에 알림이 전달됩니다.](images/incident-response-flow.svg) > 출처: [인시던트 대응 자동화](https://learn.microsoft.com/azure/sre-agent/incident-response) @@ -56,7 +80,7 @@ Azure SRE Agent는 다음 순서로 인시던트를 조사합니다. ## 근본 원인은 어떻게 찾나요? -![증상에서 시작해 로그와 메트릭, 배포 이력, 소스 코드, 과거 경험을 근거로 모으고 가설을 세워 검증한 뒤 근본 원인과 조치 방안을 제시하는 흐름](sre-agent-event-lab/assets/official/root-cause-analysis.svg) +![증상에서 시작해 로그와 메트릭, 배포 이력, 소스 코드, 과거 경험을 근거로 모으고 가설을 세워 검증한 뒤 근본 원인과 조치 방안을 제시하는 흐름](images/root-cause-analysis.svg) > 출처: [근본 원인 분석](https://learn.microsoft.com/azure/sre-agent/root-cause-analysis) @@ -73,7 +97,7 @@ Azure SRE Agent는 관리 ID와 Azure RBAC 권한을 사용해 Azure 리소스 | 소스와 문서 | GitHub, Azure DevOps, 운영 절차서, 기술 문서 | | 과거 경험 | 유사한 인시던트, 이전의 근본 원인과 해결 방법 | -![서비스 문제가 발생하면 에이전트가 Application Insights의 요청과 추적, 종속성 정보와 Log Analytics 작업 영역 로그, Azure Monitor 메트릭, Resource Graph의 토폴로지와 변경·배포 기록, 기본 제공 스킬과 Azure CLI 진단을 한 번에 조회한 뒤 근거를 서로 연결해 근본 원인을 설명하는 구조를 보여주는 다이어그램입니다. 별도 연결 설정 없이 관리 ID와 Azure RBAC 권한만으로 동작합니다.](sre-agent-event-lab/assets/official/diagnose-azure-services.svg) +![서비스 문제가 발생하면 에이전트가 Application Insights의 요청과 추적, 종속성 정보와 Log Analytics 작업 영역 로그, Azure Monitor 메트릭, Resource Graph의 토폴로지와 변경·배포 기록, 기본 제공 스킬과 Azure CLI 진단을 한 번에 조회한 뒤 근거를 서로 연결해 근본 원인을 설명하는 구조를 보여주는 다이어그램입니다. 별도 연결 설정 없이 관리 ID와 Azure RBAC 권한만으로 동작합니다.](images/diagnose-azure-services.svg) > 출처: [Azure 관찰 가능성으로 진단하기](https://learn.microsoft.com/azure/sre-agent/diagnose-azure-observability) @@ -81,17 +105,17 @@ Azure 내부 원격 분석 데이터는 기본 도구만으로도 조회할 수 ## 과거 경험과 운영 문서는 어떻게 활용하나요? -![과거 조사 기록과 사용자가 지정한 내용, 업로드한 운영 문서를 함께 검색해 근거와 출처가 있는 답변을 만드는 구조](sre-agent-event-lab/assets/official/memory-unified-search.svg) +![과거 조사 기록과 사용자가 지정한 내용, 업로드한 운영 문서를 함께 검색해 근거와 출처가 있는 답변을 만드는 구조](images/memory-unified-search.svg) > 출처: [메모리와 지식 관리](https://learn.microsoft.com/azure/sre-agent/memory) -![지식 기반 원본을 두 갈래로 정리한 다이어그램입니다. 왼쪽에는 Builder 화면에서 업로드한 운영 절차서와 아키텍처 안내서, 대기 근무 지침, API 문서, 팀 작업 절차가 있고, 오른쪽에는 설정 화면에서 연결한 MCP 커넥터로 Azure DevOps 위키, GitHub 저장소와 위키, Microsoft Learn 문서, Confluence 페이지, 사용자 지정 MCP 서버가 있습니다. 두 갈래가 하나의 지식 기반으로 모여 검색되고 출처가 함께 제시됩니다.](sre-agent-event-lab/assets/official/knowledge-sources.svg) +![지식 기반 원본을 두 갈래로 정리한 다이어그램입니다. 왼쪽에는 Builder 화면에서 업로드한 운영 절차서와 아키텍처 안내서, 대기 근무 지침, API 문서, 팀 작업 절차가 있고, 오른쪽에는 설정 화면에서 연결한 MCP 커넥터로 Azure DevOps 위키, GitHub 저장소와 위키, Microsoft Learn 문서, Confluence 페이지, 사용자 지정 MCP 서버가 있습니다. 두 갈래가 하나의 지식 기반으로 모여 검색되고 출처가 함께 제시됩니다.](images/knowledge-sources.svg) > 출처: [메모리와 지식 관리](https://learn.microsoft.com/azure/sre-agent/memory) ## 조사가 끝난 뒤 무엇을 학습하나요? -![조사가 끝나면 증상과 해결 단계, 근본 원인, 피해야 할 접근을 추출해 다음 조사에 사용할 수 있도록 학습하는 흐름](sre-agent-event-lab/assets/official/memory-auto-learning.svg) +![조사가 끝나면 증상과 해결 단계, 근본 원인, 피해야 할 접근을 추출해 다음 조사에 사용할 수 있도록 학습하는 흐름](images/memory-auto-learning.svg) > 출처: [메모리와 지식 관리](https://learn.microsoft.com/azure/sre-agent/memory) @@ -111,7 +135,7 @@ Azure 내부 원격 분석 데이터는 기본 도구만으로도 조회할 수 관리형 커넥터로 연결할 수 있는 대표적인 서비스는 다음과 같습니다. -![New Managed Connectors 화면입니다. Jira, Slack, GitLab, Notion, Asana, Box, Confluence, OneDrive, SharePoint, GitHub, Azure DevOps, Office 365 Users, Viva Engage, Security Copilot, Freshdesk 15개 서비스가 아이콘 격자로 배열되어 있고, 각 항목 아래에 미리 보기 표시가 붙어 있습니다.](sre-agent-event-lab/assets/official/managed-connectors-icon-grid.png) +![New Managed Connectors 화면입니다. Jira, Slack, GitLab, Notion, Asana, Box, Confluence, OneDrive, SharePoint, GitHub, Azure DevOps, Office 365 Users, Viva Engage, Security Copilot, Freshdesk 15개 서비스가 아이콘 격자로 배열되어 있고, 각 항목 아래에 미리 보기 표시가 붙어 있습니다.](images/managed-connectors-icon-grid.png) > 출처: [관리형 커넥터(미리 보기)](https://learn.microsoft.com/azure/sre-agent/managed-connectors) @@ -121,7 +145,7 @@ Azure 내부 원격 분석 데이터는 기본 도구만으로도 조회할 수 - Microsoft Teams 채널 - Slack 채널 -![에이전트가 조사와 권고를 마친 뒤 이어지는 세 가지 대응 경로를 보여주는 다이어그램입니다. 첫째는 크기 조정과 다시 시작, 롤백 같은 조치를 Container Apps, 가상 머신, AKS 등 Azure 리소스에 직접 실행하는 경로이고, 둘째는 Azure DevOps, Jira, GitHub에 상황과 근거를 담은 작업 항목을 만드는 경로이며, 셋째는 Teams, Outlook, Slack 채널에 정리된 요약을 보내는 알림 경로입니다.](sre-agent-event-lab/assets/official/notification-paths.svg) +![에이전트가 조사와 권고를 마친 뒤 이어지는 세 가지 대응 경로를 보여주는 다이어그램입니다. 첫째는 크기 조정과 다시 시작, 롤백 같은 조치를 Container Apps, 가상 머신, AKS 등 Azure 리소스에 직접 실행하는 경로이고, 둘째는 Azure DevOps, Jira, GitHub에 상황과 근거를 담은 작업 항목을 만드는 경로이며, 셋째는 Teams, Outlook, Slack 채널에 정리된 요약을 보내는 알림 경로입니다.](images/notification-paths.svg) > 출처: [조치 실행하기](https://learn.microsoft.com/azure/sre-agent/execute-mitigations) @@ -138,7 +162,7 @@ Azure 내부 원격 분석 데이터는 기본 도구만으로도 조회할 수 ## 권한과 승인 절차는 어떻게 제어하나요? -![요청의 의미를 파악하고 맥락 정보를 모아 추론한 뒤 안전한 작업은 실행하고 위험한 작업은 승인을 기다리며 결과를 응답하는 흐름](sre-agent-event-lab/assets/official/agent-reasoning-flow.svg) +![요청의 의미를 파악하고 맥락 정보를 모아 추론한 뒤 안전한 작업은 실행하고 위험한 작업은 승인을 기다리며 결과를 응답하는 흐름](images/agent-reasoning-flow.svg) > 출처: [에이전트 추론과 실행](https://learn.microsoft.com/azure/sre-agent/agent-reasoning) @@ -154,7 +178,7 @@ Azure SRE Agent는 실행 수준에 따라 권한을 제어합니다. 공식 기능인 검토 모드(Review)와 자율 모드(Autonomous)의 차이는 다음 그림에서도 확인할 수 있습니다. -![검토 모드와 자율 모드를 위아래로 나누어 비교하는 다이어그램입니다. 위쪽 검토 모드에서는 에이전트가 조치를 제안하고 사용자가 승인하거나 거부한 뒤에야 실행되며, 아래쪽 자율 모드에서는 에이전트가 곧바로 실행해 작업을 스스로 완료합니다.](sre-agent-event-lab/assets/official/run-modes-comparison.svg) +![검토 모드와 자율 모드를 위아래로 나누어 비교하는 다이어그램입니다. 위쪽 검토 모드에서는 에이전트가 조치를 제안하고 사용자가 승인하거나 거부한 뒤에야 실행되며, 아래쪽 자율 모드에서는 에이전트가 곧바로 실행해 작업을 스스로 완료합니다.](images/run-modes-comparison.svg) > 출처: [실행 모드](https://learn.microsoft.com/azure/sre-agent/run-modes) @@ -170,13 +194,13 @@ Azure SRE Agent는 실행 수준에 따라 권한을 제어합니다. 관리 ID 권한이 부족할 때 담당자 승인으로 넘어가는 세부 과정은 다음 그림과 같습니다. -![에이전트가 작업을 수행하기 전에 관리 ID에 필요한 Azure RBAC 역할이 있는지 확인하는 판단 흐름 다이어그램입니다. 역할이 있으면 관리 ID로 그대로 실행하고, 없으면 담당자 승인을 거쳐 사용자 위임 자격 증명을 요청한 뒤 서비스 다시 시작, 리소스 크기 조정, 로그 조회, 구성 변경, 메트릭 읽기 같은 작업을 수행합니다.](sre-agent-event-lab/assets/official/permission-flow.svg) +![에이전트가 작업을 수행하기 전에 관리 ID에 필요한 Azure RBAC 역할이 있는지 확인하는 판단 흐름 다이어그램입니다. 역할이 있으면 관리 ID로 그대로 실행하고, 없으면 담당자 승인을 거쳐 사용자 위임 자격 증명을 요청한 뒤 서비스 다시 시작, 리소스 크기 조정, 로그 조회, 구성 변경, 메트릭 읽기 같은 작업을 수행합니다.](images/permission-flow.svg) > 출처: [권한](https://learn.microsoft.com/azure/sre-agent/permissions) ## 팀에 맞게 어떻게 확장하나요? -![에이전트가 사용자 지정 스킬을 불러오고 스킬에 연결된 도구로 외부 API를 호출한 뒤 결과를 받아 조사에 활용하는 흐름](sre-agent-event-lab/assets/official/custom-skill-flow.svg) +![에이전트가 사용자 지정 스킬을 불러오고 스킬에 연결된 도구로 외부 API를 호출한 뒤 결과를 받아 조사에 활용하는 흐름](images/custom-skill-flow.svg) > 출처: [스킬](https://learn.microsoft.com/azure/sre-agent/skills) @@ -191,7 +215,7 @@ Azure SRE Agent는 실행 수준에 따라 권한을 제어합니다. 사용자 지정 에이전트는 각자 도구와 커넥터, 사용할 스킬을 따로 지정합니다. 조사 과정에서 다른 전문 에이전트로 작업을 넘기도록 구성할 수도 있어, 인시던트 분류와 상세 조사, 결과 전달을 단계별로 나눌 수 있습니다. -![포털의 캔버스 보기 화면입니다. 왼쪽에는 켜져 있는 PDtrigger 트리거가 있고, 가운데에는 자율 모드로 동작하며 지식 기반이 활성화된 AppErrorshandler 사용자 지정 에이전트가 연결되어 있으며, 오른쪽에는 GetAzCliHelp, RunAzCliReadCommands, RunAzCliWriteCommands, QueryAppInsightsByAppId, QueryAppInsightsByResourceId를 비롯한 도구 10개가 연결되어 있습니다.](sre-agent-event-lab/assets/official/portal-sub-agent-canvas-full.png) +![포털의 캔버스 보기 화면입니다. 왼쪽에는 켜져 있는 PDtrigger 트리거가 있고, 가운데에는 자율 모드로 동작하며 지식 기반이 활성화된 AppErrorshandler 사용자 지정 에이전트가 연결되어 있으며, 오른쪽에는 GetAzCliHelp, RunAzCliReadCommands, RunAzCliWriteCommands, QueryAppInsightsByAppId, QueryAppInsightsByResourceId를 비롯한 도구 10개가 연결되어 있습니다.](images/portal-sub-agent-canvas-full.png) > 출처: [사용자 지정 에이전트](https://learn.microsoft.com/azure/sre-agent/sub-agents) @@ -208,7 +232,7 @@ Azure SRE Agent는 실행 수준에 따라 권한을 제어합니다. 인시던트 플랫폼을 처음 연결하면 빠른 시작 대응 계획이 자동으로 만들어집니다. 사용자 정의 대응 계획을 만든 뒤에는 이 계획을 삭제해야 인시던트가 잘못 전달되거나 두 번 처리되지 않습니다. -![인시던트 플랫폼에서 시작해 조치와 효과 측정까지 이어지는 전체 흐름을 보여주는 다이어그램입니다. PagerDuty나 Azure Monitor 같은 플랫폼이 경고를 보내면 필터와 실행 모드를 지정한 대응 계획이 조건에 맞는 항목을 골라 에이전트에게 넘기고, 에이전트가 자동으로 조사해 얻은 결과로 문제를 완화하고 원래 플랫폼의 상태를 갱신한 뒤 처리 결과를 지표로 추적합니다.](sre-agent-event-lab/assets/official/incident-platform-flow.svg) +![인시던트 플랫폼에서 시작해 조치와 효과 측정까지 이어지는 전체 흐름을 보여주는 다이어그램입니다. PagerDuty나 Azure Monitor 같은 플랫폼이 경고를 보내면 필터와 실행 모드를 지정한 대응 계획이 조건에 맞는 항목을 골라 에이전트에게 넘기고, 에이전트가 자동으로 조사해 얻은 결과로 문제를 완화하고 원래 플랫폼의 상태를 갱신한 뒤 처리 결과를 지표로 추적합니다.](images/incident-platform-flow.svg) > 출처: [인시던트 플랫폼](https://learn.microsoft.com/azure/sre-agent/incident-platforms) @@ -236,9 +260,9 @@ Azure SRE Agent는 다음 기능을 제품에서 기본으로 지원합니다. ## 이번 실증에서 사용한 방식 -![Azure SRE Agent의 인시던트 대응 흐름](sre-agent-event-lab/assets/briefing/sre-agent-process.png) +![Azure SRE Agent의 인시던트 대응 흐름](images/sre-agent-process.png) -[편집 가능한 SVG 보기](sre-agent-event-lab/assets/briefing/sre-agent-process.svg) +[편집 가능한 SVG 보기](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/sre-agent-event-lab/assets/briefing/sre-agent-process.svg) 이번 실증에서는 대응 계획을 공개 API로 자동 구성하는 데 제약이 있어 Azure SRE Agent의 HTTP Trigger를 사용했습니다. @@ -275,9 +299,9 @@ Azure Monitor 경고 배포 설정 오류로 주문 API가 HTTP 500을 반환하는 상황을 만들고, Azure SRE Agent가 어떤 근거를 확인하는지 검증했습니다. -![주문 API HTTP 500 실증 요약](sre-agent-event-lab/assets/briefing/s1-three-panel.png) +![주문 API HTTP 500 실증 요약](images/s1-three-panel.png) -[편집 가능한 SVG 보기](sre-agent-event-lab/assets/briefing/s1-three-panel.svg) +[편집 가능한 SVG 보기](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/sre-agent-event-lab/assets/briefing/s1-three-panel.svg) ### 상황 @@ -297,7 +321,7 @@ Azure Monitor 경고 ### 실제 확인 결과 -![Azure SRE Agent가 확인한 결과](sre-agent-event-lab/assets/briefing/s1-agent-conclusion.png) +![Azure SRE Agent가 확인한 결과](images/s1-agent-conclusion.png) - Azure Monitor 경고가 발생한 뒤 2초 안에 조사 대화가 생성됐습니다. - Application Insights에서 실패한 요청 120건을 확인했습니다. @@ -309,10 +333,10 @@ Azure Monitor 경고 Azure SRE Agent가 정리한 근본 원인과 조치 방안을 작업 항목으로 전달할 수 있습니다. 이번 실증에서는 같은 조사 결과로 실제 GitHub Issue를 만들었습니다. -![실제 GitHub Issue #43](sre-agent-event-lab/assets/notifications/github-issue.png) +![실제 GitHub Issue #43](images/github-issue.png) - [GitHub Issue #43 열기](https://github.com/hellices/devguidesample/issues/43) -- [Issue 본문 보기](sre-agent-event-lab/assets/notifications/s1-github-issue.md) +- [Issue 본문 보기](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/s1-github-issue.md) Issue에는 다음 내용을 포함했습니다. @@ -330,10 +354,10 @@ Issue에는 다음 내용을 포함했습니다. 이번 실증에서는 외부 수신자에게 메일을 보내지 않았습니다. 대신 Azure SRE Agent의 조사 결과로 Outlook에서 열 수 있는 메일 초안과 화면 미리보기를 만들었습니다. -![Outlook 메일 초안](sre-agent-event-lab/assets/notifications/s1-email-preview.png) +![Outlook 메일 초안](images/s1-email-preview.png) -- [HTML 메일 보기](sre-agent-event-lab/assets/notifications/s1-incident-summary.html) -- [RFC 5322 메일 파일 보기](sre-agent-event-lab/assets/notifications/s1-incident-summary.eml) +- [HTML 메일 보기](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/s1-incident-summary.html) +- [RFC 5322 메일 파일 보기](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/s1-incident-summary.eml) 실제 운영에서는 Outlook 커넥터의 메일 보내기 작업을 사용할 수 있습니다. @@ -352,7 +376,7 @@ Issue에는 다음 내용을 포함했습니다. | 주문 API 지연 | 성공한 요청의 응답 시간, 외부 종속성, 지연 설정 | `ORDER_DELAY_MS=4000`을 확인했습니다. | | Blob 권한 오류 | 역할 삭제 이력, Blob 403, API 503 | 역할 삭제를 근본 원인으로 확인했습니다. 복구 확인 대상은 한 차례 잘못 선택해 한계로 기록했습니다. | -상세 수치, 시간 순서, 평가 기준은 [실제 동작 검증 결과](sre-agent-event-lab/validation-results.md)에서 확인할 수 있습니다. +상세 수치, 시간 순서, 평가 기준은 [실제 동작 검증 결과](../../../research/azure-monitor/sre-agent-validation-results/index.md)에서 확인할 수 있습니다. ## Dynamic Thresholds와 함께 사용할 수 있나요? @@ -365,7 +389,7 @@ Issue에는 다음 내용을 포함했습니다. 처음에는 기존 고정 임계값을 유지하고, Dynamic Thresholds를 별도의 관찰용 경고로 추가하는 방식을 권장합니다. 충분한 학습 기간을 거친 뒤 오탐과 누락을 비교해 실제 대응 흐름에 연결합니다. -자세한 내용은 [Dynamic Thresholds 연계 가이드](sre-agent-event-lab/dynamic-thresholds.md)를 참고하세요. +자세한 내용은 [Dynamic Thresholds 연계 가이드](../sre-agent-dynamic-thresholds/index.md)를 참고하세요. ## 도입 전에 확인해야 할 사전 조건 @@ -414,7 +438,7 @@ Log Analytics 작업 영역이나 AKS를 비공개 네트워크로만 노출한 ## 비공개 네트워크에는 어떻게 연결하나요? -![작업 영역 구성 화면의 네트워킹 탭을 보여 주는 화면입니다. 제목 옆에는 미리 보기 배지가 있고, 송신 모드로 Unrestricted, Limited, Azure VNet 세 가지 카드가 나란히 있으며 그중 Azure VNet이 선택되어 연결됨 배지가 붙어 있고 나머지 두 카드는 흐리게 비활성 상태로 표시됩니다. 아래에는 연결한 위임 서브넷 경로와 함께 서브넷이 /28 이상이어야 하고 Microsoft.App/environments에 위임되어야 한다는 안내, 자동으로 켜진 VNet 프라이빗 DNS 사용 옵션, 1분 전에 확인을 마쳤다고 표시된 연결 테스트 단추와 VNet 연결 해제 단추가 있습니다. 그 아래 인프라 네트워크 영역에는 이 경로가 VNet을 우회하므로 NSG 규칙과 프라이빗 DNS가 적용되지 않는다는 경고와 함께 켜져 있는 MCP 서버 접근 토글과 패키지 관리자 접근 토글, 아직 선택하지 않은 GitHub와 GitHub Enterprise, Azure DevOps 코드 저장소 선택란, 와일드카드까지 입력할 수 있는 추가 호스트 입력란이 있습니다.](sre-agent-event-lab/assets/official/azure-sre-agent-networking-vnet.png) +![작업 영역 구성 화면의 네트워킹 탭을 보여 주는 화면입니다. 제목 옆에는 미리 보기 배지가 있고, 송신 모드로 Unrestricted, Limited, Azure VNet 세 가지 카드가 나란히 있으며 그중 Azure VNet이 선택되어 연결됨 배지가 붙어 있고 나머지 두 카드는 흐리게 비활성 상태로 표시됩니다. 아래에는 연결한 위임 서브넷 경로와 함께 서브넷이 /28 이상이어야 하고 Microsoft.App/environments에 위임되어야 한다는 안내, 자동으로 켜진 VNet 프라이빗 DNS 사용 옵션, 1분 전에 확인을 마쳤다고 표시된 연결 테스트 단추와 VNet 연결 해제 단추가 있습니다. 그 아래 인프라 네트워크 영역에는 이 경로가 VNet을 우회하므로 NSG 규칙과 프라이빗 DNS가 적용되지 않는다는 경고와 함께 켜져 있는 MCP 서버 접근 토글과 패키지 관리자 접근 토글, 아직 선택하지 않은 GitHub와 GitHub Enterprise, Azure DevOps 코드 저장소 선택란, 와일드카드까지 입력할 수 있는 추가 호스트 입력란이 있습니다.](images/azure-sre-agent-networking-vnet.png) > 출처: [네트워크 통합](https://learn.microsoft.com/azure/sre-agent/network-integration) @@ -558,7 +582,7 @@ Agent Hooks를 사용하면 에이전트가 결과를 반환하기 직전이나 이 지표는 왼쪽 사이드바의 Operations Hub 개요 탭에서 한 화면으로 확인할 수 있습니다. -![Operations Hub 개요 탭 화면입니다. 왼쪽 사이드바에서는 Operations Hub 항목이 선택되어 강조되어 있고 그 아래로 인시던트, 예약 작업, Builder, 모니터, 기능, 설정 메뉴가 이어집니다. 오른쪽 본문에는 개요와 인시던트 분석, 자동화 탭이 있고 기간은 최근 7일로 지정되어 있으며, 원본별 일일 처리량을 선택한 차트 영역에는 값이 그려지지 않고 선택한 기간에 표시할 데이터가 없다는 안내만 나타납니다. 그 아래에는 대기 중인 작업 41건 카드와 실시간 시스템 상태 카드가 놓여 주요 지표를 한 화면에서 확인할 수 있습니다.](sre-agent-event-lab/assets/official/operations-hub-overview-tab.png) +![Operations Hub 개요 탭 화면입니다. 왼쪽 사이드바에서는 Operations Hub 항목이 선택되어 강조되어 있고 그 아래로 인시던트, 예약 작업, Builder, 모니터, 기능, 설정 메뉴가 이어집니다. 오른쪽 본문에는 개요와 인시던트 분석, 자동화 탭이 있고 기간은 최근 7일로 지정되어 있으며, 원본별 일일 처리량을 선택한 차트 영역에는 값이 그려지지 않고 선택한 기간에 표시할 데이터가 없다는 안내만 나타납니다. 그 아래에는 대기 중인 작업 41건 카드와 실시간 시스템 상태 카드가 놓여 주요 지표를 한 화면에서 확인할 수 있습니다.](images/operations-hub-overview-tab.png) > 출처: [Operations Hub](https://learn.microsoft.com/azure/sre-agent/operations-hub) @@ -647,5 +671,5 @@ Agent Hooks를 사용하면 에이전트가 결과를 반환하기 직전이나 ### 실증 자료 -- [실제 동작 검증 결과](sre-agent-event-lab/validation-results.md) -- [실험 환경과 재현 방법](sre-agent-event-lab/README.md) +- [실제 동작 검증 결과](../../../research/azure-monitor/sre-agent-validation-results/index.md) +- [실험 환경과 재현 방법](../../../labs/azure-monitor/sre-agent-event-lab/index.md) diff --git a/monitor/assets/images/kafka-exporter-adv.png b/docs/guides/azure-monitor/hdinsight-kafka-prometheus-grafana/images/kafka-exporter-adv.png similarity index 100% rename from monitor/assets/images/kafka-exporter-adv.png rename to docs/guides/azure-monitor/hdinsight-kafka-prometheus-grafana/images/kafka-exporter-adv.png diff --git a/monitor/assets/images/kafka-exporter-overview.png b/docs/guides/azure-monitor/hdinsight-kafka-prometheus-grafana/images/kafka-exporter-overview.png similarity index 100% rename from monitor/assets/images/kafka-exporter-overview.png rename to docs/guides/azure-monitor/hdinsight-kafka-prometheus-grafana/images/kafka-exporter-overview.png diff --git a/monitor/assets/images/kafka-jmx-broker.png b/docs/guides/azure-monitor/hdinsight-kafka-prometheus-grafana/images/kafka-jmx-broker.png similarity index 100% rename from monitor/assets/images/kafka-jmx-broker.png rename to docs/guides/azure-monitor/hdinsight-kafka-prometheus-grafana/images/kafka-jmx-broker.png diff --git a/monitor/hdinsight-kafka-prometheus-grafana.md b/docs/guides/azure-monitor/hdinsight-kafka-prometheus-grafana/index.md similarity index 76% rename from monitor/hdinsight-kafka-prometheus-grafana.md rename to docs/guides/azure-monitor/hdinsight-kafka-prometheus-grafana/index.md index 092296f1..980ff8e9 100644 --- a/monitor/hdinsight-kafka-prometheus-grafana.md +++ b/docs/guides/azure-monitor/hdinsight-kafka-prometheus-grafana/index.md @@ -1,13 +1,39 @@ +--- +services: +- azure-monitor +official_sources: +- title: Azure Monitor documentation + url: https://learn.microsoft.com/azure/azure-monitor/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: HDInsight Kafka Prometheus와 Grafana 구성 +description: JMX Exporter, Kafka Exporter, Prometheus와 Grafana 대시보드를 구성합니다. +technologies: +- kafka +- prometheus +- grafana +tags: +- monitoring +- observability +- deployment +--- + # HDInsight Kafka JMX Exporter / Kafka Exporter 설치 및 Grafana 대시보드 구성 **전제: HDInsight Kafka 클러스터 + Prometheus + Grafana 기구성.** -도구별 커버 범위·선택 근거("Why") → [hdinsight-kafka-monitoring.md](./hdinsight-kafka-monitoring.md). +도구별 커버 범위·선택 근거("Why") → [HDInsight Kafka 모니터링 방식 비교](../../../research/azure-monitor/hdinsight-kafka-monitoring-options/index.md). -> **문서 성격**: JMX Exporter·Kafka Exporter의 정식 사용법은 각 오픈소스 공식 문서([9절 출처](#9-출처)) 기준. +> **문서 성격**: JMX Exporter·Kafka Exporter의 정식 사용법은 각 오픈소스 공식 문서([9절 출처](#9)) 기준. > 이 문서는 그 위에서 **HDInsight 환경(원격 JMX 9999, Private Link 등)에 맞춰 실전 구성한 요약본**. > 아래 Grafana 대시보드도 커뮤니티 대시보드(7589) 외에는 **본 환경 지표에 맞춰 개별 구성**한 것 → 참고용, 환경별 조정 필요. -참조 설정·대시보드 파일: [`assets/prometheus-grafana/`](./assets/prometheus-grafana/) +참조 설정·대시보드 파일: [`assets/prometheus-grafana/`](https://github.com/hellices/devguidesample/tree/main/samples/azure-monitor/source-material/assets/prometheus-grafana) ``` assets/prometheus-grafana/ @@ -38,7 +64,7 @@ assets/prometheus-grafana/ > **원격 JMX 채택 이유** > jmx_exporter 정석 = 브로커 JVM에 javaagent 부착 → 브로커 롤링 재시작 필요. > HDInsight 브로커는 9999 포트 원격 JMX 개방 → 외부 호스트 `jmx_prometheus_httpserver` 접속으로 -> 브로커 무변경·동일 `kafka_server_*`/JVM 지표 수집. (javaagent 방식은 [7절](#7-참고-공식-javaagent-방식) 참고) +> 브로커 무변경·동일 `kafka_server_*`/JVM 지표 수집. (javaagent 방식은 [7절](#7-javaagent) 참고) --- @@ -96,9 +122,9 @@ for i in 0 1 2; do done ``` -- 변환 룰: [`jmx/kafka-rules.yml`](./assets/prometheus-grafana/jmx/kafka-rules.yml) +- 변환 룰: [`jmx/kafka-rules.yml`](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/assets/prometheus-grafana/jmx/kafka-rules.yml) (처리량 BytesIn/Out·MessagesIn, ReplicaManager URP/ISR, Controller, RequestMetrics 지연, JVM Heap/GC/Thread 등 16개) -- `hostPort`만 브로커별 상이, 룰 내용 동일. 예시: [`jmx/b0.yml.example`](./assets/prometheus-grafana/jmx/b0.yml.example) +- `hostPort`만 브로커별 상이, 룰 내용 동일. 예시: [`jmx/b0.yml.example`](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/assets/prometheus-grafana/jmx/b0.yml.example) ### 3.2 Kafka Exporter (Consumer Lag) @@ -119,7 +145,7 @@ kafka-exporter: ### 3.3 일괄 기동 (docker-compose) -두 Exporter + Prometheus + Grafana 일괄 기동: [`docker-compose.yml`](./assets/prometheus-grafana/docker-compose.yml). +두 Exporter + Prometheus + Grafana 일괄 기동: [`docker-compose.yml`](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/assets/prometheus-grafana/docker-compose.yml). ``, `` 실제 값 치환 후: ```bash @@ -130,7 +156,7 @@ docker compose ps # kafka-exporter, jmx-b0/b1/b2, prometheus, grafana = U ### 3.4 Prometheus 스크래핑 -[`prometheus.yml`](./assets/prometheus-grafana/prometheus.yml): `kafka-exporter:9308` + +[`prometheus.yml`](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/assets/prometheus-grafana/prometheus.yml): `kafka-exporter:9308` + `jmx-b0/b1/b2:9404` 스크래핑. JMX 잡에 `broker` 라벨 부여 → 브로커 구분. **기존 Prometheus 존재 시** 이 `scrape_configs` 두 블록만 추가 후 reload. @@ -142,8 +168,8 @@ docker compose ps # kafka-exporter, jmx-b0/b1/b2, prometheus, grafana = U docker-compose가 Grafana에 자동 주입: -- 데이터소스: [`grafana/provisioning/datasources/prometheus.yml`](./assets/prometheus-grafana/grafana/provisioning/datasources/prometheus.yml) (uid `prometheus`) -- 대시보드 폴더: [`grafana/provisioning/dashboards/provider.yml`](./assets/prometheus-grafana/grafana/provisioning/dashboards/provider.yml) → `Kafka` 폴더 +- 데이터소스: [`grafana/provisioning/datasources/prometheus.yml`](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/assets/prometheus-grafana/grafana/provisioning/datasources/prometheus.yml) (uid `prometheus`) +- 대시보드 폴더: [`grafana/provisioning/dashboards/provider.yml`](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/assets/prometheus-grafana/grafana/provisioning/dashboards/provider.yml) → `Kafka` 폴더 - `grafana/dashboards/*.json` 자동 로드 **기존 Grafana 존재 시** 아래 대시보드 Dashboards → Import + 데이터소스 지정. @@ -153,8 +179,8 @@ docker-compose가 Grafana에 자동 주입: | 대시보드 | 출처 | 지표 | |----------|------|---------| | **Kafka Exporter Overview** | Grafana.com **7589** (공식 커뮤니티) | 메시지 생산율, Consumer Group Lag, 토픽 파티션 수 | -| **HDInsight Kafka - Exporter 상세** | 커스텀 ([kafka-exporter-adv.json](./assets/prometheus-grafana/grafana/dashboards/kafka-exporter-adv.json)) | 컨슈머 그룹 **멤버 수(0=컨슈머 다운)**, Lag 상위 파티션, 언더리플리케이션·ISR·브로커별 리더 분포, 토픽별 보관 메시지 수 | -| **HDInsight Kafka - Broker (JMX Exporter)** | 커스텀 ([kafka-jmx-broker.json](./assets/prometheus-grafana/grafana/dashboards/kafka-jmx-broker.json)) | 브로커별 처리량(BytesIn/Out·MessagesIn), URP, ActiveController, 요청 지연, JVM Heap·스레드 | +| **HDInsight Kafka - Exporter 상세** | 커스텀 ([kafka-exporter-adv.json](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/assets/prometheus-grafana/grafana/dashboards/kafka-exporter-adv.json)) | 컨슈머 그룹 **멤버 수(0=컨슈머 다운)**, Lag 상위 파티션, 언더리플리케이션·ISR·브로커별 리더 분포, 토픽별 보관 메시지 수 | +| **HDInsight Kafka - Broker (JMX Exporter)** | 커스텀 ([kafka-jmx-broker.json](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/assets/prometheus-grafana/grafana/dashboards/kafka-jmx-broker.json)) | 브로커별 처리량(BytesIn/Out·MessagesIn), URP, ActiveController, 요청 지연, JVM Heap·스레드 | **Kafka Exporter Overview(7589)** = 커뮤니티 대시보드 → 파일 아닌 ID로 import. Grafana UI → Dashboards → **Import** → ID `7589` → 데이터소스 `Prometheus`. @@ -178,23 +204,23 @@ Prometheus(`:9090`) 쿼리로 확인. ## 6. 대시보드 화면 -> 이미지는 [`assets/images/`](./assets/images/) 에 아래 파일명으로 저장 시 자동 표시. +> 렌더링 이미지는 이 문서 번들의 `images/` 디렉터리에 아래 파일명으로 두면 자동 표시됩니다. 원본 자료는 [`samples/azure-monitor/source-material/assets/images/`](https://github.com/hellices/devguidesample/tree/main/samples/azure-monitor/source-material/assets/images)에서 확인할 수 있습니다. ### 6.1 Kafka Exporter Overview (7589) -![Kafka Exporter Overview](./assets/images/kafka-exporter-overview.png) +![Kafka Exporter Overview](images/kafka-exporter-overview.png) 메시지 생산율 / Consumer Group Lag / 토픽별 파티션 수. ### 6.2 HDInsight Kafka - Exporter 상세 -![Kafka Exporter 상세](./assets/images/kafka-exporter-adv.png) +![Kafka Exporter 상세](images/kafka-exporter-adv.png) 클러스터 개요(브로커/토픽/파티션/그룹 수, URP, 총 Lag) · 컨슈머 상태(그룹별 Lag, 생산 vs 소비율, 멤버 수, Lag 상위 파티션) · 복제·리더십(URP·ISR·브로커별 리더 분포) · 토픽 상세. ### 6.3 HDInsight Kafka - Broker (JMX Exporter) -![Kafka Broker JMX](./assets/images/kafka-jmx-broker.png) +![Kafka Broker JMX](images/kafka-jmx-broker.png) 브로커별 처리량 · URP · ActiveController · 요청 지연 · JVM Heap/스레드. diff --git a/monitor/sre-agent-event-lab/dynamic-thresholds.md b/docs/guides/azure-monitor/sre-agent-dynamic-thresholds/index.md similarity index 88% rename from monitor/sre-agent-event-lab/dynamic-thresholds.md rename to docs/guides/azure-monitor/sre-agent-dynamic-thresholds/index.md index 68fd1144..ca7de8fe 100644 --- a/monitor/sre-agent-event-lab/dynamic-thresholds.md +++ b/docs/guides/azure-monitor/sre-agent-dynamic-thresholds/index.md @@ -1,3 +1,27 @@ +--- +services: +- azure-monitor +official_sources: +- title: Azure Monitor documentation + url: https://learn.microsoft.com/azure/azure-monitor/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Dynamic Thresholds와 SRE Agent 연계 +description: Azure Monitor Dynamic Thresholds 경고를 SRE Agent 조사 흐름과 연계합니다. +technologies: +- rest-api +tags: +- monitoring +- ai-agents +- architecture +--- + # Azure Monitor Dynamic Thresholds와 SRE Agent 연계 설계 - 대상: Azure Monitor 정적 임계값을 사용하는 Azure SRE Agent 이벤트 연계 환경 @@ -56,7 +80,7 @@ query는 `summarize` 결과가 하나 이상의 numeric series를 반환해야 - `ignoreDataBefore`: 정상 telemetry가 안정적으로 쌓이기 시작한 UTC - Action Group: 기존 `ag-sre-agent-event-lab` - Event path(기본): Dynamic alert → Azure Monitor incident platform 연결 → Review 모드 response plan → investigation. 현재 실습이 배포하는 표준 경로이며 Dynamic rule도 같은 경로를 그대로 쓴다. -- Event path(레거시 bridge): Dynamic alert → Action Group → Logic App managed identity → SRE HTTP Trigger → Review-mode investigation. 2026-08-12 실측에 쓰인 legacy 구성이고 기본 실습에는 배포하지 않는다([validation-results.md](validation-results.md)). +- Event path(레거시 bridge): Dynamic alert → Action Group → Logic App managed identity → SRE HTTP Trigger → Review-mode investigation. 2026-08-12 실측에 쓰인 legacy 구성이고 기본 실습에는 배포하지 않는다([실측 검증 결과](../../../research/azure-monitor/sre-agent-validation-results/index.md)). 이는 시작점이지 모든 workload의 정답이 아니다. Preview Chart와 incident 결과를 보고 sensitivity와 failing periods를 조정한다. diff --git a/monitor/sre-agent-event-lab/runbooks/incident-response.md b/docs/guides/azure-monitor/sre-agent-incident-runbook/index.md similarity index 90% rename from monitor/sre-agent-event-lab/runbooks/incident-response.md rename to docs/guides/azure-monitor/sre-agent-incident-runbook/index.md index 17b36e96..02973aa1 100644 --- a/monitor/sre-agent-event-lab/runbooks/incident-response.md +++ b/docs/guides/azure-monitor/sre-agent-incident-runbook/index.md @@ -1,3 +1,27 @@ +--- +services: +- azure-monitor +official_sources: +- title: Azure Monitor documentation + url: https://learn.microsoft.com/azure/azure-monitor/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Azure SRE Agent Incident Runbook +description: Azure SRE Agent 이벤트 실습의 장애 대응 runbook을 제공합니다. +technologies: +- rest-api +tags: +- troubleshooting +- monitoring +- reliability +--- + # Azure SRE Agent Event Lab Incident Runbook ## Scope and safety diff --git a/ptu_lb/ptu_architecture.png b/docs/guides/azure-openai/adaptive-ptu-load-balancing/images/ptu_architecture.png similarity index 100% rename from ptu_lb/ptu_architecture.png rename to docs/guides/azure-openai/adaptive-ptu-load-balancing/images/ptu_architecture.png diff --git a/ptu_lb/ptu-lb-test.md b/docs/guides/azure-openai/adaptive-ptu-load-balancing/index.md similarity index 92% rename from ptu_lb/ptu-lb-test.md rename to docs/guides/azure-openai/adaptive-ptu-load-balancing/index.md index 7d235bd0..5f4fb792 100644 --- a/ptu_lb/ptu-lb-test.md +++ b/docs/guides/azure-openai/adaptive-ptu-load-balancing/index.md @@ -1,3 +1,28 @@ +--- +services: +- azure-openai +official_sources: +- title: Azure OpenAI documentation + url: https://learn.microsoft.com/azure/ai-services/openai/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Adaptive PTU TPM Quota 조정 +description: Azure OpenAI PTU 배포 사이에서 TPM quota를 적응형으로 조정하는 방법을 설명합니다. +technologies: +- python +- powershell +tags: +- performance +- reliability +- ai-agents +--- + # Adaptive PTU TPM Quota 조정 가이드 (with Sample Test) ## 문제 상황 @@ -28,7 +53,7 @@ Azure Automation은 **스케줄러 + 실행/인증 호스트** 역할이며, 실 - capacity 조정(빨강): 런북이 Reserved 총량 제약 하에 **감축→증설 순서**로 Foundry 배포 SKU capacity를 ARM(공용 경로)으로 갱신 -![PTU TPM Quota Adaptive Controller 아키텍처](ptu_architecture.png) +![PTU TPM Quota Adaptive Controller 아키텍처](images/ptu_architecture.png) ## Azure 리소스 구성 @@ -265,13 +290,12 @@ $$ ## 파일 구성 -- 메인 문서: [ptu-lb-test.md](ptu-lb-test.md) -- 아키텍처 다이어그램(원본): [ptu_architecture.excalidraw](ptu_architecture.excalidraw) -- 아키텍처 다이어그램(이미지): [ptu_architecture.png](ptu_architecture.png) -- 다이어그램 렌더러(유틸): [render_excalidraw.py](render_excalidraw.py) -- **Runbook(기본, Python): [runbooks/adaptive_tpm_quota_controller.py](runbooks/adaptive_tpm_quota_controller.py)** -- Runbook 의존성: [runbooks/requirements.txt](runbooks/requirements.txt) -- Runbook(선택, PowerShell 동등 이식본): [runbooks/optional_adaptive_tpm_quota_controller.ps1](runbooks/optional_adaptive_tpm_quota_controller.ps1) +- 아키텍처 다이어그램(원본): [ptu_architecture.excalidraw](https://github.com/hellices/devguidesample/blob/main/samples/azure-openai/ptu-load-balancing/ptu_architecture.excalidraw) +- 아키텍처 다이어그램(이미지): [ptu_architecture.png](https://github.com/hellices/devguidesample/blob/main/samples/azure-openai/ptu-load-balancing/ptu_architecture.png) +- 다이어그램 렌더러(유틸): [render_excalidraw.py](https://github.com/hellices/devguidesample/blob/main/samples/azure-openai/ptu-load-balancing/render_excalidraw.py) +- **Runbook(기본, Python): [runbooks/adaptive_tpm_quota_controller.py](https://github.com/hellices/devguidesample/blob/main/samples/azure-openai/ptu-load-balancing/runbooks/adaptive_tpm_quota_controller.py)** +- Runbook 의존성: [runbooks/requirements.txt](https://github.com/hellices/devguidesample/blob/main/samples/azure-openai/ptu-load-balancing/runbooks/requirements.txt) +- Runbook(선택, PowerShell 동등 이식본): [runbooks/optional_adaptive_tpm_quota_controller.ps1](https://github.com/hellices/devguidesample/blob/main/samples/azure-openai/ptu-load-balancing/runbooks/optional_adaptive_tpm_quota_controller.ps1) > 기본 구현은 Python 런북이다. PowerShell 버전은 동일한 Reserved 총량 공유 재분배 로직(감축→증설, 실제 actuation·쿨다운 포함)을 Az 모듈(Az.Accounts / Az.CognitiveServices / AzTable)로 이식한 **선택적 대체 구현**으로, PowerShell 선호 환경을 위한 참조본이다. Storage가 Private Endpoint 구성이면 이 런북도 프라이빗 경로가 닿는 워커(예: Windows Hybrid Worker)에서 실행해야 한다. diff --git a/azureblob/spring-resumable-upload/architecture.png b/docs/guides/azure-storage/mobile-resumable-upload-tus/images/architecture.png similarity index 100% rename from azureblob/spring-resumable-upload/architecture.png rename to docs/guides/azure-storage/mobile-resumable-upload-tus/images/architecture.png diff --git a/azureblob/mobile_resumable_upload_tus.md b/docs/guides/azure-storage/mobile-resumable-upload-tus/index.md similarity index 95% rename from azureblob/mobile_resumable_upload_tus.md rename to docs/guides/azure-storage/mobile-resumable-upload-tus/index.md index e08c6ceb..eb7d5216 100644 --- a/azureblob/mobile_resumable_upload_tus.md +++ b/docs/guides/azure-storage/mobile-resumable-upload-tus/index.md @@ -1,3 +1,28 @@ +--- +services: +- azure-storage +official_sources: +- title: Azure Storage documentation + url: https://learn.microsoft.com/azure/storage/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Spring Boot tus와 Azure Block Blob 재개 업로드 +description: 모바일 대용량 파일을 tus 프로토콜과 Azure Block Blob으로 재개 업로드하는 구성을 설명합니다. +technologies: +- java +- rest-api +tags: +- storage +- reliability +- development +--- + # Spring Boot Resumable Upload — tus.io ↔ Azure Block Blob 모바일 사용자 약 20만 명 규모, 일반 50 MB · 최대 200 MB 파일을 끊김 후 이어 올리되, 파드 OOM 없이 Azure Block Blob 으로 자연스럽게 흘러가게 만든 샘플. @@ -22,11 +47,11 @@ ## 아키텍처 -![Architecture — Mobile (tus 1.0.0) ▸ AKS Pods ▸ Azure Blob](spring-resumable-upload/architecture.png) +![Architecture — Mobile (tus 1.0.0) ▸ AKS Pods ▸ Azure Blob](images/architecture.png) -편집 원본: [spring-resumable-upload/architecture.excalidraw](spring-resumable-upload/architecture.excalidraw) +편집 원본: [spring-resumable-upload/architecture.excalidraw](https://github.com/hellices/devguidesample/blob/main/samples/azure-storage/source-material/spring-resumable-upload/architecture.excalidraw) -샘플 코드 위치: [spring-resumable-upload/](spring-resumable-upload/) +샘플 코드 위치: [spring-resumable-upload/](https://github.com/hellices/devguidesample/tree/main/samples/azure-storage/source-material/spring-resumable-upload) ## 왜 tus 인가 diff --git a/aifoundry/codex_closednetwork.md b/docs/guides/microsoft-foundry/codex-closed-network/index.md similarity index 85% rename from aifoundry/codex_closednetwork.md rename to docs/guides/microsoft-foundry/codex-closed-network/index.md index 543b0e75..e777a8e2 100644 --- a/aifoundry/codex_closednetwork.md +++ b/docs/guides/microsoft-foundry/codex-closed-network/index.md @@ -1,3 +1,27 @@ +--- +services: +- microsoft-foundry +official_sources: +- title: Microsoft Foundry documentation + url: https://learn.microsoft.com/azure/ai-foundry/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Codex와 Microsoft Foundry 폐쇄망 연동 +description: 폐쇄망에서 Codex와 Microsoft Foundry를 연동할 때 필요한 네트워크 구성을 설명합니다. +technologies: +- rest-api +tags: +- networking +- security +- ai-agents +--- + # Codex ↔ Azure AI Foundry 연동: 폐쇄망 환경 가이드 ## **문제 상황** @@ -37,12 +61,12 @@ Azure AI Foundry를 Private Endpoint로 구성한 경우, Azure 망과 업무망 Private Endpoint 도메인을 `localhost`로 매핑한다. -> 예: Private Endpoint 도메인이 `aif-krc-openai.azure.com`인 경우 +> 예: Private Endpoint 도메인이 `example-openai.openai.azure.com`인 경우 **Windows hosts 파일 경로**: `C:\Windows\System32\drivers\etc\hosts` ``` -127.0.0.1 aif-krc-openai.azure.com +127.0.0.1 example-openai.openai.azure.com ``` *** @@ -62,7 +86,7 @@ preferred_auth_method = "apikey" [model_providers.custom-provider] name = "azure-openai" -base_url = "https://aif-krc-openai.azure.com/openai/v1/" +base_url = "https://example-openai.openai.azure.com/openai/v1/" env_key = "OPENAI_API_KEY" wire_api = "responses" @@ -110,12 +134,12 @@ http { server { listen 443; - server_name aif-krc-openai.azure.com; + server_name example-openai.openai.azure.com; location / { proxy_pass http://127.0.0.1:50056; - proxy_set_header Host aif-krc-openai.azure.com; + proxy_set_header Host example-openai.openai.azure.com; proxy_set_header Proxy-Connection keep-alive; } diff --git a/aifoundry/foundry_local.md b/docs/guides/microsoft-foundry/foundry-local-air-gapped/index.md similarity index 96% rename from aifoundry/foundry_local.md rename to docs/guides/microsoft-foundry/foundry-local-air-gapped/index.md index 027829f9..2a090602 100644 --- a/aifoundry/foundry_local.md +++ b/docs/guides/microsoft-foundry/foundry-local-air-gapped/index.md @@ -1,3 +1,27 @@ +--- +services: +- microsoft-foundry +official_sources: +- title: Microsoft Foundry documentation + url: https://learn.microsoft.com/azure/ai-foundry/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Foundry Local 에어갭 구성과 제약 +description: Foundry Local SDK와 CLI의 제약 및 에어갭 환경 구성을 정리합니다. +technologies: +- azure-cli +tags: +- deployment +- security +- ai-agents +--- + # Foundry Local (On-device SDK/CLI): 제약 사항과 에어갭(Air-gapped) 구성 가이드 > 본 문서는 **Microsoft Foundry Local — 온디바이스 SDK/CLI** (`foundry-local-sdk`) 를 기준으로 한다. diff --git a/memory/hybrid-architecture.png b/docs/guides/microsoft-foundry/gpt-memory-layer/images/hybrid-architecture.png similarity index 100% rename from memory/hybrid-architecture.png rename to docs/guides/microsoft-foundry/gpt-memory-layer/images/hybrid-architecture.png diff --git a/memory/foundry-gpt-memory-v1.md b/docs/guides/microsoft-foundry/gpt-memory-layer/index.md similarity index 96% rename from memory/foundry-gpt-memory-v1.md rename to docs/guides/microsoft-foundry/gpt-memory-layer/index.md index a94b3eea..c87f86ac 100644 --- a/memory/foundry-gpt-memory-v1.md +++ b/docs/guides/microsoft-foundry/gpt-memory-layer/index.md @@ -1,6 +1,29 @@ +--- +services: +- microsoft-foundry +official_sources: +- title: Microsoft Foundry documentation + url: https://learn.microsoft.com/azure/ai-foundry/ +document_type: guide +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 180 +applies_to: +- 공식 원문 재검토 필요 +title: Microsoft Foundry GPT 메모리 레이어 설계 +description: Microsoft Foundry 기반 GPT 애플리케이션의 메모리 레이어 아키텍처를 설명합니다. +technologies: +- rag +tags: +- ai-agents +- architecture +--- + # Azure Foundry GPT-5.x 메모리 레이어 아키텍처 가이드 -> 공식 Microsoft Learn 문서 기반 검증 완료. 2026년 7월 22일 기준. +> 이관된 원문입니다. 공식 Microsoft Learn 문서와의 의미 검증이 필요합니다. --- @@ -497,7 +520,7 @@ Australia East, Brazil South, Canada East, East US 2, France Central, Italy Nort 현재 Redis 기반 세션 메모리를 운영 중이라면, 세 계층을 분리하되 각각의 강점을 활용하는 하이브리드 아키텍처를 권장합니다. -![하이브리드 메모리 아키텍처](hybrid-architecture.png) +![하이브리드 메모리 아키텍처](images/hybrid-architecture.png) @@ -526,4 +549,4 @@ Australia East, Brazil South, Canada East, East US 2, France Central, Italy Nort --- -*마지막 업데이트: 2026년 7월 22일 — 공식 문서 검증 완료* +*마지막 업데이트: 2026년 7월 22일 — 공식 원문 의미 검증 대기* diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..7f691f34 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,75 @@ +--- +title: DevGuideSample +description: P99 지연부터 검색 품질, GPU 비용까지. 코드와 관측 결과로 살펴보는 Azure·Kubernetes·AI 엔지니어링 노트 +hide: + - toc +--- + +
+ +
+
+ +

ENGINEERING NOTES / AZURE · KUBERNETES · AI

+ +# 증상 너머,
동작 원리까지. + +

P99 지연, CPU throttling, 검색 품질, GPU 비용.
운영에서 마주친 질문을 코드·로그·측정 결과로 좁혀갑니다.

+ +
+ +[트러블슈팅 읽기](cases/index.md){ .dg-button .dg-button--primary } +[서비스별로 찾아보기](services/index.md){ .dg-button .dg-button--secondary } + +
+
+ +
+ +

어디서부터 파고들까요?

+ +[**지연과 병목**
P95는 괜찮은데, 왜 P99에서 문제가 드러날까?](cases/index.md){ .dg-topic-link } + +[**성능과 비용**
단건 지연과 최대 처리량, GPU 선택의 기준은?](research/index.md){ .dg-topic-link } + +[**구성과 동작**
설정한 대로 동작하는지, 어디서 확인해야 할까?](guides/index.md){ .dg-topic-link } + +
+
+ + + +
+ +

SELECTED NOTES

+ +## 먼저 파고들 만한 기록 + +기술 이름보다, 그 기술을 쓰며 부딪힌 질문에서 시작합니다. + +
+ + + +
+ +

EXPLORE THE COLLECTION

+ +## 지금 필요한 방식으로 읽기 + +원인을 찾을 때와 구성을 만들 때, 선택지를 비교할 때 필요한 글은 다릅니다. + +
+ + + +
+ +**같은 문제를 겪었다면, 기록을 보태주세요.** + +어떤 환경에서 무엇을 관측했고, 왜 그 방법을 선택했는지. 다음 사람이 같은 시행착오를 줄일 수 있도록요. + +[글 기여하기](contributing/index.md){ .dg-text-link } + +
+
diff --git a/monitor/sre-agent-event-lab/assets/official/portal-complete-setup-page.png b/docs/labs/azure-monitor/sre-agent-event-lab-setup/images/portal-complete-setup-page.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/official/portal-complete-setup-page.png rename to docs/labs/azure-monitor/sre-agent-event-lab-setup/images/portal-complete-setup-page.png diff --git a/monitor/sre-agent-event-lab/assets/official/portal-incident-response-plans-list.png b/docs/labs/azure-monitor/sre-agent-event-lab-setup/images/portal-incident-response-plans-list.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/official/portal-incident-response-plans-list.png rename to docs/labs/azure-monitor/sre-agent-event-lab-setup/images/portal-incident-response-plans-list.png diff --git a/monitor/sre-agent-event-lab/assets/official/portal-response-plan-autonomy-step.png b/docs/labs/azure-monitor/sre-agent-event-lab-setup/images/portal-response-plan-autonomy-step.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/official/portal-response-plan-autonomy-step.png rename to docs/labs/azure-monitor/sre-agent-event-lab-setup/images/portal-response-plan-autonomy-step.png diff --git a/monitor/sre-agent-event-lab/assets/official/portal-setup-status-bar.png b/docs/labs/azure-monitor/sre-agent-event-lab-setup/images/portal-setup-status-bar.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/official/portal-setup-status-bar.png rename to docs/labs/azure-monitor/sre-agent-event-lab-setup/images/portal-setup-status-bar.png diff --git a/monitor/sre-agent-event-lab/guides/01-agent-setup.md b/docs/labs/azure-monitor/sre-agent-event-lab-setup/index.md similarity index 92% rename from monitor/sre-agent-event-lab/guides/01-agent-setup.md rename to docs/labs/azure-monitor/sre-agent-event-lab-setup/index.md index 9515c96b..14c2e435 100644 --- a/monitor/sre-agent-event-lab/guides/01-agent-setup.md +++ b/docs/labs/azure-monitor/sre-agent-event-lab-setup/index.md @@ -1,3 +1,28 @@ +--- +services: +- azure-monitor +official_sources: +- title: Azure Monitor documentation + url: https://learn.microsoft.com/azure/azure-monitor/ +document_type: lab +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 90 +estimated_time: 60m +cost: paid +cleanup_required: true +title: Azure SRE Agent 실습 설정 +description: 이벤트 기반 장애 분석 실습에 필요한 Azure SRE Agent를 설정합니다. +technologies: +- bicep +tags: +- ai-agents +- deployment +- monitoring +--- + # 01. Azure SRE Agent 설정 포털에서만 할 수 있는 설정을 마치고, 그 사실을 실습 상태 파일에 기록하는 단계입니다. 여기까지 끝나야 S1을 시작할 수 있습니다. @@ -5,13 +30,13 @@ ## 시작 조건 - **이 저장소를 본인 계정으로 fork했습니다.** Agent에 연결하는 저장소는 fork여야 합니다. 아래 "연결할 원본"에서 설명하듯 조사 결과 이슈가 연결된 저장소에 생성되므로, 원본 저장소를 연결하면 참가자 전원의 이슈가 한곳에 쌓입니다. -- [README](../README.md)의 `azd up`이 성공했고 `/healthz`가 HTTP 200을 반환합니다. +- [README](../sre-agent-event-lab/index.md)의 `azd up`이 성공했고 `/healthz`가 HTTP 200을 반환합니다. - `https://sre.azure.com`에 로그인할 수 있고, 대상 구독에 Azure SRE Agent가 **Running** 상태로 하나 있습니다. - 역할을 만들 수 있는 권한(Owner 또는 User Access Administrator)이 있습니다. - 아래 값을 손에 들고 시작합니다. ```bash -cd monitor/sre-agent-event-lab +cd samples/azure-monitor/source-material/sre-agent-event-lab source ./scripts/lab-env.sh ``` @@ -21,13 +46,13 @@ source ./scripts/lab-env.sh Agent를 열면 위쪽 상태 표시줄이 아직 연결하지 않은 데이터 원본 수를 보여 줍니다. 오른쪽 **Complete setup**을 누르면 설정 페이지로 이동합니다. -![Azure SRE Agent 포털 첫 화면. 위쪽 상태 표시줄에 주황색 글씨로 6 sources not configured가 뜨고 그 오른쪽에 Code, Logs, Deployments, Incidents, Azure resources, Knowledge files 여섯 항목이 점 표시와 함께 나열되며, 줄 끝에 Complete setup 링크와 Expand 화살표가 있다. 왼쪽 탐색에는 Activities, Builder, Monitor, Capabilities, Settings가 접힌 채로 있고 아래에 Favorites의 Team onboarding 항목이 보인다. 가운데는 Azure SRE Agent 로고와 질문 입력창이다.](../assets/official/portal-setup-status-bar.png) +![Azure SRE Agent 포털 첫 화면. 위쪽 상태 표시줄에 주황색 글씨로 6 sources not configured가 뜨고 그 오른쪽에 Code, Logs, Deployments, Incidents, Azure resources, Knowledge files 여섯 항목이 점 표시와 함께 나열되며, 줄 끝에 Complete setup 링크와 Expand 화살표가 있다. 왼쪽 탐색에는 Activities, Builder, Monitor, Capabilities, Settings가 접힌 채로 있고 아래에 Favorites의 Team onboarding 항목이 보인다. 가운데는 Azure SRE Agent 로고와 질문 입력창이다.](images/portal-setup-status-bar.png) > 출처: [Complete setup for Azure SRE Agent](https://learn.microsoft.com/azure/sre-agent/complete-setup) 설정 페이지에는 탭이 두 개입니다. 이 실습은 Azure 리소스와 지식 문서까지 연결해야 하므로 **Full setup** 탭을 사용합니다. -![설정 페이지 상단에 More context. Better investigations. 제목과 그 아래 Quickstart, Full setup 두 개의 탭이 있고 Quickstart 탭이 파란색으로 선택되어 있다. 첫 카드에는 SRE Agent doesn't know anything about your app 이라는 경고 문구와 아직 채워지지 않은 회색 진행률 막대가 있다. 그 아래 Code 카드와 Logs 카드에는 각각 Recommended 배지와 오른쪽 끝 파란색 더하기 버튼이 있다.](../assets/official/portal-complete-setup-page.png) +![설정 페이지 상단에 More context. Better investigations. 제목과 그 아래 Quickstart, Full setup 두 개의 탭이 있고 Quickstart 탭이 파란색으로 선택되어 있다. 첫 카드에는 SRE Agent doesn't know anything about your app 이라는 경고 문구와 아직 채워지지 않은 회색 진행률 막대가 있다. 그 아래 Code 카드와 Logs 카드에는 각각 Recommended 배지와 오른쪽 끝 파란색 더하기 버튼이 있다.](images/portal-complete-setup-page.png) > 출처: [Complete setup for Azure SRE Agent](https://learn.microsoft.com/azure/sre-agent/complete-setup) @@ -37,7 +62,7 @@ Agent를 열면 위쪽 상태 표시줄이 아직 연결하지 않은 데이터 |---|---|---| | Code | **본인이 fork한 저장소**와 브랜치 (`source ./scripts/lab-env.sh`가 출력한 `Repository` 값) | 조사 결론이 코드와 최근 변경을 짚게 합니다 | | Azure resources | `lab-env.sh`가 출력한 `Resource group` 값 | 메트릭·리소스 상태·Activity Log를 읽습니다 | -| Knowledge files | [runbooks/incident-response.md](../runbooks/incident-response.md) | 조사 순서와 금지 사항을 팀 규칙으로 강제합니다 | +| Knowledge files | [인시던트 대응 런북](../../../guides/azure-monitor/sre-agent-incident-runbook/index.md) | 조사 순서와 금지 사항을 팀 규칙으로 강제합니다 | | Incidents | Azure Monitor | 경고를 자동으로 받아 조사 스레드를 엽니다 | 저장소는 [Connect source code](https://learn.microsoft.com/azure/sre-agent/connect-source-code)로 연결합니다. 이슈·PR 조작까지 맡기려면 [GitHub connector](https://learn.microsoft.com/azure/sre-agent/setup-github-connector)를 추가로 설정하되, 토큰 값은 포털 입력창에만 넣고 이 저장소의 어떤 파일에도 남기지 않습니다. @@ -110,7 +135,7 @@ UAMI_MONITORING_CONTRIBUTOR_ASSIGNMENT_ID="$(az role assignment create \ 연결되면 오른쪽 위에 초록색 체크가 붙습니다. 경고는 몇 분 안에 흘러들기 시작합니다. -![왼쪽 탐색의 Builder 메뉴가 펼쳐져 Agent Canvas, Skills, Incident response plans, Scheduled tasks, Plugins, Hooks, Connectors, Knowledge base 항목이 보이고 그중 Incident response plans가 선택되어 있다. 오른쪽 위에는 초록색 체크 아이콘과 함께 Azure Monitor is connected 문구가 있다. 표에는 계획 한 건이 있고 Status 열은 On, Autonomy level 열은 Autonomous로 표시되며, 위쪽에는 New incident response plan, Refresh, Delete, Turn off 버튼과 Severity equals All 필터가 있다.](../assets/official/portal-incident-response-plans-list.png) +![왼쪽 탐색의 Builder 메뉴가 펼쳐져 Agent Canvas, Skills, Incident response plans, Scheduled tasks, Plugins, Hooks, Connectors, Knowledge base 항목이 보이고 그중 Incident response plans가 선택되어 있다. 오른쪽 위에는 초록색 체크 아이콘과 함께 Azure Monitor is connected 문구가 있다. 표에는 계획 한 건이 있고 Status 열은 On, Autonomy level 열은 Autonomous로 표시되며, 위쪽에는 New incident response plan, Refresh, Delete, Turn off 버튼과 Severity equals All 필터가 있다.](images/portal-incident-response-plans-list.png) > 출처: [Tutorial: Automate incident response in Azure SRE Agent](https://learn.microsoft.com/azure/sre-agent/automate-incidents) > @@ -125,7 +150,7 @@ UAMI_MONITORING_CONTRIBUTOR_ASSIGNMENT_ID="$(az role assignment create \ `Review`는 Agent가 진단은 스스로 하되 리소스 변경 전에 승인을 기다리는 모드입니다. 이 실습은 장애 주입과 복구를 스크립트가 통제하므로, 자율 모드를 고르면 Agent와 스크립트가 같은 리소스를 동시에 되돌리려 할 수 있습니다. -![응답 계획 마법사의 세 번째 화면. 왼쪽 단계 목록에서 Set up incident filters와 Preview filter results는 초록색 체크로 완료되어 있고 3번 Save response plan이 진행 중이다. 오른쪽에는 Choose agent autonomy level for this handler 문구 아래 Review (Default)와 Autonomous 두 개의 라디오 버튼이 설명과 함께 있으며 Autonomous 쪽이 파란 점으로 켜져 있다. 그 아래 Turn on deep investigation 항목의 Run deep investigation autonomously 체크박스는 비어 있고, 화면 맨 아래에 Back, Save, Cancel 버튼이 있다.](../assets/official/portal-response-plan-autonomy-step.png) +![응답 계획 마법사의 세 번째 화면. 왼쪽 단계 목록에서 Set up incident filters와 Preview filter results는 초록색 체크로 완료되어 있고 3번 Save response plan이 진행 중이다. 오른쪽에는 Choose agent autonomy level for this handler 문구 아래 Review (Default)와 Autonomous 두 개의 라디오 버튼이 설명과 함께 있으며 Autonomous 쪽이 파란 점으로 켜져 있다. 그 아래 Turn on deep investigation 항목의 Run deep investigation autonomously 체크박스는 비어 있고, 화면 맨 아래에 Back, Save, Cancel 버튼이 있다.](images/portal-response-plan-autonomy-step.png) > 출처: [Tutorial: Automate incident response in Azure SRE Agent](https://learn.microsoft.com/azure/sre-agent/automate-incidents) > @@ -236,4 +261,4 @@ python3 scripts/lab_state.py acknowledge-agent ## 다음 단계 -첫 장애를 주입합니다: [02-scenario-s1.md](02-scenario-s1.md) +첫 장애를 주입합니다: [S1 — HTTP 500 장애](../sre-agent-scenario-http-500/index.md) diff --git a/monitor/sre-agent-event-lab/README.md b/docs/labs/azure-monitor/sre-agent-event-lab/index.md similarity index 82% rename from monitor/sre-agent-event-lab/README.md rename to docs/labs/azure-monitor/sre-agent-event-lab/index.md index d41a3a8b..e23585e0 100644 --- a/monitor/sre-agent-event-lab/README.md +++ b/docs/labs/azure-monitor/sre-agent-event-lab/index.md @@ -1,8 +1,34 @@ +--- +services: +- azure-monitor +official_sources: +- title: Azure Monitor documentation + url: https://learn.microsoft.com/azure/azure-monitor/ +document_type: lab +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 90 +estimated_time: 150m +cost: paid +cleanup_required: true +title: Azure SRE Agent 이벤트 기반 장애 분석 실습 +description: 장애를 주입하고 Azure SRE Agent의 조사 결과를 검증하는 전체 실습입니다. +technologies: +- bicep +- python +tags: +- ai-agents +- monitoring +- troubleshooting +--- + # Azure SRE Agent 이벤트 기반 장애 분석 실습 -Azure Container Apps에 장애를 세 번 주입하고, Azure Monitor 경고를 받은 Azure SRE Agent가 실제로 조사·결론까지 도달하는지 확인합니다. 제품 개요는 [Azure SRE Agent 소개](../azure-sre-agent.md)를 먼저 읽어 주세요. +Azure Container Apps에 장애를 세 번 주입하고, Azure Monitor 경고를 받은 Azure SRE Agent가 실제로 조사·결론까지 도달하는지 확인합니다. 제품 개요는 [Azure SRE Agent 소개](../../../guides/azure-monitor/azure-sre-agent-overview/index.md)를 먼저 읽어 주세요. -> ⚠️ 이 실습은 실제 Azure 리소스를 만들고 **과금**합니다. 끝나면 반드시 [정리](#정리) 절차로 지우세요. +> ⚠️ 이 실습은 실제 Azure 리소스를 만들고 **과금**합니다. 끝나면 반드시 [정리](#_8) 절차로 지우세요. ## 결과물 @@ -35,14 +61,14 @@ Azure SRE Agent는 이 실습이 만들지 않습니다. 미리 만들어 둔 Ag 이 실습은 **본인 fork에서** 진행합니다. Agent에 저장소를 연결하면 조사 결과 이슈가 그 저장소에 생성되므로, 원본을 연결하면 참가자 전원의 이슈가 한곳에 쌓이고 쓰기 권한도 없습니다. 1. 이 저장소를 본인 계정으로 fork합니다. -2. fork에서 **Code > Codespaces > Create codespace**를 엽니다. 저장소 기본 dev container가 `az`, `azd`, `gh`, Python, `uv`, `jq`를 갖춰 줍니다. 도구 목록과 로컬 설치 방법은 [.devcontainer/README.md](../../.devcontainer/README.md)에 있습니다. +2. fork에서 **Code > Codespaces > Create codespace**를 엽니다. 저장소 기본 dev container가 `az`, `azd`, `gh`, Python, `uv`, `jq`를 갖춰 줍니다. 도구 목록과 로컬 설치 방법은 [.devcontainer/README.md](https://github.com/hellices/devguidesample/blob/main/.devcontainer/README.md)에 있습니다. 3. 터미널에서 로그인한 뒤 환경을 한 번 읽습니다. ```bash az login --use-device-code azd auth login -source ./monitor/sre-agent-event-lab/scripts/lab-env.sh +source ./samples/azure-monitor/source-material/sre-agent-event-lab/scripts/lab-env.sh ``` `lab-env.sh`는 `azd`가 게시한 배포 출력만 읽어 리소스 그룹·구독·Container App·Storage 범위 등을 현재 셸에 export하고, 값이 하나라도 없으면 `LAB_READY=0`으로 알려 줍니다. 이후 가이드의 명령은 이 값들을 그대로 사용하므로 단계마다 다시 조회하지 않습니다. 비밀 값은 읽지도 출력하지도 않습니다. @@ -51,7 +77,7 @@ source ./monitor/sre-agent-event-lab/scripts/lab-env.sh ## 사전 조건 -- `az`, `azd`, `jq`, `curl`, `python3`, `uv` — 설치 명령은 [.devcontainer/README.md](../../.devcontainer/README.md)가 관리합니다. `uv`는 `app/.venv`를 만드는 `scripts/setup-venv.sh`가 쓰는 유일한 도구이며, 사내 프록시로 구성된 인덱스 설정을 그대로 씁니다(공개 PyPI `pip` 폴백 없음). +- `az`, `azd`, `jq`, `curl`, `python3`, `uv` — 설치 명령은 [.devcontainer/README.md](https://github.com/hellices/devguidesample/blob/main/.devcontainer/README.md)가 관리합니다. `uv`는 `app/.venv`를 만드는 `scripts/setup-venv.sh`가 쓰는 유일한 도구이며, 사내 프록시로 구성된 인덱스 설정을 그대로 씁니다(공개 PyPI `pip` 폴백 없음). - `az extension add --name log-analytics` (`az monitor log-analytics query` 제공) - `az login`과 `azd auth login` — 두 CLI는 자격 증명을 따로 관리합니다. - 구독 Contributor, 역할 할당을 위한 Owner 또는 User Access Administrator @@ -62,7 +88,7 @@ source ./monitor/sre-agent-event-lab/scripts/lab-env.sh 이 실습의 모든 명령은 아래에서 한 번만 진입하는 이 디렉터리를 기준으로 합니다. ```bash -cd monitor/sre-agent-event-lab +cd samples/azure-monitor/source-material/sre-agent-event-lab ``` 로컬 검증만 먼저 해 보려면 다음을 실행합니다. `setup-venv.sh`는 `azd up`의 postprovision 단계에서도 실행되므로, 배포를 먼저 한 경우에는 이미 준비된 상태입니다. @@ -108,13 +134,13 @@ postprovision 단계가 실패하면 로컬 환경만 실패한 것입니다. `. ## Azure SRE Agent 설정 -포털에서만 할 수 있는 설정이 남아 있습니다. 저장소 연결, 지식 문서 업로드, **Azure Monitor incident platform** 연결, `Review` 모드 응답 계획, 역할 할당을 [guides/01-agent-setup.md](guides/01-agent-setup.md)가 순서대로 안내합니다. +포털에서만 할 수 있는 설정이 남아 있습니다. 저장소 연결, 지식 문서 업로드, **Azure Monitor incident platform** 연결, `Review` 모드 응답 계획, 역할 할당을 [에이전트 설정](../sre-agent-event-lab-setup/index.md)이 순서대로 안내합니다. -기본 실습에는 Logic App bridge를 배포하지 않습니다. 제품 표준 경로는 Azure Monitor를 incident platform으로 연결하는 것이고, 예전 실측에서 쓰던 Action Group + Logic App 인증 경로는 레거시 기록으로만 남아 있습니다([validation-results.md](validation-results.md)). +기본 실습에는 Logic App bridge를 배포하지 않습니다. 제품 표준 경로는 Azure Monitor를 incident platform으로 연결하는 것이고, 예전 실측에서 쓰던 Action Group + Logic App 인증 경로는 레거시 기록으로만 남아 있습니다([실측 검증 결과](../../../research/azure-monitor/sre-agent-validation-results/index.md)). ## 정상 상태 확인과 승인 -장애를 주입하기 전에 정상 부하가 Application Insights까지 도달하는지 확인하고, 그 사실을 기록해야 S1이 열립니다. 텔레메트리가 없는 워크로드에 장애를 넣으면 주입한 장애와 원래부터 안 보이던 상태를 구별할 수 없습니다. 명령은 [guides/01-agent-setup.md](guides/01-agent-setup.md)에 있습니다. +장애를 주입하기 전에 정상 부하가 Application Insights까지 도달하는지 확인하고, 그 사실을 기록해야 S1이 열립니다. 텔레메트리가 없는 워크로드에 장애를 넣으면 주입한 장애와 원래부터 안 보이던 상태를 구별할 수 없습니다. 명령은 [에이전트 설정](../sre-agent-event-lab-setup/index.md)에 있습니다. ```bash python3 scripts/lab_state.py mark baseline_passed --evidence-dir "${EVIDENCE_DIR}" @@ -133,9 +159,9 @@ python3 scripts/lab_state.py acknowledge-agent | 시나리오 | 주입하는 장애 | 안내 문서 | |---|---|---| -| S1 | HTTP 500 응답 | [guides/02-scenario-s1.md](guides/02-scenario-s1.md) | -| S2 | 주문 API 지연 | [guides/03-scenario-s2.md](guides/03-scenario-s2.md) | -| S3 | Blob 읽기 권한 제거 | [guides/04-scenario-s3.md](guides/04-scenario-s3.md) | +| S1 | HTTP 500 응답 | [S1 — HTTP 500 장애](../sre-agent-scenario-http-500/index.md) | +| S2 | 주문 API 지연 | [S2 — 응답 지연](../sre-agent-scenario-latency/index.md) | +| S3 | Blob 읽기 권한 제거 | [S3 — Blob 권한 장애](../sre-agent-scenario-blob-permission/index.md) | ## 결과 확인 @@ -143,7 +169,7 @@ python3 scripts/lab_state.py acknowledge-agent app/.venv/bin/python scripts/score.py --evidence-root evidence ``` -채점 기준, 사람이 채워야 하는 판정, 종합 판정 해석은 [guides/05-results.md](guides/05-results.md)에 있습니다. +채점 기준, 사람이 채워야 하는 판정, 종합 판정 해석은 [결과 채점](../sre-agent-results/index.md)에 있습니다. ## 정리 @@ -182,11 +208,11 @@ az group delete --subscription "${SUBSCRIPTION_ID}" --name "${RESOURCE_GROUP}" - | 증상 | 확인할 곳 | |---|---| | 배포 후 앱이 응답하지 않음 | `azd env get-value AZURE_CONTAINER_APP_FQDN`으로 FQDN을 확인한 뒤 `/healthz` 호출 | -| Agent가 스레드를 만들지 않음 | [guides/01-agent-setup.md](guides/01-agent-setup.md)의 incident platform·응답 계획 확인 | +| Agent가 스레드를 만들지 않음 | [에이전트 설정](../sre-agent-event-lab-setup/index.md)의 incident platform·응답 계획 확인 | | 경고가 발생하지 않음 | 시나리오 문서의 "복구 확인" 절 | -| 채점이 `INCOMPLETE` | [guides/05-results.md](guides/05-results.md)의 수동 판정 절 | +| 채점이 `INCOMPLETE` | [결과 채점](../sre-agent-results/index.md)의 수동 판정 절 | -정적 임계값 대신 Dynamic Threshold로 확장하는 설계는 [dynamic-thresholds.md](dynamic-thresholds.md)에 정리해 두었습니다. +정적 임계값 대신 Dynamic Threshold로 확장하는 설계는 [Dynamic Thresholds 확장 설계](../../../guides/azure-monitor/sre-agent-dynamic-thresholds/index.md)에 정리해 두었습니다. ## 공식 자료 diff --git a/monitor/sre-agent-event-lab/guides/05-results.md b/docs/labs/azure-monitor/sre-agent-results/index.md similarity index 86% rename from monitor/sre-agent-event-lab/guides/05-results.md rename to docs/labs/azure-monitor/sre-agent-results/index.md index 9acb66c6..f34b272f 100644 --- a/monitor/sre-agent-event-lab/guides/05-results.md +++ b/docs/labs/azure-monitor/sre-agent-results/index.md @@ -1,3 +1,28 @@ +--- +services: +- azure-monitor +official_sources: +- title: Azure Monitor documentation + url: https://learn.microsoft.com/azure/azure-monitor/ +document_type: lab +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 90 +estimated_time: 60m +cost: paid +cleanup_required: true +title: SRE Agent 실습 결과와 채점 +description: 이벤트 기반 장애 분석 실습의 결과를 읽고 채점합니다. +technologies: +- python +tags: +- ai-agents +- monitoring +- diagnostics +--- + # 05. 결과 읽기와 채점 수집한 근거만으로 세 시나리오를 채점하고, 사람이 판단해야 하는 부분을 남김없이 드러내는 단계입니다. @@ -11,7 +36,7 @@ ## 실행 명령 ```bash -cd monitor/sre-agent-event-lab +cd samples/azure-monitor/source-material/sre-agent-event-lab app/.venv/bin/python scripts/score.py --evidence-root evidence ``` @@ -93,4 +118,4 @@ python3 scripts/generate_notifications.py \ azd down --purge ``` -정리 훅이 무엇을 지우는지, 확인 프롬프트에서 취소하면 무엇이 남는지는 [README의 정리 절](../README.md)에 있습니다. +정리 훅이 무엇을 지우는지, 확인 프롬프트에서 취소하면 무엇이 남는지는 [README의 정리 절](../sre-agent-event-lab/index.md)에 있습니다. diff --git a/monitor/sre-agent-event-lab/guides/04-scenario-s3.md b/docs/labs/azure-monitor/sre-agent-scenario-blob-permission/index.md similarity index 93% rename from monitor/sre-agent-event-lab/guides/04-scenario-s3.md rename to docs/labs/azure-monitor/sre-agent-scenario-blob-permission/index.md index b71264d9..c1158e46 100644 --- a/monitor/sre-agent-event-lab/guides/04-scenario-s3.md +++ b/docs/labs/azure-monitor/sre-agent-scenario-blob-permission/index.md @@ -1,10 +1,35 @@ +--- +services: +- azure-monitor +official_sources: +- title: Azure Monitor documentation + url: https://learn.microsoft.com/azure/azure-monitor/ +document_type: lab +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 90 +estimated_time: 60m +cost: paid +cleanup_required: true +title: SRE Agent Blob 권한 장애 시나리오 +description: Blob 권한을 제거하고 SRE Agent의 권한 문제 진단을 확인합니다. +technologies: +- python +tags: +- ai-agents +- authorization +- monitoring +--- + # 04. S3 — Blob 권한 제거 장애 워크로드의 Blob 읽기 권한을 지우고, 애플리케이션 코드가 아니라 권한이 원인임을 Agent가 구분해 내는지 봅니다. ## 시작 조건 -- [03-scenario-s2.md](03-scenario-s2.md)의 S2가 복구되고 캡처가 `conclusion`으로 끝났습니다. +- [S2 — 응답 지연](../sre-agent-scenario-latency/index.md)의 S2가 복구되고 캡처가 `conclusion`으로 끝났습니다. - `evidence/state.json`에 `s2_recovered`와 `s2_captured`가 있습니다. - 다른 시나리오의 실행이 `running`이나 `failed`로 남아 있지 않습니다. S1을 다시 돌리다 실패한 채로 두면 S2 기록이 멀쩡해도 S3는 거부됩니다. - 역할 할당을 만들고 지울 권한이 그대로 있습니다. @@ -16,7 +41,7 @@ Codespaces에서 이 저장소를 열었다면 `az login`을 마친 뒤 아래 한 줄로 이번 실습에 필요한 값이 모두 셸에 준비됩니다. 여기서부터 마지막 `record-capture`까지는 같은 셸에서 실행합니다. ```bash -cd monitor/sre-agent-event-lab +cd samples/azure-monitor/source-material/sre-agent-event-lab source ./scripts/lab-env.sh ``` @@ -290,4 +315,4 @@ az role assignment create \ ## 다음 단계 -수집한 근거를 채점합니다: [05-results.md](05-results.md) +수집한 근거를 채점합니다: [결과 채점](../sre-agent-results/index.md) diff --git a/monitor/sre-agent-event-lab/guides/02-scenario-s1.md b/docs/labs/azure-monitor/sre-agent-scenario-http-500/index.md similarity index 92% rename from monitor/sre-agent-event-lab/guides/02-scenario-s1.md rename to docs/labs/azure-monitor/sre-agent-scenario-http-500/index.md index a3eeea9b..66934432 100644 --- a/monitor/sre-agent-event-lab/guides/02-scenario-s1.md +++ b/docs/labs/azure-monitor/sre-agent-scenario-http-500/index.md @@ -1,10 +1,35 @@ +--- +services: +- azure-monitor +official_sources: +- title: Azure Monitor documentation + url: https://learn.microsoft.com/azure/azure-monitor/ +document_type: lab +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 90 +estimated_time: 60m +cost: paid +cleanup_required: true +title: SRE Agent HTTP 500 장애 시나리오 +description: HTTP 500 장애를 주입하고 SRE Agent의 탐지와 조사를 확인합니다. +technologies: +- python +tags: +- ai-agents +- troubleshooting +- monitoring +--- + # 02. S1 — HTTP 500 장애 주문 API가 500을 반환하도록 바꾸고, Azure Monitor Sev2 경고가 발생한 뒤 Agent가 원인을 짚어내는지 봅니다. ## 시작 조건 -- [01-agent-setup.md](01-agent-setup.md)를 마쳤고 `evidence/state.json`에 `baseline_passed`와 `agent_setup_acknowledged`가 기록되어 있습니다. +- [에이전트 설정](../sre-agent-event-lab-setup/index.md)을 마쳤고 `evidence/state.json`에 `baseline_passed`와 `agent_setup_acknowledged`가 기록되어 있습니다. - 현재 활성 구독이 azd 환경의 구독과 같습니다. 이 두 가지는 `evidence/state.json`을 통해 강제됩니다. 아래 "수동 실행"의 첫 단계인 `lab_state.py begin-run`이 순서와 중복 실행을 함께 확인합니다. @@ -20,7 +45,7 @@ Codespaces에서 이 저장소를 열었다면 `az login`을 마친 뒤 아래 한 줄로 이번 실습에 필요한 값이 모두 셸에 준비됩니다. 여기서부터 마지막 `record-capture`까지는 같은 셸에서 실행합니다. ```bash -cd monitor/sre-agent-event-lab +cd samples/azure-monitor/source-material/sre-agent-event-lab source ./scripts/lab-env.sh ``` @@ -324,9 +349,9 @@ fi | `conclusion-missing` | 조사는 했지만 결론에 도달하지 못했습니다 | 막힙니다 | | `thread-not-created` | 경고가 Agent에 도달하지 못했습니다 | 막힙니다 | -성공은 `conclusion` 하나뿐입니다. 부분 성공은 결론이 나왔지만 내용이 얕은 경우이며, 이때도 상태는 `conclusion`이고 점수는 [05-results.md](05-results.md)의 채점에서 갈립니다. 빈 성공 화면을 만들지 않고 누락 상태와 마지막 확인 시각을 그대로 그림에 남깁니다. +성공은 `conclusion` 하나뿐입니다. 부분 성공은 결론이 나왔지만 내용이 얕은 경우이며, 이때도 상태는 `conclusion`이고 점수는 [결과 채점](../sre-agent-results/index.md)에서 갈립니다. 빈 성공 화면을 만들지 않고 누락 상태와 마지막 확인 시각을 그대로 그림에 남깁니다. -`thread-not-created`가 나오면 [01-agent-setup.md](01-agent-setup.md)의 incident platform과 응답 계획부터 다시 확인합니다. +`thread-not-created`가 나오면 [에이전트 설정](../sre-agent-event-lab-setup/index.md)의 incident platform과 응답 계획부터 다시 확인합니다. ## 복구 확인 @@ -347,4 +372,4 @@ azd env get-value AZURE_CONTAINER_APP_FQDN ## 다음 단계 -지연 장애로 넘어갑니다: [03-scenario-s2.md](03-scenario-s2.md) +지연 장애로 넘어갑니다: [S2 — 응답 지연](../sre-agent-scenario-latency/index.md) diff --git a/monitor/sre-agent-event-lab/guides/03-scenario-s2.md b/docs/labs/azure-monitor/sre-agent-scenario-latency/index.md similarity index 93% rename from monitor/sre-agent-event-lab/guides/03-scenario-s2.md rename to docs/labs/azure-monitor/sre-agent-scenario-latency/index.md index f2404445..2b505e2f 100644 --- a/monitor/sre-agent-event-lab/guides/03-scenario-s2.md +++ b/docs/labs/azure-monitor/sre-agent-scenario-latency/index.md @@ -1,10 +1,35 @@ +--- +services: +- azure-monitor +official_sources: +- title: Azure Monitor documentation + url: https://learn.microsoft.com/azure/azure-monitor/ +document_type: lab +status: needs-review +verification_status: needs-review +sources_checked_at: 2026-09-12 +last_verified: null +review_cycle_days: 90 +estimated_time: 60m +cost: paid +cleanup_required: true +title: SRE Agent 응답 지연 장애 시나리오 +description: 응답 지연을 주입하고 SRE Agent의 원인 분석을 확인합니다. +technologies: +- python +tags: +- ai-agents +- latency +- monitoring +--- + # 03. S2 — 응답 지연 장애 주문 API가 느려지게 만들고, p95 지연 경고를 받은 Agent가 오류 없는 장애를 설명하는지 봅니다. 500이 하나도 없기 때문에 S1보다 근거를 고르기 어렵습니다. ## 시작 조건 -- [02-scenario-s1.md](02-scenario-s1.md)의 S1이 복구되고 캡처가 `conclusion`으로 끝났습니다. +- [S1 — HTTP 500 장애](../sre-agent-scenario-http-500/index.md)의 S1이 복구되고 캡처가 `conclusion`으로 끝났습니다. - `evidence/state.json`에 `s1_recovered`와 `s1_captured`가 있습니다. - 다른 시나리오의 실행이 `running`이나 `failed`로 남아 있지 않습니다. 하나라도 남아 있으면 S2도 거부되고, 거부 메시지가 막고 있는 시나리오와 해결 명령을 알려 줍니다. - 워크로드가 정상이고 S1 경고가 해제되어 있습니다. @@ -16,7 +41,7 @@ S1과 같은 구조이며, 바뀌는 것은 주입 값과 부하 조건뿐입니 Codespaces에서 이 저장소를 열었다면 `az login`을 마친 뒤 아래 한 줄로 이번 실습에 필요한 값이 모두 셸에 준비됩니다. 여기서부터 마지막 `record-capture`까지는 같은 셸에서 실행합니다. ```bash -cd monitor/sre-agent-event-lab +cd samples/azure-monitor/source-material/sre-agent-event-lab source ./scripts/lab-env.sh ``` @@ -300,4 +325,4 @@ fi ## 다음 단계 -권한 장애로 넘어갑니다: [04-scenario-s3.md](04-scenario-s3.md) +권한 장애로 넘어갑니다: [S3 — Blob 권한 장애](../sre-agent-scenario-blob-permission/index.md) diff --git a/aisearch/custom_vectorization/ref_a10_vs_t4_embedding_benchmark.md b/docs/research/azure-ai-search/a10-vs-t4-embedding-benchmark/index.md similarity index 94% rename from aisearch/custom_vectorization/ref_a10_vs_t4_embedding_benchmark.md rename to docs/research/azure-ai-search/a10-vs-t4-embedding-benchmark/index.md index cfcc0649..320847ae 100644 --- a/aisearch/custom_vectorization/ref_a10_vs_t4_embedding_benchmark.md +++ b/docs/research/azure-ai-search/a10-vs-t4-embedding-benchmark/index.md @@ -1,8 +1,29 @@ +--- +services: +- azure-ai-search +official_sources: +- title: Azure AI Search documentation + url: https://learn.microsoft.com/azure/search/ +document_type: research +status: current +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: 'A10 vs T4: 처리량과 비용으로 고르는 임베딩 GPU' +description: BGE-m3-ko와 TEI를 사용한 실측입니다. 단건 지연, 배치 처리량, 청크 길이를 나눠 보고 GPU 선택과 증설의 기준을 비교합니다. +featured: true +technologies: +- python +tags: +- benchmarking +- performance +published_at: 2026-07-23 +--- + # A10 vs T4 임베딩 추론 벤치마크: BGE-m3-ko + TEI (Azure Spot VM) **AI Search + Agent 구성(Indexer 미사용, Push API)에서 TEI로 서빙한 dragonkue/BGE-m3-ko의 추론 성능을 A10·T4에서 실측 비교** -> 관련 문서: [GPU vLLM RAG 가이드](03_gpu_vllm_rag_guide.md) | [Custom 임베딩 적재 가이드](01_custom_embedding_guide.md) | [BGE-M3 vs Qwen3 품질 비교](ref_bge_m3_vs_qwen3_comparison.md) +> 관련 문서: [GPU vLLM RAG 가이드](../../../guides/azure-ai-search/gpu-vllm-rag/index.md) | [Custom 임베딩 적재 가이드](../../../guides/azure-ai-search/custom-embedding-ingestion/index.md) | [BGE-M3 vs Qwen3 품질 비교](../bge-m3-vs-qwen3-embedding/index.md) > > 작성일: 2026-07-22 @@ -18,10 +39,10 @@ | 최대 처리량 (~1,000자 청크) | 23 texts/s | **130 texts/s** | **5.5×** | | 1M 청크(500자) 적재 소요 | 4.5 hr | **0.9 hr** | 5× | -- **쿼리 경로(단건 추론)**: 양쪽 모두 single-digit ms — latency-bound 구간으로, 전체 검색 응답 시간에서 차지하는 비중이 상대적으로 작음. QPS 스윕 실측 기준 T4 1대는 **~200 QPS(40자 쿼리)까지 p50 ~6ms 유지, 초과 시 큐잉 지연 — 안전 운용 상한 ~150 QPS/대** → [증설 기준](#쿼리-트래픽-증가-시-증설-기준--qps-스윕-실측) +- **쿼리 경로(단건 추론)**: 양쪽 모두 single-digit ms — latency-bound 구간으로, 전체 검색 응답 시간에서 차지하는 비중이 상대적으로 작음. QPS 스윕 실측 기준 T4 1대는 **~200 QPS(40자 쿼리)까지 p50 ~6ms 유지, 초과 시 큐잉 지연 — 안전 운용 상한 ~150 QPS/대** → [증설 기준](#qps) - **적재 경로(배치 추론)**: throughput-bound 구간에서 **본 테스트 환경 기준 A10 처리량이 T4 대비 약 5배 수준으로 측정**. T4는 ~60 texts/s 부근에서 GPU 사용률·전력이 상한에 도달했으며, 본 벤치마크 조건에서는 batch size/concurrency 조정만으로는 유의미한 추가 향상을 확인하지 못함 -- 공개 스펙 교차검증 — FP16 Tensor TFLOPS비 1.9×, Flash Attention 지원 차이, 메모리 대역폭 2×가 **주요 원인으로 추정**되며, 공개 스펙 기반 예측 범위(4.3~5.8×)와 실측 결과의 방향성이 일치 → [교차검증 섹션](#교차검증-공개-스펙벤치마크와의-정합성) -- 대체 전략: A10 수급 불가 시, 본 측정 조건 기준 **A10 × N ≈ T4 × 5N**(500자) / **× 6N**(1,000자) 수준으로 대체 산정 — 최소 SKU(NC4as) × N 구성 시 VM 비용 기준 등가 처리량 확보 가능 → [대체 전략](#대체-전략-a10-수급-불가-시-t4-스케일아웃) +- 공개 스펙 교차검증 — FP16 Tensor TFLOPS비 1.9×, Flash Attention 지원 차이, 메모리 대역폭 2×가 **주요 원인으로 추정**되며, 공개 스펙 기반 예측 범위(4.3~5.8×)와 실측 결과의 방향성이 일치 → [교차검증 섹션](#_4) +- 대체 전략: A10 수급 불가 시, 본 측정 조건 기준 **A10 × N ≈ T4 × 5N**(500자) / **× 6N**(1,000자) 수준으로 대체 산정 — 최소 SKU(NC4as) × N 구성 시 VM 비용 기준 등가 처리량 확보 가능 → [대체 전략](#a10-t4) > ⚠️ **측정 범위**: 본 측정은 **임베딩 생성 구간(TEI `/embed`)만 대상**이며, Azure AI Search 문서 업로드 및 인덱싱 시간은 포함하지 않는다. 실제 End-to-End 적재 시간은 Search 서비스 SKU, 문서 크기, 배치 크기 및 서비스 부하에 따라 달라질 수 있다. 또한 측정값은 모델, 텍스트 길이·토큰 분포, 서빙 엔진 버전, 드라이버 버전, 배치 설정 및 운영 환경에 따라 달라질 수 있다. @@ -45,7 +66,7 @@ - NVadsA10v5 시리즈 중 **full-GPU 프로필(NV36ads, A10-24Q 24GB 단독 점유)** 사용. NV6/12/18ads는 GPU fractional 파티션(1/6~1/2) — 본 결과 적용 불가 - T4는 TEI **Turing 전용 이미지(`turing-1.8`) 필수**. Turing은 TEI [experimental 지원](https://github.com/huggingface/text-embeddings-inference#docker-images), Flash Attention 기본 비활성화 - NVIDIA 드라이버는 Azure VM extension `NvidiaGpuDriverLinux`(Microsoft.HpcCompute)로 설치 — GRID/CUDA 자동 판별. A10은 GRID 드라이버 필수 (수동 CUDA 드라이버 설치 시 vGPU 라이선스 실패) -- 실행 스크립트: [bench/](bench/) +- 실행 스크립트: [bench/](https://github.com/hellices/devguidesample/tree/main/samples/azure-ai-search/source-material/custom_vectorization/bench) > ⚠️ **vCPU 격차(4 vs 36)의 결과 왜곡 여부 교차검증 완료.** T4 VM을 NC16as_T4_v3(16 vCPU)로 리사이즈 후 동일 벤치마크 5라운드 재실행: > - 500자 최대 처리량 61.0 texts/s (4 vCPU: 61.3) / 1,000자 22.6 (4 vCPU: 23.4) / 단건 5.86ms (4 vCPU: 5.59ms) — **vCPU 4배 증가에도 측정 오차(±3%) 이내** @@ -223,7 +244,7 @@ Indexer 미사용 구성의 임베딩 호출 경로 2종: ### 쿼리 트래픽 증가 시 증설 기준 — QPS 스윕 실측 -T4 1대(NC4as_T4_v3, TEI turing-1.8, BGE-m3-ko)에 오픈루프 고정 도착률로 단건 쿼리(~40자)를 주입하며 포화 지점을 실측 (30s/단계, 3라운드 스윕 — [bench_qps_sweep.py](bench/bench_qps_sweep.py)): +T4 1대(NC4as_T4_v3, TEI turing-1.8, BGE-m3-ko)에 오픈루프 고정 도착률로 단건 쿼리(~40자)를 주입하며 포화 지점을 실측 (30s/단계, 3라운드 스윕 — [bench_qps_sweep.py](https://github.com/hellices/devguidesample/blob/main/samples/azure-ai-search/source-material/custom_vectorization/bench/bench_qps_sweep.py)): | 도착률 (QPS) | p50 | p95 | p99 | GPU util | 판정 | |-------------|-----|-----|-----|----------|------| @@ -286,7 +307,7 @@ T4 1대(NC4as_T4_v3, TEI turing-1.8, BGE-m3-ko)에 오픈루프 고정 도착률 **운영 파라미터:** - Push 파이프라인 최적점: T4 = batch 8~32 / conc 4 (초과 시 역효과), A10 = batch 32~64 / conc 4~8 -- 1M+ 청크 적재: A10 1대 ≈ T4 5대 — A10 수급 불가 시 T4 × 5N으로 대체 산정 → [대체 전략](#대체-전략-a10-수급-불가-시-t4-스케일아웃) 참고 +- 1M+ 청크 적재: A10 1대 ≈ T4 5대 — A10 수급 불가 시 T4 × 5N으로 대체 산정 → [대체 전략](#a10-t4) 참고 - Spot eviction 대비: Push 파이프라인에 retry + checkpoint(마지막 성공 문서 ID) 필수. 본 측정 중 eviction 미발생 - **A10 Spot 수급 주의**: NVadsA10_v5 Spot은 수급 변동성이 크며 특정 시간대에는 할당이 실패할 수 있다. 운영 환경에서는 PAYG 또는 예약 인스턴스(RI/Savings Plan)와의 조합 검토를 권장 diff --git a/aisearch/custom_vectorization/ref_bge_m3_vs_qwen3_comparison.md b/docs/research/azure-ai-search/bge-m3-vs-qwen3-embedding/index.md similarity index 96% rename from aisearch/custom_vectorization/ref_bge_m3_vs_qwen3_comparison.md rename to docs/research/azure-ai-search/bge-m3-vs-qwen3-embedding/index.md index 3f4e2198..bd20a937 100644 --- a/aisearch/custom_vectorization/ref_bge_m3_vs_qwen3_comparison.md +++ b/docs/research/azure-ai-search/bge-m3-vs-qwen3-embedding/index.md @@ -1,3 +1,24 @@ +--- +services: +- azure-ai-search +official_sources: +- title: Azure AI Search documentation + url: https://learn.microsoft.com/azure/search/ +document_type: research +status: current +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: BGE-M3와 Qwen3 임베딩 품질 비교 +description: BGE-M3와 Qwen3-Embedding 모델의 벡터 검색 품질을 비교합니다. +technologies: +- python +- rag +tags: +- benchmarking +- performance +published_at: 2026-05-22 +--- + # BGE-M3 vs Qwen3-Embedding-0.6B 벡터 검색 품질 비교 **동일한 Azure AI Search Integrated Vectorization 파이프라인에서 BGE-M3와 Qwen3-Embedding-0.6B의 벡터 검색 품질을 비교한 결과** @@ -62,14 +83,14 @@ Alibaba Qwen 팀에서 개발한 경량 임베딩 모델. Instruction-tuned 방 | 항목 | 값 | |------|-----| -| AI Search | `ais-aiplay-krc-01` (Standard, koreacentral) | +| AI Search | `search-example-koreacentral-01` (Standard, koreacentral) | | BGE-M3 인덱스 | `sample-vector-idx` (영어) / `samsung-bge-idx` (한글) | | Qwen3 인덱스 | `qwen3-vector-idx` (영어) / `samsung-qwen-idx` (한글) | | 데이터 소스 (영어) | `sample-docs-ds` (5건 Azure 서비스 설명 문서) | | 데이터 소스 (한글) | `samsung-docs` (7건 삼성 갤럭시 제품 소개) | | 벡터 차원 | 1024 (동일) | | 알고리즘 | HNSW cosine (동일 파라미터) | -| HTTPS 엔드포인트 | `ca-bge-m3-embed` / `ca-qwen3-embed` (Container Apps) | +| HTTPS 엔드포인트 | `ca-embedding-example-01` / `ca-qwen3-example-01` (Container Apps) | | Instruction 사용 | 두 모델 모두 미사용 (동일 조건) | ### 테스트 문서 목록 diff --git a/aisearch/custom_vectorization/ref_chunking_strategies_research.md b/docs/research/azure-ai-search/rag-chunking-strategies/index.md similarity index 97% rename from aisearch/custom_vectorization/ref_chunking_strategies_research.md rename to docs/research/azure-ai-search/rag-chunking-strategies/index.md index 93168dfe..84ad2b92 100644 --- a/aisearch/custom_vectorization/ref_chunking_strategies_research.md +++ b/docs/research/azure-ai-search/rag-chunking-strategies/index.md @@ -1,8 +1,28 @@ +--- +services: +- azure-ai-search +official_sources: +- title: Azure AI Search documentation + url: https://learn.microsoft.com/azure/search/ +document_type: research +status: current +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: RAG Chunking 전략 비교 +description: RAG 검색을 위한 청킹 전략과 선택 기준을 비교합니다. +technologies: +- rag +tags: +- architecture +- performance +published_at: 2026-05-22 +--- + # RAG Chunking 전략 리서치 보고서 **Azure AI Search Custom Web API 파이프라인에서 활용 가능한 최신 Chunking 전략, 프레임워크, 논문 정리** -> 작성일: 2026-05-22 | 관련 문서: [02_custom_vectorization.md](02_custom_vectorization.md) +> 작성일: 2026-05-22 | 관련 문서: [Custom Web API 벡터화 가이드](../../../guides/azure-ai-search/custom-web-api-vectorization/index.md) --- diff --git a/hdinsight/kafka-catchup-sku-fetch-benchmark.md b/docs/research/azure-hdinsight/kafka-catchup-sku-fetch-benchmark/index.md similarity index 96% rename from hdinsight/kafka-catchup-sku-fetch-benchmark.md rename to docs/research/azure-hdinsight/kafka-catchup-sku-fetch-benchmark/index.md index 85ab5261..77cdf131 100644 --- a/hdinsight/kafka-catchup-sku-fetch-benchmark.md +++ b/docs/research/azure-hdinsight/kafka-catchup-sku-fetch-benchmark/index.md @@ -1,6 +1,26 @@ +--- +services: +- azure-hdinsight +official_sources: +- title: Azure HDInsight documentation + url: https://learn.microsoft.com/azure/hdinsight/ +document_type: research +status: current +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: HDInsight Kafka Catch-up 성능 벤치마크 +description: HDInsight Kafka broker SKU와 consumer fetch size에 따른 catch-up 성능을 비교합니다. +technologies: +- kafka +tags: +- benchmarking +- performance +published_at: 2026-07-31 +--- + # HDInsight Kafka lag/catch-up 벤치마크 — broker SKU / consumer fetch size -- Date: 2026-07-16 | Region: japaneast | Cluster: `krafton-kafka-hdi-68944` (HDInsight Kafka, broker 3) +- Date: 2026-07-16 | Region: japaneast | Cluster: `hdi-example-japaneast-01` (HDInsight Kafka, broker 3) - Scope: partition·consumer 수 고정 조건에서 broker VM SKU, consumer fetch size 2개 변수만 스윕. 특정 축소 구성·특정 배포 인스턴스 한정 관측값이며 일반화 결론 아님. - Base SKU 기준: 고객 운영 worker = D8a_v4 (8 vCPU / 32GB) × 18. 본 테스트 base를 이에 맞춰 D8(32GB)로 두고 D16(64GB)으로의 상향 효과를 주 비교축으로 함(D4는 하위 참고점). @@ -246,12 +266,12 @@ partition 수 = consumer pod 수를 고정한다는 고객 제약 하에서 brok ## 12. Teardown -- Cluster `krafton-kafka-hdi-68944`: deleted (D4·D8·D16 각 테스트 후 재생성/삭제). +- Cluster `hdi-example-japaneast-01`: deleted (D4·D8·D16 각 테스트 후 재생성/삭제). - VM `vm-kafka-producer`, `vm-kafka-loadgen`, `vm-jumpbox`, `vm-monitoring`: deallocated/삭제. ```bash -az hdinsight delete -g rg-krafton-kafka-dev-jpe -n krafton-kafka-hdi-68944 --yes -# 전체 정리: az group delete -n rg-krafton-kafka-dev-jpe --yes +az hdinsight delete -g rg-example-japaneast-01 -n hdi-example-japaneast-01 --yes +# 전체 정리: az group delete -n rg-example-japaneast-01 --yes ``` - 재현 아티팩트: worker SKU별 private template(`hdi-priv-Standard_D4ads_v5.json`, `hdi-priv-Standard_D8ads_v5.json`, `hdi-priv-Standard_D16ads_v5.json`), 연속 파이프라인 harness(producer VM 8-parallel flood + consumer VM single perf-consumer, fetch size 스윕). @@ -265,8 +285,8 @@ az hdinsight delete -g rg-krafton-kafka-dev-jpe -n krafton-kafka-hdi-68944 --yes ## 관련 문서 -- [`../monitor/hdinsight-kafka-monitoring.md`](../monitor/hdinsight-kafka-monitoring.md) — HDInsight Kafka 모니터링 개요(Azure Monitor·Log Analytics·Ambari·진단설정). -- [`../monitor/hdinsight-kafka-prometheus-grafana.md`](../monitor/hdinsight-kafka-prometheus-grafana.md) — Prometheus + Grafana broker JMX·kafka-exporter(partition별 lag) 대시보드 구성. +- [HDInsight Kafka 모니터링 방식 비교](../../azure-monitor/hdinsight-kafka-monitoring-options/index.md) — Azure Monitor·Log Analytics·Ambari·진단 설정 개요. +- [Prometheus + Grafana 대시보드 구성](../../../guides/azure-monitor/hdinsight-kafka-prometheus-grafana/index.md) — broker JMX·kafka-exporter(partition별 lag) 대시보드 구성. --- diff --git a/monitor/assets/official/dynamic-threshold-preview-chart.png b/docs/research/azure-monitor/dynamic-thresholds-brief/images/dynamic-threshold-preview-chart.png similarity index 100% rename from monitor/assets/official/dynamic-threshold-preview-chart.png rename to docs/research/azure-monitor/dynamic-thresholds-brief/images/dynamic-threshold-preview-chart.png diff --git a/monitor/azure-monitor-dynamic-thresholds-brief.md b/docs/research/azure-monitor/dynamic-thresholds-brief/index.md similarity index 91% rename from monitor/azure-monitor-dynamic-thresholds-brief.md rename to docs/research/azure-monitor/dynamic-thresholds-brief/index.md index 515c124c..7e213d28 100644 --- a/monitor/azure-monitor-dynamic-thresholds-brief.md +++ b/docs/research/azure-monitor/dynamic-thresholds-brief/index.md @@ -1,3 +1,23 @@ +--- +services: +- azure-monitor +official_sources: +- title: Azure Monitor documentation + url: https://learn.microsoft.com/azure/azure-monitor/ +document_type: research +status: current +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: Azure Monitor Dynamic Thresholds 도입 분석 +description: Dynamic Thresholds 기능 변화와 도입 시점을 분석합니다. +technologies: +- rest-api +tags: +- monitoring +- reliability +published_at: 2026-08-17 +--- + # Azure Monitor Dynamic Thresholds: What Changes and When to Adopt Dynamic Thresholds replace a single manually chosen alert value with a machine-learned @@ -7,7 +27,7 @@ dynamic ones without losing existing safety guarantees. ## Preview chart -[![Screenshot that shows a metric alert preview chart with dynamic threshold: a blue line for the measured metric, a blue shaded allowed range, and red dots marking values outside that range.](assets/official/dynamic-threshold-preview-chart.png)](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-dynamic-thresholds) +[![Screenshot that shows a metric alert preview chart with dynamic threshold: a blue line for the measured metric, a blue shaded allowed range, and red dots marking values outside that range.](images/dynamic-threshold-preview-chart.png)](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-dynamic-thresholds) *Source: [Create a Log Search alert rule with dynamic threshold](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-dynamic-thresholds).* diff --git a/monitor/assets/images/hdinsight_monitor_integration_1.png b/docs/research/azure-monitor/hdinsight-kafka-monitoring-options/images/hdinsight_monitor_integration_1.png similarity index 100% rename from monitor/assets/images/hdinsight_monitor_integration_1.png rename to docs/research/azure-monitor/hdinsight-kafka-monitoring-options/images/hdinsight_monitor_integration_1.png diff --git a/monitor/hdinsight-kafka-monitoring.md b/docs/research/azure-monitor/hdinsight-kafka-monitoring-options/index.md similarity index 92% rename from monitor/hdinsight-kafka-monitoring.md rename to docs/research/azure-monitor/hdinsight-kafka-monitoring-options/index.md index 070b0ec8..a446f30d 100644 --- a/monitor/hdinsight-kafka-monitoring.md +++ b/docs/research/azure-monitor/hdinsight-kafka-monitoring-options/index.md @@ -1,3 +1,24 @@ +--- +services: +- azure-monitor +official_sources: +- title: Azure Monitor documentation + url: https://learn.microsoft.com/azure/azure-monitor/ +document_type: research +status: current +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: HDInsight Kafka 모니터링 방식 비교 +description: Ambari, Azure Monitor, JMX Exporter와 Kafka Exporter 기반 모니터링을 비교합니다. +technologies: +- kafka +- prometheus +tags: +- monitoring +- observability +published_at: 2026-07-10 +--- + # HDInsight Kafka 모니터링 방식 비교 (Ambari / Monitor Integration / JMX Exporter / Kafka Exporter) 지표별(브로커 리소스, Consumer Lag, 클러스터 상태, JVM 내부) 수집 경로·저장 위치가 상이 → 단일 도구로 미커버. @@ -7,7 +28,7 @@ > - **노드 리소스 · 클러스터 상태 · 장기 로그** → Ambari + Monitor Integration (HDInsight 기본, 추가 설치 없음) > - **브로커/토픽 처리량 · JVM internals · Consumer Group Lag** → HDInsight 기본 도구 **관측 불가** > - → **JMX Exporter + Kafka Exporter (+Prometheus/Grafana)** 도입 권장 -> - 설치·대시보드 구성: **[hdinsight-kafka-prometheus-grafana.md](./hdinsight-kafka-prometheus-grafana.md)** +> - 설치·대시보드 구성: **[Prometheus + Grafana 구성 가이드](../../../guides/azure-monitor/hdinsight-kafka-prometheus-grafana/index.md)** ## 1. 관측 항목 → 조회 도구 매핑 @@ -45,7 +66,7 @@ O = 기본 제공 / △ = 조건부·간접 제공 / X = 제공 안 함 - **활성화**: Azure Portal → 클러스터 → **Monitoring > Monitor integration** → Log Analytics 워크스페이스 연결 - ![HDInsight Monitor integration 활성화 화면](./assets/images/hdinsight_monitor_integration_1.png) + ![HDInsight Monitor integration 활성화 화면](images/hdinsight_monitor_integration_1.png) - **주요 테이블**: @@ -77,7 +98,7 @@ O = 기본 제공 / △ = 조건부·간접 제공 / X = 제공 안 함 ### JMX Exporter / Kafka Exporter (오픈소스, 직접 구성) — 권장 HDInsight 기본 미제공 → 수동 설치. 둘 다 **Prometheus 스크래핑 + Grafana 시각화** 조합. -설치·대시보드: **[hdinsight-kafka-prometheus-grafana.md](./hdinsight-kafka-prometheus-grafana.md)** +설치·대시보드: **[Prometheus + Grafana 구성 가이드](../../../guides/azure-monitor/hdinsight-kafka-prometheus-grafana/index.md)** - **JMX Exporter** — 브로커 JVM MBean을 Prometheus 형식으로 노출. **JVM internals**(Heap/GC/스레드) + **브로커/토픽/파티션 지표**(BytesIn/Out, Under-Replicated Partitions 등). @@ -217,7 +238,7 @@ HDInsightKafkaMetrics > **권장 목표 상태**: Monitor Integration으로 노드/클러스터 baseline 확보 + > **브로커/토픽 상세·JVM internals·Consumer Lag은 JMX/Kafka Exporter로 관측**. -> → 설치·대시보드: **[hdinsight-kafka-prometheus-grafana.md](./hdinsight-kafka-prometheus-grafana.md)** +> → 설치·대시보드: **[Prometheus + Grafana 구성 가이드](../../../guides/azure-monitor/hdinsight-kafka-prometheus-grafana/index.md)** --- @@ -279,5 +300,5 @@ HDInsightKafkaMetrics - JMX Exporter — [prometheus/jmx_exporter (GitHub)](https://github.com/prometheus/jmx_exporter) - Kafka Exporter — [danielqsj/kafka_exporter (GitHub)](https://github.com/danielqsj/kafka_exporter) -> 이 문서는 `rg-krafton-kafka-dev-jpe` 배포(`krafton-kafka-hdi-68944`) 기준 실측 기록. +> 이 문서는 `rg-example-japaneast-01` 배포(`hdi-example-japaneast-01`) 기준 실측 기록. > 리소스명·IP는 환경에 맞게 치환. diff --git a/monitor/sre-agent-event-lab/assets/captures/s1-before-bridge/investigation.gif b/docs/research/azure-monitor/sre-agent-validation-results/images/investigation.gif similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s1-before-bridge/investigation.gif rename to docs/research/azure-monitor/sre-agent-validation-results/images/investigation.gif diff --git a/monitor/sre-agent-event-lab/assets/captures/s1/investigation.gif b/docs/research/azure-monitor/sre-agent-validation-results/images/s1-investigation.gif similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s1/investigation.gif rename to docs/research/azure-monitor/sre-agent-validation-results/images/s1-investigation.gif diff --git a/monitor/sre-agent-event-lab/assets/captures/s2/investigation.gif b/docs/research/azure-monitor/sre-agent-validation-results/images/s2-investigation.gif similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s2/investigation.gif rename to docs/research/azure-monitor/sre-agent-validation-results/images/s2-investigation.gif diff --git a/monitor/sre-agent-event-lab/assets/captures/s3/investigation.gif b/docs/research/azure-monitor/sre-agent-validation-results/images/s3-investigation.gif similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s3/investigation.gif rename to docs/research/azure-monitor/sre-agent-validation-results/images/s3-investigation.gif diff --git a/monitor/sre-agent-event-lab/validation-results.md b/docs/research/azure-monitor/sre-agent-validation-results/index.md similarity index 82% rename from monitor/sre-agent-event-lab/validation-results.md rename to docs/research/azure-monitor/sre-agent-validation-results/index.md index dd62be3e..0a11046f 100644 --- a/monitor/sre-agent-event-lab/validation-results.md +++ b/docs/research/azure-monitor/sre-agent-validation-results/index.md @@ -1,10 +1,31 @@ +--- +services: +- azure-monitor +official_sources: +- title: Azure Monitor documentation + url: https://learn.microsoft.com/azure/azure-monitor/ +document_type: research +status: current +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: Azure SRE Agent 실제 동작 검증 결과 +description: 세 가지 장애 시나리오에서 관측한 Azure SRE Agent 동작과 결과를 분석합니다. +technologies: +- python +tags: +- ai-agents +- monitoring +- diagnostics +published_at: 2026-08-15 +--- + # Azure SRE Agent 실제 동작 검증 결과 -> 제품 개요와 실사용 패턴은 [Azure SRE Agent 소개 자료](../azure-sre-agent.md)를 먼저 참고한다. +> 제품 개요와 실사용 패턴은 [Azure SRE Agent 소개 자료](../../../guides/azure-monitor/azure-sre-agent-overview/index.md)를 먼저 참고한다. > > 이 문서는 S1/S2/S3 시나리오에서 측정한 수치, timeline, evidence, 한계를 정리한다. > -> **기록 시점 주의.** 아래 결과는 azd 재구성 이전에 손으로 구축한 실습(2026-08-12)의 측정치다. 여기 적힌 `rg-sre-agent-event-lab-krc` 같은 리소스 그룹과 리소스 이름, Action Group + Logic App bridge는 모두 그때의 환경이고, 현재 실습의 `azd up`은 이 이름들을 만들지 않는다. 지금 실행하는 절차와 실제로 배포되는 구성은 [README](README.md)와 [guides/](guides/)를 따르고, 이 문서는 그 절차로 무엇을 관찰할 수 있었는지 보여 주는 과거 기록으로 읽는다. +> **기록 시점 주의.** 아래 결과는 azd 재구성 이전에 손으로 구축한 실습(2026-08-12)의 측정치다. 여기 적힌 `rg-sre-agent-event-lab-krc` 같은 리소스 그룹과 리소스 이름, Action Group + Logic App bridge는 모두 그때의 환경이고, 현재 실습의 `azd up`은 이 이름들을 만들지 않는다. 지금 실행하는 절차와 실제로 배포되는 구성은 [현재 실습 문서](../../../labs/azure-monitor/sre-agent-event-lab/index.md)를 따르고, 이 문서는 그 절차로 무엇을 관찰할 수 있었는지 보여 주는 과거 기록으로 읽는다. - 실행일: 2026-08-12 | 리전: Korea Central | 구독: 비식별화 - 목표: Azure Monitor 경고를 Azure SRE Agent가 자동 수신해 원인과 안전한 완화책을 올바르게 도출하는지 실증 @@ -36,9 +57,9 @@ |---|---|---| | Resource group | `rg-sre-agent-event-lab-krc` | ✅ | | Container App | `ca-sre-event-lab-vnet` / image `20260812.4` | ✅ Healthy | -| Log Analytics | `law-sre-event-lab-95933ae5` | ✅ | -| Application Insights | `appi-sre-event-lab-95933ae5` | ✅ | -| Storage | `stsrelab95933ae5`, Blob private endpoint | ✅ | +| Log Analytics | `law-sre-event-lab-example` | ✅ | +| Application Insights | `appi-sre-event-lab-example` | ✅ | +| Storage | `stexample00000000`, Blob private endpoint | ✅ | | Alert rules | S1/S2/S3 scheduled query, Sev2 | ✅ Enabled | | Event bridge | Azure Monitor Action Group → Logic App MI → SRE HTTP Trigger | ✅ | @@ -46,7 +67,7 @@ | 항목 | 값 | 상태 | |---|---|---| -| Agent | `sre-devguidesample-95933ae5` | ✅ Running | +| Agent | `sre-devguidesample-example` | ✅ Running | | Region | Korea Central | ✅ | | Model provider | Microsoft Foundry / Automatic | ✅ | | Azure resource access | 테스트 RG Reader | ✅ | @@ -110,17 +131,17 @@ **Event bridge 구성 전 — alert는 발생했지만 Agent thread가 생성되지 않음** -![S1 before bridge](assets/captures/s1-before-bridge/investigation.gif) +![S1 before bridge](images/investigation.gif) **Action Group + Logic App managed identity bridge 구성 후 — 실제 Agent 조사** -![S1 SRE Agent investigation](assets/captures/s1/investigation.gif) +![S1 SRE Agent investigation](images/s1-investigation.gif) -- [결론 frame](assets/captures/s1/07-conclusion.png) -- [실제 event timeline](assets/captures/s1/timeline.md) -- [Mermaid sequence source](assets/captures/s1/timeline.mmd) +- [결론 frame](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/07-conclusion.png) +- [실제 event timeline](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/timeline.md) +- [Mermaid sequence source](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/timeline.mmd) - 원본 evidence: `monitor/sre-agent-event-lab/evidence/s1-20260812T080606Z/` (Git 제외) -- Agent thread: `6dd0e640-d969-46cb-a976-7c81b66fcadc` +- Agent thread: `` ### Agent 분석 @@ -156,13 +177,13 @@ ### 실제 동작 캡처 -![S2 SRE Agent investigation](assets/captures/s2/investigation.gif) +![S2 SRE Agent investigation](images/s2-investigation.gif) -- [결론 frame](assets/captures/s2/07-conclusion.png) -- [실제 event timeline](assets/captures/s2/timeline.md) -- [Mermaid sequence source](assets/captures/s2/timeline.mmd) +- [결론 frame](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/07-conclusion.png) +- [실제 event timeline](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/timeline.md) +- [Mermaid sequence source](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/timeline.mmd) - 원본 evidence: `monitor/sre-agent-event-lab/evidence/s2-20260812T081539Z/` (Git 제외) -- Agent thread: `29befef3-0ef6-4ccf-aa43-af1355cff767` +- Agent thread: `` ### Agent 분석 @@ -198,13 +219,13 @@ Container App workload identity의 테스트 Blob container data-plane read 역 ### 실제 동작 캡처 -![S3 SRE Agent investigation](assets/captures/s3/investigation.gif) +![S3 SRE Agent investigation](images/s3-investigation.gif) -- [결론 frame](assets/captures/s3/07-conclusion.png) -- [실제 event timeline](assets/captures/s3/timeline.md) -- [Mermaid sequence source](assets/captures/s3/timeline.mmd) +- [결론 frame](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/07-conclusion.png) +- [실제 event timeline](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/timeline.md) +- [Mermaid sequence source](https://github.com/hellices/devguidesample/blob/main/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/timeline.mmd) - 원본 evidence: `monitor/sre-agent-event-lab/evidence/s3-20260812T084004Z/` (Git 제외) -- Agent thread: `691f9f43-31c4-4822-8d1f-5f647d45f643` +- Agent thread: `` ### Agent 분석 @@ -214,7 +235,7 @@ Container App workload identity의 테스트 Blob container data-plane read 역 - 제안 완화책: original least-privilege role 복원 후 propagation 대기; 실제 복구와 일치 - 잘못된 주장/누락: old app FQDN을 확인해 recovery가 미완료라고 판단. 실제 vnet app endpoint는 HTTP 200 -후속 보정: vnet workload의 `OTEL_SERVICE_NAME`을 `sre-event-lab-95933ae5`로 변경하고 alert/evidence query도 이 deployment-unique 값으로 제한했다. 변경 후 새 role name으로 `/api/orders` 5건, `/api/documents` 2건과 health telemetry가 분리 수집됨을 확인했다. +후속 보정: vnet workload의 `OTEL_SERVICE_NAME`을 `sre-event-lab-example`로 변경하고 alert/evidence query도 이 deployment-unique 값으로 제한했다. 변경 후 새 role name으로 `/api/orders` 5건, `/api/documents` 2건과 health telemetry가 분리 수집됨을 확인했다. ### 점수 diff --git a/aifoundry/agent_framework_2026_customer_brief.md b/docs/research/microsoft-foundry/agent-framework-2026/index.md similarity index 96% rename from aifoundry/agent_framework_2026_customer_brief.md rename to docs/research/microsoft-foundry/agent-framework-2026/index.md index 5cdba503..960f1b25 100644 --- a/aifoundry/agent_framework_2026_customer_brief.md +++ b/docs/research/microsoft-foundry/agent-framework-2026/index.md @@ -1,3 +1,24 @@ +--- +services: +- microsoft-foundry +official_sources: +- title: Microsoft Foundry documentation + url: https://learn.microsoft.com/azure/ai-foundry/ +document_type: research +status: current +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: 'Microsoft Agent Framework 2026: Python과 Go 구현 분석' +description: Microsoft Agent Framework의 Python 주요 기능과 Go 구현 선택지를 비교합니다. +technologies: +- python +- go +tags: +- ai-agents +- architecture +published_at: 2026-08-17 +--- + # Microsoft Agent Framework 2026: Python Highlights and the Go Implementation Python highlights Agent Harness and Agent Skills. The Go section briefly diff --git a/memory/agent-memory/images/02-architecture-patterns.svg b/docs/research/microsoft-foundry/agent-memory-architecture-patterns/images/02-architecture-patterns.svg similarity index 100% rename from memory/agent-memory/images/02-architecture-patterns.svg rename to docs/research/microsoft-foundry/agent-memory-architecture-patterns/images/02-architecture-patterns.svg diff --git a/memory/agent-memory/02-architecture-patterns.md b/docs/research/microsoft-foundry/agent-memory-architecture-patterns/index.md similarity index 94% rename from memory/agent-memory/02-architecture-patterns.md rename to docs/research/microsoft-foundry/agent-memory-architecture-patterns/index.md index 224a2499..9967a612 100644 --- a/memory/agent-memory/02-architecture-patterns.md +++ b/docs/research/microsoft-foundry/agent-memory-architecture-patterns/index.md @@ -1,3 +1,23 @@ +--- +services: +- microsoft-foundry +official_sources: +- title: Microsoft Foundry documentation + url: https://learn.microsoft.com/azure/ai-foundry/ +document_type: research +status: current +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: Agent Memory 아키텍처 패턴 +description: Agent memory를 저장하고 연결하는 주요 아키텍처 패턴을 비교합니다. +technologies: +- rag +tags: +- ai-agents +- architecture +published_at: 2026-08-24 +--- + # 02. 아키텍처 패턴 — 어떤 구조로 담을 것인가 ![아키텍처 5패턴](images/02-architecture-patterns.svg) @@ -223,7 +243,7 @@ CPU 캐시 계층(L1/L2/메인메모리)에서 착안했다. 접근 빈도·최 | **Phase 3** | + Tiered (HOT/WARM) + Graph-Augmented | 지연·비용이 실제 문제가 되고, 상품·브랜드 관계 추천이 필요해질 때. COLD는 그 다음 | | **Phase 4** | 필요 시 Full Cognitive | 라우터 도입은 저장소가 3개 이상, 오라우팅 모니터링 체계가 갖춰진 후 | -이 Phase 구분은 [06. 커머스 적용 설계 §10 로드맵](06-commerce-application.md)과 동일한 기준이다. **티어링을 Phase 2로 당기지 않는 것**이 핵심이다 — 저장소를 나누기 전에 retention 정책과 평가 체계가 먼저 서야 한다. +이 Phase 구분은 [06. 커머스 적용 설계 §10 로드맵](../agent-memory-commerce/index.md)과 동일한 기준이다. **티어링을 Phase 2로 당기지 않는 것**이 핵심이다 — 저장소를 나누기 전에 retention 정책과 평가 체계가 먼저 서야 한다. **Full Cognitive를 처음부터 짓지 말 것.** 이 패턴의 최대 약점으로 지목되는 것이 "오버엔지니어링 위험"이며, 라우터 오분류는 **조용히 실패한다**(데이터가 엉뚱한 저장소에 들어가 영영 검색되지 않음). 자체 프레임워크를 운영 중인 팀이라면 라우팅 계층을 얹기 전에 로깅·평가 체계부터 갖추는 편이 안전하다. @@ -231,8 +251,8 @@ CPU 캐시 계층(L1/L2/메인메모리)에서 착안했다. 접근 빈도·최 ## 다음 문서 -- [03. 파이프라인과 검색](03-pipeline-and-retrieval.md) — 각 패턴 내부에서 데이터가 흐르는 방식 -- [05. 프로덕션과 평가](05-production-evaluation.md) — Tiered 패턴의 운영 구현 +- [03. 파이프라인과 검색](../agent-memory-pipeline-retrieval/index.md) — 각 패턴 내부에서 데이터가 흐르는 방식 +- [05. 프로덕션과 평가](../agent-memory-production-evaluation/index.md) — Tiered 패턴의 운영 구현 --- diff --git a/memory/agent-memory/images/05-commerce-blueprint.svg b/docs/research/microsoft-foundry/agent-memory-commerce/images/05-commerce-blueprint.svg similarity index 100% rename from memory/agent-memory/images/05-commerce-blueprint.svg rename to docs/research/microsoft-foundry/agent-memory-commerce/images/05-commerce-blueprint.svg diff --git a/memory/agent-memory/06-commerce-application.md b/docs/research/microsoft-foundry/agent-memory-commerce/index.md similarity index 97% rename from memory/agent-memory/06-commerce-application.md rename to docs/research/microsoft-foundry/agent-memory-commerce/index.md index 63d196cc..019ae802 100644 --- a/memory/agent-memory/06-commerce-application.md +++ b/docs/research/microsoft-foundry/agent-memory-commerce/index.md @@ -1,3 +1,23 @@ +--- +services: +- microsoft-foundry +official_sources: +- title: Microsoft Foundry documentation + url: https://learn.microsoft.com/azure/ai-foundry/ +document_type: research +status: current +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: 커머스 Agent Memory 적용 설계 +description: 실시간 추천과 개인화 구매 경험에 agent memory를 적용하는 설계를 제안합니다. +technologies: +- rag +tags: +- ai-agents +- architecture +published_at: 2026-08-24 +--- + # 06. 커머스 적용 설계 — 실시간 추천과 개인 맞춤 구매 유도 ![B2C 커머스 메모리 블루프린트](images/05-commerce-blueprint.svg) diff --git a/memory/agent-memory/04-frameworks.md b/docs/research/microsoft-foundry/agent-memory-frameworks/index.md similarity index 95% rename from memory/agent-memory/04-frameworks.md rename to docs/research/microsoft-foundry/agent-memory-frameworks/index.md index b99eb3b1..b4c6040c 100644 --- a/memory/agent-memory/04-frameworks.md +++ b/docs/research/microsoft-foundry/agent-memory-frameworks/index.md @@ -1,3 +1,23 @@ +--- +services: +- microsoft-foundry +official_sources: +- title: Microsoft Foundry documentation + url: https://learn.microsoft.com/azure/ai-foundry/ +document_type: research +status: current +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: Agent Memory 프레임워크와 플랫폼 비교 +description: Agent memory 구현을 위한 프레임워크와 관리형 플랫폼을 비교합니다. +technologies: +- rag +tags: +- ai-agents +- architecture +published_at: 2026-08-24 +--- + # 04. 프레임워크 · 플랫폼 비교 — 만들 것인가 가져올 것인가 자체 에이전트 프레임워크를 이미 운영 중인 팀에게 이 문서의 질문은 하나다. **"메모리 레이어까지 직접 만들 것인가, 특정 부분만 가져올 것인가."** @@ -201,8 +221,8 @@ TTL / Decay 정책 ┃ ## 다음 문서 -- [05. 프로덕션과 평가](05-production-evaluation.md) -- [06. 커머스 적용 설계](06-commerce-application.md) +- [05. 프로덕션과 평가](../agent-memory-production-evaluation/index.md) +- [06. 커머스 적용 설계](../agent-memory-commerce/index.md) --- diff --git a/memory/agent-memory/README.md b/docs/research/microsoft-foundry/agent-memory-overview/index.md similarity index 62% rename from memory/agent-memory/README.md rename to docs/research/microsoft-foundry/agent-memory-overview/index.md index e0633229..57bc232c 100644 --- a/memory/agent-memory/README.md +++ b/docs/research/microsoft-foundry/agent-memory-overview/index.md @@ -1,3 +1,23 @@ +--- +services: +- microsoft-foundry +official_sources: +- title: Microsoft Foundry documentation + url: https://learn.microsoft.com/azure/ai-foundry/ +document_type: research +status: current +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: Agent Memory 종합 리서치 +description: AI agent memory의 분류, 아키텍처, 검색, 프레임워크와 운영 평가를 종합합니다. +technologies: +- rag +tags: +- ai-agents +- architecture +published_at: 2026-08-24 +--- + # Agent Memory 종합 리서치 자체 에이전트 프레임워크로 **B2C 서비스 에이전트**를 운영하는 개발 조직을 대상으로, 메모리를 정교화해 **실시간 추천**과 **개인 맞춤 구매 유도 메시지**를 제공하기 위한 리서치 결과물이다. @@ -21,12 +41,12 @@ | 문서 | 내용 | 대상 | |------|------|------| -| **[01. 메모리 분류 체계](01-memory-taxonomy.md)** | Agent Memory **8유형 통합 정의**, 유형별 **저장소 선택지**와 Azure 매핑, Retention 정책, 30기법 6패밀리 지도 | 전원 | -| **[02. 아키텍처 패턴](02-architecture-patterns.md)** | Single-Store / Dual-Store / Tiered / Graph-Augmented / Full Cognitive 5패턴, 비교표, 선택 결정 트리 | 아키텍트 | -| **[03. 파이프라인과 검색](03-pipeline-and-retrieval.md)** | 쓰기 경로(추출→중복·모순 해소→라우팅) / 읽기 경로(hybrid search→RRF→rerank→MMR→temporal), 단계별 지연 비용 | 개발자 | -| **[04. 프레임워크 비교](04-frameworks.md)** | Mem0 · Zep · Graphiti · Letta(MemGPT) · Cognee · Azure AI Search, **자체 구현 vs 도입 판단 기준** | 의사결정자 | -| **[05. 프로덕션과 평가](05-production-evaluation.md)** | Tiered storage, PII·GDPR, TTL·샤딩·관측성·비용, 평가 지표, LoCoMo/LongMemEval 벤치마크 | SRE / 개발자 | -| **[06. 커머스 적용 설계](06-commerce-application.md)** | **메모리 스키마 · 지연 예산 · 개인화 강도 단계화 · 안티패턴 · Phase 0~4 로드맵 · 기술+비즈니스 지표** | 전원 (핵심) | +| **[01. 메모리 분류 체계](../agent-memory-taxonomy/index.md)** | Agent Memory **8유형 통합 정의**, 유형별 **저장소 선택지**와 Azure 매핑, Retention 정책, 30기법 6패밀리 지도 | 전원 | +| **[02. 아키텍처 패턴](../agent-memory-architecture-patterns/index.md)** | Single-Store / Dual-Store / Tiered / Graph-Augmented / Full Cognitive 5패턴, 비교표, 선택 결정 트리 | 아키텍트 | +| **[03. 파이프라인과 검색](../agent-memory-pipeline-retrieval/index.md)** | 쓰기 경로(추출→중복·모순 해소→라우팅) / 읽기 경로(hybrid search→RRF→rerank→MMR→temporal), 단계별 지연 비용 | 개발자 | +| **[04. 프레임워크 비교](../agent-memory-frameworks/index.md)** | Mem0 · Zep · Graphiti · Letta(MemGPT) · Cognee · Azure AI Search, **자체 구현 vs 도입 판단 기준** | 의사결정자 | +| **[05. 프로덕션과 평가](../agent-memory-production-evaluation/index.md)** | Tiered storage, PII·GDPR, TTL·샤딩·관측성·비용, 평가 지표, LoCoMo/LongMemEval 벤치마크 | SRE / 개발자 | +| **[06. 커머스 적용 설계](../agent-memory-commerce/index.md)** | **메모리 스키마 · 지연 예산 · 개인화 강도 단계화 · 안티패턴 · Phase 0~4 로드맵 · 기술+비즈니스 지표** | 전원 (핵심) | --- @@ -34,17 +54,17 @@ | 파일 | 내용 | |------|------| -| [images/01-memory-taxonomy.svg](images/01-memory-taxonomy.svg) | 6패밀리 30기법 전체 지도 | -| [images/02-architecture-patterns.svg](images/02-architecture-patterns.svg) | 5가지 아키텍처 패턴 비교 | -| [images/03-memory-pipeline.svg](images/03-memory-pipeline.svg) | 쓰기 경로 / 읽기 경로 분리 | -| [images/04-production-tiers.svg](images/04-production-tiers.svg) | 프로덕션 계층 저장 + 가드레일 | -| [images/05-commerce-blueprint.svg](images/05-commerce-blueprint.svg) | B2C 커머스 메모리 블루프린트 | +| [images/01-memory-taxonomy.svg](https://github.com/hellices/devguidesample/blob/main/samples/microsoft-foundry/memory-artifacts/agent-memory/images/01-memory-taxonomy.svg) | 6패밀리 30기법 전체 지도 | +| [images/02-architecture-patterns.svg](https://github.com/hellices/devguidesample/blob/main/samples/microsoft-foundry/memory-artifacts/agent-memory/images/02-architecture-patterns.svg) | 5가지 아키텍처 패턴 비교 | +| [images/03-memory-pipeline.svg](https://github.com/hellices/devguidesample/blob/main/samples/microsoft-foundry/memory-artifacts/agent-memory/images/03-memory-pipeline.svg) | 쓰기 경로 / 읽기 경로 분리 | +| [images/04-production-tiers.svg](https://github.com/hellices/devguidesample/blob/main/samples/microsoft-foundry/memory-artifacts/agent-memory/images/04-production-tiers.svg) | 프로덕션 계층 저장 + 가드레일 | +| [images/05-commerce-blueprint.svg](https://github.com/hellices/devguidesample/blob/main/samples/microsoft-foundry/memory-artifacts/agent-memory/images/05-commerce-blueprint.svg) | B2C 커머스 메모리 블루프린트 | --- ## 읽는 순서 -**시간이 없다면** → [06. 커머스 적용 설계](06-commerce-application.md) 만 읽는다. 나머지 문서의 결론이 여기 수렴한다. +**시간이 없다면** → [06. 커머스 적용 설계](../agent-memory-commerce/index.md) 만 읽는다. 나머지 문서의 결론이 여기 수렴한다. **설계를 시작한다면** ``` @@ -68,7 +88,7 @@ | **3** | HOT/WARM 티어링, Structured RAG, Product Graph, 관측성 | 규모에서의 지연·비용 통제 | | **4** | Memory Routing, Procedural, Self-Reflection, COLD 아카이브 | 선택적 고도화 | -상세는 [06. 커머스 적용 설계 §10](06-commerce-application.md) 참조. +상세는 [06. 커머스 적용 설계 §10](../agent-memory-commerce/index.md) 참조. --- @@ -96,6 +116,6 @@ | 11 | [A Survey on the Memory Mechanism of LLM based Agents (arXiv:2404.13501)](https://arxiv.org/abs/2404.13501) | 학술적 분류 체계 · [정리 저장소](https://github.com/nuster1128/LLM_Agent_Memory_Survey) | | 12 | [GDPR Article 17 — Right to erasure](https://gdpr-info.eu/art-17-gdpr/) | 전 계층 삭제 요구사항 | -> **자료 활용 원칙.** 문서 본문의 서술은 1차 자료 2건을 기준으로 구성했고, 그 안에서 언급되거나 개념의 출처가 되는 논문을 확인해 근거 문헌으로 분리했다. 표의 모든 링크는 실제 접속해 제목·저자·연도를 확인했다. 다만 RRF(SIGIR 2009)와 MMR(SIGIR 1998)은 ACM DL 유료 문헌이라 [03. 파이프라인과 검색](03-pipeline-and-retrieval.md)에 서지 정보만 표기했다. +> **자료 활용 원칙.** 문서 본문의 서술은 1차 자료 2건을 기준으로 구성했고, 그 안에서 언급되거나 개념의 출처가 되는 논문을 확인해 근거 문헌으로 분리했다. 표의 모든 링크는 실제 접속해 제목·저자·연도를 확인했다. 다만 RRF(SIGIR 2009)와 MMR(SIGIR 1998)은 ACM DL 유료 문헌이라 [03. 파이프라인과 검색](../agent-memory-pipeline-retrieval/index.md)에 서지 정보만 표기했다. 문서별 상세 참고 자료는 각 문서 하단에 있다. diff --git a/memory/agent-memory/images/03-memory-pipeline.svg b/docs/research/microsoft-foundry/agent-memory-pipeline-retrieval/images/03-memory-pipeline.svg similarity index 100% rename from memory/agent-memory/images/03-memory-pipeline.svg rename to docs/research/microsoft-foundry/agent-memory-pipeline-retrieval/images/03-memory-pipeline.svg diff --git a/memory/agent-memory/03-pipeline-and-retrieval.md b/docs/research/microsoft-foundry/agent-memory-pipeline-retrieval/index.md similarity index 93% rename from memory/agent-memory/03-pipeline-and-retrieval.md rename to docs/research/microsoft-foundry/agent-memory-pipeline-retrieval/index.md index bf7a6640..a5555039 100644 --- a/memory/agent-memory/03-pipeline-and-retrieval.md +++ b/docs/research/microsoft-foundry/agent-memory-pipeline-retrieval/index.md @@ -1,3 +1,24 @@ +--- +services: +- microsoft-foundry +official_sources: +- title: Microsoft Foundry documentation + url: https://learn.microsoft.com/azure/ai-foundry/ +document_type: research +status: current +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: 'Agent Memory: 무엇을 남기고, 언제 다시 찾을까?' +description: 대화에서 기억을 추출하는 쓰기 경로와 필요한 순간에 검색하는 읽기 경로를 나눕니다. 중복·모순 처리와 지연·비용의 설계 쟁점을 살펴봅니다. +featured: true +technologies: +- rag +tags: +- ai-agents +- architecture +published_at: 2026-08-24 +--- + # 03. 파이프라인과 검색 — 쓰기 경로 / 읽기 경로 ![메모리 파이프라인](images/03-memory-pipeline.svg) @@ -73,7 +94,7 @@ Ebbinghaus 망각 곡선(1885)의 exponential decay를 적용한다. S(t) = S₀ · e^(−λ · t) ``` -- λ가 클수록 빨리 잊는다. **half-life는 도메인과 기억 유형에 따라 수십 분에서 수백 일까지 벌어진다** — 단일 값을 쓰면 안 된다. 유형별 권장치는 [01. 메모리 분류 체계의 Retention 정책 표](01-memory-taxonomy.md)와 [06. 커머스 적용 설계 §6](06-commerce-application.md) 참조 +- λ가 클수록 빨리 잊는다. **half-life는 도메인과 기억 유형에 따라 수십 분에서 수백 일까지 벌어진다** — 단일 값을 쓰면 안 된다. 유형별 권장치는 [01. 메모리 분류 체계의 Retention 정책 표](../agent-memory-taxonomy/index.md)와 [06. 커머스 적용 설계 §6](../agent-memory-commerce/index.md) 참조 - **접근 시 강화(reinforcement)**: 검색될 때마다 강도 부스트 → 자주 유용한 기억은 오래 살아남음 - **프루닝 임계값**: 미만이면 soft delete(아카이브) 또는 hard delete - **저장 압력 모니터**가 임계값을 동적으로 조정 → 용량 한계 근접 시 더 공격적으로 망각 @@ -214,8 +235,8 @@ LLM이 **잘못된 가상 답변**을 생성하면, 검색은 정답이 아니 ## 다음 문서 -- [04. 프레임워크 비교](04-frameworks.md) — 직접 만들 것인가 가져다 쓸 것인가 -- [05. 프로덕션과 평가](05-production-evaluation.md) — 이 파이프라인을 규모에서 운영하기 +- [04. 프레임워크 비교](../agent-memory-frameworks/index.md) — 직접 만들 것인가 가져다 쓸 것인가 +- [05. 프로덕션과 평가](../agent-memory-production-evaluation/index.md) — 이 파이프라인을 규모에서 운영하기 --- diff --git a/memory/agent-memory/images/04-production-tiers.svg b/docs/research/microsoft-foundry/agent-memory-production-evaluation/images/04-production-tiers.svg similarity index 100% rename from memory/agent-memory/images/04-production-tiers.svg rename to docs/research/microsoft-foundry/agent-memory-production-evaluation/images/04-production-tiers.svg diff --git a/memory/agent-memory/05-production-evaluation.md b/docs/research/microsoft-foundry/agent-memory-production-evaluation/index.md similarity index 93% rename from memory/agent-memory/05-production-evaluation.md rename to docs/research/microsoft-foundry/agent-memory-production-evaluation/index.md index 4b72a615..263d33d0 100644 --- a/memory/agent-memory/05-production-evaluation.md +++ b/docs/research/microsoft-foundry/agent-memory-production-evaluation/index.md @@ -1,3 +1,23 @@ +--- +services: +- microsoft-foundry +official_sources: +- title: Microsoft Foundry documentation + url: https://learn.microsoft.com/azure/ai-foundry/ +document_type: research +status: current +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: Agent Memory 프로덕션 운영과 평가 +description: Agent memory를 프로덕션에서 운영하고 품질을 평가하는 기준을 정리합니다. +technologies: +- rag +tags: +- ai-agents +- reliability +published_at: 2026-08-24 +--- + # 05. 프로덕션 운영과 평가 ![프로덕션 계층 아키텍처](images/04-production-tiers.svg) @@ -181,7 +201,7 @@ demote → M턴 미접근 | relevance score 하락 | 세션 종료 | decay sco **1번과 2번을 건너뛰면 이후 모든 최적화가 추측이 된다.** -이 순서는 [06. 커머스 적용 설계 §10 로드맵](06-commerce-application.md)의 Phase 0(1~4) → Phase 2(5) → Phase 3(6) → Phase 4(7)에 그대로 대응한다. +이 순서는 [06. 커머스 적용 설계 §10 로드맵](../agent-memory-commerce/index.md)의 Phase 0(1~4) → Phase 2(5) → Phase 3(6) → Phase 4(7)에 그대로 대응한다. --- @@ -202,7 +222,7 @@ demote → M턴 미접근 | relevance score 하락 | 세션 종료 | decay sco ## 다음 문서 -- [06. 커머스 적용 설계](06-commerce-application.md) +- [06. 커머스 적용 설계](../agent-memory-commerce/index.md) --- diff --git a/memory/agent-memory/images/01-memory-taxonomy.svg b/docs/research/microsoft-foundry/agent-memory-taxonomy/images/01-memory-taxonomy.svg similarity index 100% rename from memory/agent-memory/images/01-memory-taxonomy.svg rename to docs/research/microsoft-foundry/agent-memory-taxonomy/images/01-memory-taxonomy.svg diff --git a/memory/agent-memory/01-memory-taxonomy.md b/docs/research/microsoft-foundry/agent-memory-taxonomy/index.md similarity index 93% rename from memory/agent-memory/01-memory-taxonomy.md rename to docs/research/microsoft-foundry/agent-memory-taxonomy/index.md index b401c9ce..dec9b91b 100644 --- a/memory/agent-memory/01-memory-taxonomy.md +++ b/docs/research/microsoft-foundry/agent-memory-taxonomy/index.md @@ -1,3 +1,23 @@ +--- +services: +- microsoft-foundry +official_sources: +- title: Microsoft Foundry documentation + url: https://learn.microsoft.com/azure/ai-foundry/ +document_type: research +status: current +verification_status: needs-review +sources_checked_at: 2026-09-12 +title: Agent Memory 분류 체계 +description: AI agent가 기억할 정보의 유형과 수명주기를 분류합니다. +technologies: +- rag +tags: +- ai-agents +- architecture +published_at: 2026-08-24 +--- + # 01. 메모리 분류 체계 — 무엇을 기억할 것인가 ![메모리 분류 체계](images/01-memory-taxonomy.svg) @@ -121,7 +141,7 @@ | Procedural / Persona | **No decay**, 버전 관리 | — | | Cold Archive | **Retention period** (규제 기준) | 3~5년 | -관련 개념은 [03. 파이프라인과 검색](03-pipeline-and-retrieval.md)에서 Temporal Memory · Forgetting & Decay · Consolidation으로 자세히 다룬다. +관련 개념은 [03. 파이프라인과 검색](../agent-memory-pipeline-retrieval/index.md)에서 Temporal Memory · Forgetting & Decay · Consolidation으로 자세히 다룬다. --- @@ -148,7 +168,7 @@ ## 커머스 관점 우선순위 -실시간 추천 + 개인 맞춤 구매 유도라는 목표에 비춰 기법의 실효 순위를 매기면 다음과 같다. Phase 번호는 [06. 커머스 적용 설계 §10 로드맵](06-commerce-application.md)과 같은 기준을 쓴다. +실시간 추천 + 개인 맞춤 구매 유도라는 목표에 비춰 기법의 실효 순위를 매기면 다음과 같다. Phase 번호는 [06. 커머스 적용 설계 §10 로드맵](../agent-memory-commerce/index.md)과 같은 기준을 쓴다. ### 선행 필수 (Phase 0) | 기법 | 이유 | @@ -191,8 +211,8 @@ ## 다음 문서 -- [02. 아키텍처 패턴](02-architecture-patterns.md) — 이 메모리들을 어떤 구조로 담을 것인가 -- [06. 커머스 적용 설계](06-commerce-application.md) — 실시간 추천/구매 유도로 직결되는 설계 +- [02. 아키텍처 패턴](../agent-memory-architecture-patterns/index.md) — 이 메모리들을 어떤 구조로 담을 것인가 +- [06. 커머스 적용 설계](../agent-memory-commerce/index.md) — 실시간 추천/구매 유도로 직결되는 설계 --- diff --git a/docs/tags.md b/docs/tags.md new file mode 100644 index 00000000..8191a0c6 --- /dev/null +++ b/docs/tags.md @@ -0,0 +1,32 @@ +--- +title: 태그별 찾기 +description: 지연, 네트워크, 성능, AI 에이전트 등 지금 다루는 문제에서 관련 문서를 찾습니다. +hide: + - toc +--- + +
+ +
+ +

TOPIC INDEX

+ +# 지금 다루는 문제에서 시작하기 + +

같은 증상도 여러 서비스에서 나타납니다. 제품 이름 대신 관심 있는 주제로 관련 기록을 모아보세요.

+ +
+ + + + + +
diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..eca136e3 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,79 @@ +site_name: DevGuideSample +site_description: Azure, Kubernetes, AI 시스템의 장애 분석과 구현, 벤치마크를 담은 엔지니어링 노트 +site_url: https://hellices.github.io/devguidesample/ +repo_url: https://github.com/hellices/devguidesample +repo_name: hellices/devguidesample +edit_uri: edit/main/docs/ +docs_dir: docs +site_dir: site +use_directory_urls: true + +validation: + links: + anchors: warn + +theme: + name: material + language: ko + font: + text: Noto Sans KR + code: JetBrains Mono + features: + - navigation.indexes + - navigation.top + - search.suggest + - search.highlight + - search.share + - content.code.copy + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: custom + accent: custom + toggle: + icon: material/brightness-7 + name: 어두운 테마로 전환 + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: custom + accent: custom + toggle: + icon: material/brightness-4 + name: 밝은 테마로 전환 + +plugins: + - search: + lang: + - ko + - en + - tags + - gen-files: + scripts: + - scripts/docs/generate_indexes.py + - awesome-nav + +hooks: + - scripts/docs/hooks.py + +markdown_extensions: + - admonition + - attr_list + - md_in_html + - tables + - toc: + permalink: true + - pymdownx.details + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + +extra_css: + - assets/stylesheets/extra.css + +copyright: Copyright © DevGuideSample contributors diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_lab_guides.py b/monitor/sre-agent-event-lab/scripts/tests/test_lab_guides.py deleted file mode 100644 index b4a24210..00000000 --- a/monitor/sre-agent-event-lab/scripts/tests/test_lab_guides.py +++ /dev/null @@ -1,1115 +0,0 @@ -"""Contract tests for the ordered lab walkthrough. - -The README is the quickstart an operator reads first, and `guides/` holds -the step-by-step documents it hands off to. These tests check behaviour a -reader depends on -- that the commands are the ones the lab really -accepts, in the order `lab_state.py` really enforces; that every path, -link and screenshot resolves; and that nothing here asks anyone to paste a -credential into a file or an environment variable. -""" -import re -from pathlib import Path - - -REPO_ROOT = Path(__file__).parents[4] -LAB_ROOT = REPO_ROOT / "monitor" / "sre-agent-event-lab" -README = LAB_ROOT / "README.md" -GUIDES = LAB_ROOT / "guides" -OFFICIAL_ASSETS = LAB_ROOT / "assets" / "official" -RUNBOOK = LAB_ROOT / "runbooks" / "incident-response.md" -VALIDATION_RESULTS = LAB_ROOT / "validation-results.md" -DYNAMIC_THRESHOLDS = LAB_ROOT / "dynamic-thresholds.md" -RESULTS_GUIDE = LAB_ROOT / "guides" / "05-results.md" -DEPLOYMENT_PLAN = REPO_ROOT / ".azure" / "deployment-plan.md" - -# The table `infra/alerts.bicep` actually queries for each scenario -- -# workspace schema, not the legacy Application Insights component schema. -SCENARIO_GUIDE_TABLES = { - "02-scenario-s1.md": "AppRequests", - "03-scenario-s2.md": "AppRequests", - "04-scenario-s3.md": "AppDependencies", -} - -GUIDE_NAMES = ( - "01-agent-setup.md", - "02-scenario-s1.md", - "03-scenario-s2.md", - "04-scenario-s3.md", - "05-results.md", -) - -SCENARIO_GUIDES = { - "02-scenario-s1.md": "s1", - "03-scenario-s2.md": "s2", - "04-scenario-s3.md": "s3", -} - -# The screenshots selected from the live Learn articles, each tied to one -# portal action an operator performs by hand. Anything the lab can prove -# with its own captured evidence is deliberately not copied here. -GUIDE_SCREENSHOTS = { - "portal-setup-status-bar.png": ( - "https://learn.microsoft.com/azure/sre-agent/complete-setup" - ), - "portal-complete-setup-page.png": ( - "https://learn.microsoft.com/azure/sre-agent/complete-setup" - ), - "portal-incident-response-plans-list.png": ( - "https://learn.microsoft.com/azure/sre-agent/automate-incidents" - ), - "portal-response-plan-autonomy-step.png": ( - "https://learn.microsoft.com/azure/sre-agent/automate-incidents" - ), -} - -# Every keyword is a string or state actually visible in the downloaded -# screenshot, so alt text that drifts from the picture fails here. -SCREENSHOT_ALT_KEYWORDS = { - "portal-setup-status-bar.png": ( - "6 sources not configured", - "Complete setup", - "Code", - "Logs", - "Deployments", - "Incidents", - "Azure resources", - "Knowledge files", - "Builder", - ), - "portal-complete-setup-page.png": ( - "Quickstart", - "Full setup", - "Code", - "Logs", - "Recommended", - ), - "portal-incident-response-plans-list.png": ( - "Builder", - "Incident response plans", - "Azure Monitor is connected", - "Autonomy level", - "Autonomous", - "On", - ), - "portal-response-plan-autonomy-step.png": ( - "Review (Default)", - "Autonomous", - "Save response plan", - "Save", - ), -} - -# Claims the picture does not support. The two response-plan screenshots -# both show `Autonomous`, and this lab runs in `Review`: alt text may never -# describe them as showing the mode the lab asks for. -FORBIDDEN_ALT_CLAIMS = { - "portal-incident-response-plans-list.png": ("Review 모드로", "Review로 표시"), - "portal-response-plan-autonomy-step.png": ("Review가 선택", "Review를 선택한 상태"), - "portal-complete-setup-page.png": ("Azure resources", "Knowledge files"), -} - -SECRET_ASSIGNMENTS = ( - "GITHUB_PAT=", - "OAUTH_TOKEN=", - "CLIENT_SECRET=", - "ACCESS_TOKEN=", - "AZURE_CLIENT_SECRET=", -) - -FIXED_RESOURCE_GROUP = "rg-sre-agent-event-lab-krc" -UUID_PATTERN = re.compile(r"\b[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\b") - - -def guide_paths(): - return [GUIDES / name for name in GUIDE_NAMES] - - -def all_docs(): - return [README] + guide_paths() - - -def joined_docs() -> str: - return "\n".join(path.read_text() for path in all_docs()) - - -def images(text: str): - """(alt, target) for every rendered image, Markdown or HTML.""" - markdown = re.findall(r"!\[([^\]]*)\]\(([^)]+)\)", text) - html = [("", target) for target in re.findall(r"]*\ssrc=[\"']([^\"']+)", text)] - return markdown + html - - -def body_sentences(markdown: str, minimum_length: int = 30): - """Substantive body sentences, with code, images and captions removed.""" - markdown = re.sub(r"```.*?```", "", markdown, flags=re.DOTALL) - markdown = re.sub(r"!\[[^\]]*\]\([^)]*\)", "", markdown) - - fragments = [] - for line in markdown.splitlines(): - line = line.strip() - if not line or line.startswith(("#", ">")): - continue - if re.fullmatch(r"\|[\s:\-|]+\|", line): - continue - cells = line.strip("|").split("|") if line.startswith("|") else [line] - for cell in cells: - cell = re.sub(r"^[-*+]\s+", "", cell.strip()) - cell = re.sub(r"^\d+\.\s+", "", cell) - cell = re.sub(r"\[([^\]]+)\]\([^)]*\)", r"\1", cell) - cell = cell.replace("**", "").replace("`", "").strip() - if cell: - fragments.append(cell) - - sentences = [] - for fragment in fragments: - for sentence in re.split(r"(?<=다\.)\s+", fragment): - sentence = re.sub(r"\s+", " ", sentence).strip() - if len(sentence) >= minimum_length and re.search(r"(다|요)\.$", sentence): - sentences.append(sentence) - return sentences - - -def blocks(text: str): - return [block for block in re.split(r"\n\s*\n", text) if block.strip()] - - -# --- README quickstart --------------------------------------------------- - - -def test_readme_is_azd_first_and_ordered(): - text = README.read_text() - headings = [ - "## 시작하기: fork와 Codespaces", - "## 사전 조건", - "## azd 환경 만들기", - "## 배포", - "## Azure SRE Agent 설정", - "## 정상 상태 확인과 승인", - "## 시나리오 실행", - "## 결과 확인", - "## 정리", - ] - positions = [text.index(heading) for heading in headings] - assert positions == sorted(positions) - - # The commands that carry each step, in the section that owns them. - def section(heading): - return text.split(heading, 1)[1].split("\n## ", 1)[0] - - assert "azd env new" in section("## azd 환경 만들기") - assert "azd up" in section("## 배포") - assert "lab_state.py acknowledge-agent" in section("## 정상 상태 확인과 승인") - assert "scripts/score.py" in section("## 결과 확인") - assert "azd down --purge" in section("## 정리") - scenarios = section("## 시나리오 실행") - for name in ("02-scenario-s1.md", "03-scenario-s2.md", "04-scenario-s3.md"): - assert name in scenarios, name - - -def test_readme_warns_about_cost_and_teardown_before_the_first_azure_command(): - text = README.read_text() - - warning = re.search(r"(과금|비용)", text) - assert warning, "the README must state that the lab bills real resources" - assert warning.start() < text.index("azd up") - assert text.index("azd down --purge") > text.index("azd up") - for driver in ("Container Apps", "Log Analytics", "Azure SRE Agent"): - assert driver in text, driver - - -def test_readme_states_one_minute_alert_evaluation(): - """The `QueryNotContainKnownTable` failure came from the legacy - Application Insights `requests`/`dependencies` schema on the component - scope, not from the one-minute cadence: `az deployment group validate` - accepts `evaluationFrequency: PT1M` for the workspace-scoped - `AppRequests`/`AppDependencies` rules. The cost callout must state the - one-minute cadence `infra/alerts.bicep` actually deploys. - """ - text = README.read_text() - - assert "1분 주기 로그 검색 경고 규칙 3개" in text - assert "5분 주기 로그 검색 경고 규칙" not in text - - -def test_dynamic_thresholds_guide_states_one_minute_static_evaluation(): - """The Static Threshold section of dynamic-thresholds.md describes the - deployed infra/alerts.bicep rules, which evaluate every minute over a - five-minute window. The separate, still-true statement that Log Search - *dynamic* thresholds do not support one-minute evaluation stays. - """ - text = DYNAMIC_THRESHOLDS.read_text() - - assert "- evaluation: 1분" in text - assert "- evaluation: 5분" not in text - assert "Log Search dynamic threshold는 1분 evaluation을 지원하지 않는다." in text - - -def test_dynamic_thresholds_sections_are_numbered_without_gaps(): - """A reader following "8절 참고" would find no section 8: the document - jumped from 7 to 9. Numbered top-level sections must be consecutive.""" - numbers = [ - int(match) - for match in re.findall( - r"^## (\d+)\. ", DYNAMIC_THRESHOLDS.read_text(), re.MULTILINE - ) - ] - - assert numbers, "dynamic-thresholds.md has no numbered sections" - assert numbers == list(range(1, len(numbers) + 1)), numbers - - -def test_deployment_plan_status_matches_what_was_actually_deployed(): - """The plan carried both a `Validated` status *and* the pre-deployment - notes it was written with: "Ready for Validation", "no resources - deployed", a fresh environment still being needed and an unchecked - provision preview. A live `azd up`/`azd down --purge` cycle has since - run, so those cannot both be true. `Validated` is kept for the base azd - deployment only, and it has to say so on the status line itself. - """ - text = DEPLOYMENT_PLAN.read_text() - - status = re.search(r"^> \*\*Status:\*\* (.+)$", text, re.MULTILINE) - assert status, "the plan has no status line" - assert status.group(1).startswith("Validated — base azd deployment only"), ( - status.group(1) - ) - - for stale in ( - "Ready for Validation", - "no resources deployed", - "a fresh environment is needed", - ): - assert stale not in text, stale - assert "- [ ]" not in text, ( - "an unchecked preflight item contradicts the recorded live deployment" - ) - - -def test_deployment_plan_does_not_claim_the_manual_scenario_sequence_ran(): - """The operator said they would connect the Agent and run S1/S2/S3 one - by one themselves. Only the pre-acknowledgement refusal (a scenario - rejected before `acknowledge-agent`) was exercised, so the plan - must record the scenario sequence as pending -- in the Live Deployment - Proof table, where a reader looks for what the deployment proved. - """ - text = DEPLOYMENT_PLAN.read_text() - - proof = text.split("## Live Deployment Proof", 1) - assert len(proof) == 2, "the plan has no Live Deployment Proof section" - proof_table = proof[1] - - pending = re.search(r"^\| Manual scenario sequence \| (.+) \|$", proof_table, re.MULTILINE) - assert pending, "no Live Deployment Proof row for the pending manual sequence" - row = pending.group(1) - assert row.startswith("Pending"), row - for expected in ("S1", "S2", "S3", "capture", "score", "one by one"): - assert expected in row, expected - - assert "have not been run" in text - assert "pre-acknowledgement" in text or "pre-ack" in text - - -def test_deployment_plan_records_one_current_test_and_bicep_count(): - """The preflight list still claimed the 460-test, three-template run - that predates the workspace-schema fix, while Section 7 records the - current one. Two different suite sizes in one plan is exactly the kind - of drift this document is read for. - """ - text = DEPLOYMENT_PLAN.read_text() - - preflight = re.search(r"Build Verification — (\d+) tests and (\w+) Bicep builds", text) - assert preflight, "the preflight list records no build verification count" - section_seven = re.search(r"\| (\d+) passed", text) - assert section_seven, "Section 7 records no test count" - - assert preflight.group(1) == section_seven.group(1), ( - preflight.group(1), - section_seven.group(1), - ) - assert int(preflight.group(1)) >= 472, "the recorded suite shrank" - assert preflight.group(2) == "five", "five Bicep templates are built, not three" - assert "460 tests" not in text - - -def test_scenario_guides_document_the_critical_recovery_failure_path(): - """The operator reverts the injected fault by hand, and - that revert can itself fail (a rejected `az containerapp update`, a - revision that never becomes ready, a refused role restore). It then - prints `CRITICAL:` and exits non-zero with the fault still live, so each - scenario guide has to tell the operator to revert it by hand instead of - starting the next scenario. - """ - for name in SCENARIO_GUIDES: - recovery = (GUIDES / name).read_text().split("## 복구 확인", 1) - assert len(recovery) == 2, name - section = recovery[1].split("\n## ", 1)[0] - assert "CRITICAL" in section, name - assert "수동" in section or "직접 되돌" in section, name - - -def test_validation_results_opens_by_dating_itself_to_the_pre_azd_lab(): - """Everything in this report -- the fixed subscription, the - `rg-sre-agent-event-lab-krc` resource group, the Logic App bridge -- - comes from a hand-built lab that ran before the azd rewrite. A reader - who takes the header at face value goes looking for resources `azd up` - never creates, so the note has to be at the top, above the numbers, not - inferred from a bridge caveat 240 lines down. - """ - text = VALIDATION_RESULTS.read_text() - header = text.split("\n## ", 1)[0] - note = "\n".join(block for block in blocks(header) if block.lstrip().startswith(">")) - - assert note, "validation-results.md opens with no note about what it records" - assert "azd" in note - assert re.search(r"(이전|예전|과거)", note), note - assert FIXED_RESOURCE_GROUP in note, note - assert re.search(r"(현재|지금).{0,40}(실습|lab)", note), note - - -def test_dynamic_thresholds_labels_the_logic_app_event_path_as_legacy(): - """The recommended-settings section proposed the Action Group → Logic - App → HTTP Trigger chain as *the* event path. That bridge is the - historical one this lab no longer deploys; naming it without the label - tells an operator to rebuild it for a dynamic rule that should ride the - same Azure Monitor incident platform the standard exercise uses. - """ - text = DYNAMIC_THRESHOLDS.read_text() - - logic_app_lines = [line for line in text.splitlines() if "Logic App" in line] - assert logic_app_lines, "the document no longer mentions the bridge at all" - for line in logic_app_lines: - assert re.search(r"(레거시|legacy)", line), line - assert "incident platform" in text - assert re.search(r"(기본|표준).{0,60}incident platform", text) or re.search( - r"incident platform.{0,60}(기본|표준)", text - ), "the standard Azure Monitor path is not named as the default" - - -def test_autonomy_screenshots_warn_that_the_lab_must_choose_review(): - """Both response-plan screenshots come from the Learn tutorial and show - `Autonomous`, which is the mode this lab must not use: the scripts own - fault injection and recovery, so an autonomous Agent and the scripts - would revert the same resources at once. The alt text may not claim the - picture shows `Review` (see FORBIDDEN_ALT_CLAIMS), which leaves the - caption as the only place that can warn a reader copying the screen. - """ - autonomy_screenshots = ( - "portal-incident-response-plans-list.png", - "portal-response-plan-autonomy-step.png", - ) - captions = {} - for path in guide_paths(): - lines = path.read_text().splitlines() - for index, line in enumerate(lines): - match = re.match(r"!\[[^\]]*\]\(\.\./assets/official/([^)]+)\)", line.strip()) - if not match: - continue - caption = [] - for following in lines[index + 1 :]: - stripped = following.strip() - if stripped.startswith(">"): - caption.append(stripped) - elif stripped: - break - captions[match.group(1)] = "\n".join(caption) - - for name in autonomy_screenshots: - caption = captions.get(name, "") - assert caption, name - assert "Autonomous" in caption, name - assert "Review" in caption, name - assert re.search(r"(주의|경고)", caption), (name, caption) - assert re.search(r"(고릅니다|선택합니다|골라야|선택해야)", caption), (name, caption) - - -def test_scenario_guides_say_a_rerun_retires_the_previous_attempt(): - """The guide records the new attempt before injecting - anything, which clears whatever the previous attempt recorded -- - including a `conclusion` that was already unblocking the next - scenario. An operator who re-runs a captured scenario to collect a - second capture has to know the gate closes again at that moment, and - stays closed if the re-run dies before it recovers. - """ - for name, scenario in SCENARIO_GUIDES.items(): - text = (GUIDES / name).read_text() - rerun = [line for line in text.splitlines() if "다시" in line or "새 시도" in line] - assert rerun, name - joined = "\n".join(rerun) - assert "{0}_captured".format(scenario) in joined, name - assert re.search(r"(지워|지웁|초기화|사라)", joined), (name, joined) - - -def test_s1_guide_documents_the_full_stateful_alert_resolution_budget(): - text = (GUIDES / "02-scenario-s1.md").read_text() - - assert "25분" in text - - -def test_agent_setup_guide_creates_roles_and_captures_assignment_ids(): - text = (GUIDES / "01-agent-setup.md").read_text() - - assert text.count("az role assignment create") >= 4 - assert "--assignee-object-id" in text - assert '"Reader"' in text - assert '"Monitoring Contributor"' in text - assert "--query id" in text - assert "MONITORING_CONTRIBUTOR_ASSIGNMENT_ID" in text - assert "UAMI_MONITORING_CONTRIBUTOR_ASSIGNMENT_ID" in text - assert "az role assignment list" in text - assert "jq -e" in text - assert "같은 셸" in text - assert "agent-setup.json is incomplete" in text - assert "exit 1" not in text - assert re.search(r"\n\s+false\n", text) - assert 'test("^https://[^<>]+$")' in text - - -def test_s3_guide_documents_propagation_wait_and_manual_restore_command(): - text = (GUIDES / "04-scenario-s3.md").read_text() - - assert "최대 5분" in text - assert "did not produce HTTP 503 within 300s" in text - assert "AZURE_BLOB_ROLE_ASSIGNMENT_NAME" in text - assert "AZURE_CONTAINER_APP_PRINCIPAL_ID" in text - assert "AZURE_STORAGE_CONTAINER_SCOPE" in text - assert "az role assignment create" in text - - -MANUAL_SCENARIO_COMMANDS = { - "02-scenario-s1.md": ( - "az containerapp update", - "--set-env-vars FAILURE_MODE=http500", - "--set-env-vars FAILURE_MODE=none", - ), - "03-scenario-s2.md": ( - "az containerapp update", - "--set-env-vars ORDER_DELAY_MS=4000", - "--set-env-vars ORDER_DELAY_MS=0", - ), - "04-scenario-s3.md": ( - "az role assignment delete", - "--ids", - "az role assignment create", - ), -} - - -def test_scenario_guides_lead_with_the_real_azure_commands(): - """A script that runs the scenario hides the operation it performs, - which is the part worth learning. Each scenario guide states the injection and the revert - as commands the operator runs, so the lab teaches the Azure change - instead of a wrapper.""" - for name, commands in MANUAL_SCENARIO_COMMANDS.items(): - text = (GUIDES / name).read_text() - manual_index = text.index("## 수동 실행") - manual_section = text[manual_index:].split("\n## ", 1)[0] - for command in commands: - assert command in manual_section, (name, command) - - -def test_scenario_guides_keep_every_manual_step_runnable(): - """A manual run still has to produce the same evidence and recorded - state the scorer reads, or the manual path dead-ends at scoring.""" - for name, scenario in SCENARIO_GUIDES.items(): - text = (GUIDES / name).read_text() - manual_section = text[text.index("## 수동 실행"):].split("\n## ", 1)[0] - assert "scripts/loadgen.py" in manual_section, name - assert "az rest" in manual_section, name - assert ( - "scripts/lab_state.py mark-recovered {0}".format(scenario) - in manual_section - ), name - assert "scripts/capture_agent.py" in manual_section, name - assert ( - "scripts/lab_state.py record-capture {0}".format(scenario) - in manual_section - ), name - - -def test_no_guide_offers_a_script_that_runs_the_scenario_for_the_operator(): - """A one-command shortcut is the fastest way to finish the lab having - learned nothing: the operator watches a script scroll instead of making - the Azure change and seeing what the Agent does with it. There is no - shortcut section, and nothing dispatches the scenarios.""" - for path in guide_paths() + [README]: - text = path.read_text() - assert "## 지름길" not in text, path.name - assert "lab.sh" not in text, path.name - - -def test_validation_results_keeps_the_one_minute_static_run_and_explains_it(): - """The recorded S1/S2/S3 run used a one-minute static threshold. That - result stays plausible because the later live failure was the legacy - Application Insights schema on the component scope, not the cadence, and - the report must say so instead of leaving readers to assume the run used - an unsupported configuration. - """ - text = VALIDATION_RESULTS.read_text() - - assert "1분 evaluation의 static threshold" in text - assert "QueryNotContainKnownTable" in text - assert "AppRequests" in text - - -def test_scenario_guides_name_the_workspace_table_not_the_legacy_schema(): - """`infra/alerts.bicep` scopes every rule to the Log Analytics workspace - and queries the workspace-schema `AppRequests`/`AppDependencies` tables. - The legacy Application Insights component-scope names (`requests`, - `dependencies`) are not tables there -- they are functions over the - workspace tables that reject one-minute evaluation (see - dynamic-thresholds.md and validation-results.md). Each scenario guide's - "Azure에서 발생하는 변화" table must name the table the deployed alert - rule actually queries, not the legacy schema, so a reader who only - reads the guide never learns a query the rule cannot run. - """ - for name, table in SCENARIO_GUIDE_TABLES.items(): - text = (GUIDES / name).read_text() - assert "`{0}`".format(table) in text, name - assert "`requests`" not in text, name - assert "`dependencies`" not in text, name - - -def test_runbook_distinguishes_the_alert_scope_from_the_workload_impact(): - """The alert rules are scoped to the Log Analytics workspace - (`infra/alerts.bicep`'s `scopes`/`targetResourceTypes: - Microsoft.OperationalInsights/workspaces`), so Azure Monitor's own - "affected resource" for the alert *is* that workspace -- not the - Container App. The `AppRequests`/`AppDependencies`/`AppExceptions` rows - the runbook queries next additionally carry a `_ResourceId` column that - resolves to the Application Insights component, one indirection closer - but still not the workload. Neither of those is the incident's impact - scope: the runbook must tell the Agent to identify the actually - affected Container App/service from the telemetry itself - (`AppRoleName`/`Name`), and must say not to report the alert's own - scope as that impact. - """ - text = RUNBOOK.read_text() - - assert "Log Analytics workspace" in text - assert "_ResourceId" in text - assert "AppRoleName" in text - assert re.search(r"[Nn]ot (the )?(Container App|workload)", text) - assert re.search(r"[Dd]o not report .*workspace.* as", text) - - -def test_results_guide_impact_scope_rejects_the_alert_target_as_the_answer(): - """Scoring guidance for `impact_scope` must not credit an answer that - only restates the alert rule's own scope (the Log Analytics workspace) - or the `_ResourceId` telemetry column (the Application Insights - component) -- both are one or two indirections away from the actually - affected Container App/service, which is what the criterion scores. - """ - text = RESULTS_GUIDE.read_text() - - assert "impact_scope" in text - assert "Log Analytics workspace" in text - assert "_ResourceId" in text - assert "AppRoleName" in text - - -def test_deployment_plan_does_not_overclaim_appinsights_resource_id_usage(): - """`appInsightsResourceId` (observability -> lab -> main.bicep) remains - an output for backward compatibility, but no lab script and no module - reads it anymore: `infra/alerts.bicep` takes `workspaceResourceId`, and - every script that queries telemetry reads `workspaceCustomerId` - (`scripts/common.sh`'s `deployment_output`/`load_lab_config`). The plan - must not claim scripts still consume `appInsightsResourceId`. - """ - text = DEPLOYMENT_PLAN.read_text() - - assert "appInsightsResourceId" in text - assert "workspaceCustomerId" in text - assert "scripts that read it" not in text - assert not re.search(r"appInsightsResourceId[^\n.]*scripts that read", text) - - -def test_readme_is_a_quickstart_not_the_full_walkthrough(): - """Scenario, capture and scoring detail belongs in the guides.""" - text = README.read_text() - - assert len(text.splitlines()) <= 200, "README is no longer a quickstart" - for moved in ("impact_scope", "conclusion-review.json", "FAILURE_MODE=http500"): - assert moved not in text, moved - assert "impact_scope" in (GUIDES / "05-results.md").read_text() - - -def test_readme_links_every_numbered_guide_in_order(): - text = README.read_text() - - positions = [text.index("guides/{0}".format(name)) for name in GUIDE_NAMES] - assert positions == sorted(positions) - - -def test_readme_troubleshooting_index_routes_to_the_step_documents(): - text = README.read_text() - heading = "## 문제 해결" - - assert heading in text - section = text.split(heading, 1)[1] - assert "azd env get-value" in section - assert "guides/" in section - - -def test_manual_steps_distinguish_product_path_from_old_bridge(): - text = README.read_text() - assert "Azure Monitor incident platform" in text - assert "기본 실습에는 Logic App bridge를 배포하지 않습니다" in text - - -def test_logic_app_bridge_is_only_described_as_legacy(): - for path in all_docs(): - for block in blocks(path.read_text()): - if "Logic App" not in block: - continue - assert "레거시" in block, (path.name, block) - - -def test_incident_platform_path_is_the_documented_default(): - setup = (GUIDES / "01-agent-setup.md").read_text() - - assert "Builder > Incident platform" in setup - assert "Azure Monitor" in setup - plan_index = setup.index("Review") - assert setup.index("Builder > Incident platform") < plan_index - - -def test_azuresre_dev_audience_fact_is_preserved_as_legacy_history(): - """Finding #3: the Task 6 rewrite deleted a verified fact (the HTTP - Trigger endpoint only accepts an `https://azuresre.dev` audience token, - not `https://management.azure.com/`) instead of relocating it. It must - survive somewhere user-facing, framed as historical record for the - (non-default) Logic App bridge -- not restored as a default-flow - instruction anywhere in the README/guides.""" - text = VALIDATION_RESULTS.read_text() - - assert "azuresre.dev" in text - assert "management.azure.com" in text - audience_block = next(block for block in blocks(text) if "azuresre.dev" in block) - assert "레거시" in audience_block - - for path in all_docs(): - assert "azuresre.dev" not in path.read_text(), path.name - - -def test_readme_and_guides_use_exactly_one_cd_per_document(): - """Finding #4: every command in a document must be runnable - sequentially from the single working directory that document's own - (at most one) `cd` establishes -- a second `cd monitor/sre-agent-event-lab` - later in the same document would fail, since no such nested directory - exists once the first `cd` already landed there.""" - for path in all_docs(): - cd_count = len(re.findall(r"(?m)^cd\s+\S", path.read_text())) - assert cd_count <= 1, (path.name, cd_count) - - -def test_readme_scorecard_row_states_per_scenario_and_overall_maximums(): - """Finding #5: `scripts/score.py` computes MAX_POINTS = 10 per scenario - and MAX_POINTS * len(SCENARIOS) = 30 overall; the README's summary - table must describe both, not label the whole lab "10점 만점".""" - text = README.read_text() - - row = next(line for line in text.splitlines() if "scorecard.json" in line) - assert "10점" in row - assert "30점" in row - - -def test_guide02_start_conditions_do_not_claim_an_unenforced_concurrency_lock(): - """Finding #6: `lab_state.py` has no concurrency lock (its own - docstring says so); S1's start conditions must only claim what - `RUN_REQUIREMENTS["s1"]` actually enforces.""" - text = (GUIDES / "02-scenario-s1.md").read_text() - conditions = text.split("## 시작 조건", 1)[1].split("\n## ", 1)[0] - - assert "진행 중인 다른 시나리오가 없습니다" not in conditions - assert "baseline_passed" in conditions - assert "agent_setup_acknowledged" in conditions - - -def test_guide05_generate_notifications_runs_under_plain_python3(): - """Finding #6: `generate_notifications.py` only imports the standard - library (html, json, re, email.*, pathlib, typing) -- unlike - `render_capture.py`, it has no Pillow/venv dependency, so the guide - must not invoke it through `app/.venv/bin/python`.""" - text = (GUIDES / "05-results.md").read_text() - - assert "python3 scripts/generate_notifications.py" in text - assert "app/.venv/bin/python scripts/generate_notifications.py" not in text - - -# --- guide structure ----------------------------------------------------- - - -def test_guides_directory_holds_exactly_the_five_numbered_guides(): - assert {path.name for path in GUIDES.glob("*.md")} == set(GUIDE_NAMES) - - -def test_every_guide_opens_with_prerequisites_and_closes_with_a_next_step(): - for path in guide_paths(): - text = path.read_text() - assert "## 시작 조건" in text, path.name - assert "## 다음 단계" in text, path.name - assert text.index("## 시작 조건") < text.index("## 다음 단계"), path.name - - -def test_scenario_guides_use_the_required_section_order(): - required = [ - "## 시작 조건", - "## 수동 실행", - "## Azure에서 발생하는 변화", - "## SRE Agent에서 확인할 항목", - "## 성공·부분 성공·실패 판정", - "## 복구 확인", - "## 다음 단계", - ] - for name in SCENARIO_GUIDES: - text = (GUIDES / name).read_text() - positions = [text.index(heading) for heading in required] - assert positions == sorted(positions), name - - -def test_each_guide_hands_off_to_the_next_document(): - for index, name in enumerate(GUIDE_NAMES[:-1]): - section = (GUIDES / name).read_text().split("## 다음 단계", 1)[1] - assert GUIDE_NAMES[index + 1] in section, name - - final = (GUIDES / GUIDE_NAMES[-1]).read_text().split("## 다음 단계", 1)[1] - assert "azd down --purge" in final - - -def test_scenario_guides_name_the_injected_change_and_its_alert_rule(): - expected = { - "02-scenario-s1.md": ("FAILURE_MODE=http500", "alert-sre-lab-s1-http500", "500"), - "03-scenario-s2.md": ("ORDER_DELAY_MS=4000", "alert-sre-lab-s2-latency", "p95"), - "04-scenario-s3.md": ( - "Storage Blob Data Reader", - "alert-sre-lab-s3-storage-rbac", - "403", - ), - } - for name, markers in expected.items(): - text = (GUIDES / name).read_text() - for marker in markers: - assert marker in text, (name, marker) - - -def test_scenario_guides_judge_success_partial_and_failure_with_recovery(): - for name, scenario in SCENARIO_GUIDES.items(): - text = (GUIDES / name).read_text() - verdict = text.split("## 성공·부분 성공·실패 판정", 1)[1].split("\n## ", 1)[0] - for state in ("conclusion", "thread-not-created", "investigation-missing", "conclusion-missing"): - assert state in verdict, (name, state) - - recovery = text.split("## 복구 확인", 1)[1].split("\n## ", 1)[0] - assert "Resolved" in recovery, name - assert "state.json" in text, name - assert "lab_state.py begin-run {0}".format(scenario) in text, name - assert "lab_state.py record-capture {0}".format(scenario) in text, name - - -def test_results_guide_documents_the_scoring_thresholds_and_manual_gap(): - text = (GUIDES / "05-results.md").read_text() - - for marker in ("8", "5", "MANUAL", "INCOMPLETE", "scorecard.json"): - assert marker in text, marker - for criterion in ( - "impact_scope", - "direct_cause", - "actual_evidence", - "safe_minimum_mitigation", - "uncertainty", - ): - assert criterion in text, criterion - - -# --- commands match the scripts ----------------------------------------- - - -def test_every_script_the_docs_name_actually_exists(): - """Deleting a script has to delete the instructions that call it, or the - operator meets `No such file or directory` mid-lab.""" - referenced = set(re.findall(r"scripts/([A-Za-z0-9_.-]+\.(?:sh|py))", joined_docs())) - - assert referenced, "the documents name no script at all" - for name in sorted(referenced): - assert (LAB_ROOT / "scripts" / name).is_file(), name - - -def test_agent_setup_guide_matches_the_interactive_acknowledge_contract(): - text = (GUIDES / "01-agent-setup.md").read_text() - - assert "scripts/lab_state.py acknowledge-agent" in text - assert "acknowledge" in text - assert "표준 입력" in text or "stdin" in text - assert re.search(r"환경 변수[^.]{0,60}(대체할 수 없|불가)", text), ( - "the guide must say no environment variable can replace the typed word" - ) - - -def test_agent_setup_guide_offers_azd_env_set_without_storing_secrets(): - text = (GUIDES / "01-agent-setup.md").read_text() - - for setting in ( - "azd env set SRE_AGENT_NAME", - "azd env set SRE_AGENT_RESOURCE_ID", - "azd env set SRE_REPOSITORY_URL", - "azd env set SRE_KNOWLEDGE_PATH", - ): - assert setting in text, setting - assert "evidence/agent-setup.json" in text - for key in ("agent_principal_id", "agent_user_assigned_principal_id", "agent_endpoint"): - assert key in text, key - - -def test_agent_setup_guide_offers_a_python_environment_remedy(): - """The first commands an operator runs need `app/.venv`, and a missing - or half-built one is a local failure with a local fix. The guide's - failure table has to say so: the remedy is independent of the - postprovision-hook ordering contract (see - `test_setup_venv_orders_local_recovery_correctly` below), so it may -- - and should -- name `./scripts/setup-venv.sh` directly. - """ - text = (GUIDES / "01-agent-setup.md").read_text() - heading = "## 실패했을 때" - - assert heading in text - section = text.split(heading, 1)[1] - assert "app/.venv" in section - assert "./scripts/setup-venv.sh" in section - - -def test_readme_documents_the_two_phase_provision_and_deploy_flow(): - """`azd provision` alone leaves the public placeholder image running: - the lab image is built and switched in only during the deploy phase, - after the workload identity's `AcrPull` grant is observable at the - registry. The README's deployment section is the one place an operator - learns that, so it must name both phases and the gate between them -- - and must not describe the image build as part of postprovision. - """ - text = README.read_text() - heading = "## 배포" - - assert heading in text - section = text.split(heading, 1)[1].split("## ", 1)[0] - - assert "azd up" in section - assert "azd provision" in section - assert "azd deploy" in section - assert "AcrPull" in section, ( - "the gate the deploy phase waits on has to be named" - ) - assert "postprovision" not in section or "setup-venv.sh" in section - assert not re.search(r"postprovision[^\n]*(ACR 빌드|이미지 교체)", section), ( - "the README must not describe the ACR build or the image switch as " - "part of the postprovision hook" - ) - - -def test_readme_recovery_matches_the_hook_that_actually_failed(): - """The provision-phase hook only prepares `app/.venv`, so - `./scripts/setup-venv.sh` is a complete recovery for it -- and the - application deployment is a separate `azd deploy` the operator still - has to run. - """ - text = README.read_text() - heading = "## 배포" - section = text.split(heading, 1)[1].split("## ", 1)[0] - - assert "setup-venv.sh" in section - assert "azd deploy" in section - - -def test_guides_do_not_request_secrets_in_environment(): - text = "\n".join(path.read_text() for path in GUIDES.glob("*.md")) - for forbidden in ("GITHUB_PAT=", "OAUTH_TOKEN=", "CLIENT_SECRET="): - assert forbidden not in text - - -def test_docs_never_show_a_credential_value(): - text = joined_docs() - - for forbidden in SECRET_ASSIGNMENTS: - assert forbidden not in text, forbidden - for pattern in (r"ghp_[A-Za-z0-9]", r"github_pat_", r"\bsig=", r"--password\b"): - assert not re.search(pattern, text), pattern - - -def test_docs_do_not_pin_the_original_subscription_or_resource_group(): - """No subscription ID at all, not just the one the original validation - run used: the guides are followed against whatever subscription the - reader owns, and a GUID in the prose is either someone else's or a - disclosure. Forbidding the shape also keeps this file from having to - restate a real subscription ID in order to prove it is gone. - """ - for path in all_docs() + [RUNBOOK]: - text = path.read_text() - leaked = UUID_PATTERN.search(text) - assert leaked is None, "{0}: {1}".format(path.name, leaked.group()) - assert FIXED_RESOURCE_GROUP not in text, path.name - - -def test_the_guides_state_that_one_unfinished_run_stops_every_scenario(): - """The S1 guide told operators that nothing stops two scenarios from - overlapping and that keeping them apart was their own discipline. It is - enforced now -- a scenario that is `running` or `failed` refuses every - other scenario's run, not just the next one -- and a guide that still - describes the old, unenforced rule teaches an operator to misread the - refusal they will actually hit. - """ - text = (GUIDES / "02-scenario-s1.md").read_text() - - assert "잠금이 없" not in text, ( - "the S1 guide still says nothing stops two scenarios overlapping" - ) - assert re.search(r"(running|failed|실행 중|실패)", text), text[:400] - assert "mark-failed" in text, ( - "the guide must name the command that ends a run stuck at `running`" - ) - - -def test_the_later_scenario_guides_name_the_lab_wide_precondition(): - """S2 and S3 list their own predecessor's recovery and capture. Neither - mentioned the condition that actually stops them most often after a - re-run: some *other* scenario left unfinished.""" - for name in ("03-scenario-s2.md", "04-scenario-s3.md"): - text = (GUIDES / name).read_text() - conditions = text.split("## 시작 조건", 1)[1].split("\n## ", 1)[0] - assert re.search(r"(다른 시나리오|나머지 시나리오)", conditions), name - assert re.search(r"(running|failed|실행 중|실패)", conditions), name - - -def test_runbook_scopes_itself_to_the_provisioned_resource_group(): - text = RUNBOOK.read_text() - - assert "AZURE_RESOURCE_GROUP" in text - assert "purpose=sre-agent-event-lab" in text - - -# --- links and screenshots ---------------------------------------------- - - -def test_every_relative_link_in_the_walkthrough_resolves(): - checked = 0 - for path in all_docs(): - targets = re.findall(r"\]\((?!https?://|mailto:)([^)#]+)", path.read_text()) - for target in targets: - checked += 1 - assert (path.parent / target).resolve().exists(), (path.name, target) - assert checked - - -def test_guides_render_only_local_official_screenshots(): - for path in guide_paths(): - for alt, target in images(path.read_text()): - assert not target.startswith(("http://", "https://")), target - assert target.startswith("../assets/official/"), target - resolved = (path.parent / target).resolve() - assert resolved.is_file(), target - assert resolved.read_bytes()[:8] == b"\x89PNG\r\n\x1a\n", target - assert alt.strip(), target - - -def test_selected_screenshots_are_stored_and_referenced_exactly_once(): - rendered = [] - for path in guide_paths(): - rendered.extend(target.rsplit("/", 1)[-1] for _, target in images(path.read_text())) - - assert sorted(rendered) == sorted(GUIDE_SCREENSHOTS), rendered - for name in GUIDE_SCREENSHOTS: - assert (OFFICIAL_ASSETS / name).is_file(), name - - -def test_no_result_screenshots_are_copied_from_the_tutorial(): - """Investigation results are shown with this lab's own captures.""" - stored = {path.name for path in OFFICIAL_ASSETS.glob("*")} - - for tutorial_only in ( - "incident-completed.png", - "incident-full-page-top.png", - "incident-full-page-code-fix.png", - "response-plan-step-1.png", - ): - assert tutorial_only not in stored, tutorial_only - - -def test_every_screenshot_names_its_learn_source_next_to_the_image(): - for path in guide_paths(): - lines = path.read_text().splitlines() - for index, line in enumerate(lines): - match = re.match(r"!\[[^\]]*\]\(\.\./assets/official/([^)]+)\)", line.strip()) - if not match: - continue - name = match.group(1) - caption = "\n".join(lines[index + 1 : index + 4]) - assert caption.lstrip().startswith(">"), name - assert "출처" in caption, name - assert GUIDE_SCREENSHOTS[name] in caption, name - - -def test_screenshot_alt_text_describes_the_captured_screen_in_korean(): - alts = {} - for path in guide_paths(): - for alt, target in images(path.read_text()): - alts[target.rsplit("/", 1)[-1]] = alt - - assert set(alts) == set(GUIDE_SCREENSHOTS) - for name, keywords in SCREENSHOT_ALT_KEYWORDS.items(): - alt = alts[name] - assert len(alt) >= 60, (name, len(alt)) - assert re.search(r"[가-힣]", alt), name - for keyword in keywords: - assert keyword in alt, (name, keyword) - - -def test_screenshot_alt_text_does_not_claim_what_the_picture_lacks(): - alts = {} - for path in guide_paths(): - for alt, target in images(path.read_text()): - alts[target.rsplit("/", 1)[-1]] = alt - - for name, forbidden in FORBIDDEN_ALT_CLAIMS.items(): - for claim in forbidden: - assert claim not in alts[name], (name, claim) - - -def test_screenshot_alt_text_is_not_repeated_as_body_prose(): - for path in guide_paths(): - text = path.read_text() - body = re.sub(r"\s+", " ", re.sub(r"!\[[^\]]*\]\([^)]*\)", "", text)) - repeated = [ - sentence - for alt, _ in images(text) - for sentence in body_sentences(alt) - if sentence in body - ] - assert repeated == [], (path.name, repeated) - - -# --- The lab runs against the operator's own fork -------------------------- - -# The Agent's GitHub connector writes issues into the repository it is -# connected to. Naming this repository as that target would send every -# participant's incident into it, so the guides must point at a fork the -# operator owns. -UPSTREAM_SLUG = "hellices/devguidesample" - - -def test_agent_setup_tells_the_operator_to_connect_their_own_fork(): - text = (GUIDES / "01-agent-setup.md").read_text() - - assert "fork" in text.lower() or "포크" in text - assert "SRE_REPOSITORY_URL" in text - - -def test_no_operator_facing_document_names_the_upstream_as_the_connected_repo(): - """`validation-results.md` and the captured assets are a record of one - past run and may keep their URLs; the documents an operator follows must - not hand the upstream repository to the Agent as its issue target.""" - for path in [README] + [GUIDES / name for name in GUIDE_NAMES]: - assert UPSTREAM_SLUG not in path.read_text(), path.name - - -def test_agent_setup_warns_that_issues_land_in_the_connected_repository(): - text = (GUIDES / "01-agent-setup.md").read_text() - - section = text.split("## 연결할 원본", 1)[1].split("\n## ", 1)[0] - assert "이슈" in section diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_repo_readme.py b/monitor/sre-agent-event-lab/scripts/tests/test_repo_readme.py deleted file mode 100644 index 52f94401..00000000 --- a/monitor/sre-agent-event-lab/scripts/tests/test_repo_readme.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Contract tests for the repository README's lab index. - -The root README is where someone arriving at this repository looks first, -so a lab that can actually be deployed and run has to be reachable from -there. These tests keep that index honest as labs are added: every runnable -lab is listed exactly once, every link resolves, and the columns a reader -uses to decide whether to start (cost, time) are filled in. - -A "runnable lab" is defined the same way the repository already -distinguishes one: a directory with its own `azure.yaml`, i.e. something -`azd up` can provision. Documentation-only guides are not labs and are not -listed here. -""" -import re -from functools import lru_cache -from pathlib import Path - - -REPO_ROOT = Path(__file__).parents[4] -README = REPO_ROOT / "README.md" - -LAB_SECTION_HEADING = "## 🧪 실습 랩" - -# Directories that are never scanned for labs: build output, virtual -# environments, and the worktrees this repository's own workflow creates. -IGNORED_PARTS = { - ".git", - ".worktrees", - ".venv", - "node_modules", - "__pycache__", -} - - -@lru_cache(maxsize=1) -def runnable_labs(): - """Every directory an operator can provision with `azd up`. - - Cached: this walks the whole repository, and every test below needs the - same answer. - """ - labs = [] - for azure_yaml in REPO_ROOT.rglob("azure.yaml"): - relative = azure_yaml.relative_to(REPO_ROOT) - if IGNORED_PARTS & set(relative.parts): - continue - labs.append(relative.parent) - return tuple(sorted(labs)) - - -def lab_section(): - text = README.read_text() - assert LAB_SECTION_HEADING in text, ( - "the README has no lab index; a reader cannot find what is runnable" - ) - start = text.index(LAB_SECTION_HEADING) - rest = text[start + len(LAB_SECTION_HEADING):] - end = rest.find("\n## ") - return rest if end == -1 else rest[:end] - - -def linked_paths(section): - """Every relative Markdown link target in the section.""" - return re.findall(r"\]\((?!https?://)([^)#]+)", section) - - -def test_every_runnable_lab_is_listed(): - labs = runnable_labs() - assert labs, "no runnable lab found; the discovery rule is wrong" - - section = lab_section() - for lab in labs: - assert str(lab) in section, ( - "{0} can be provisioned but is missing from the README index".format(lab) - ) - - -def test_a_lab_is_listed_only_once(): - """A duplicated row is how an index starts drifting from reality.""" - section = lab_section() - for lab in runnable_labs(): - rows = [ - line - for line in section.splitlines() - if line.startswith("|") and str(lab) in line - ] - assert len(rows) == 1, (lab, rows) - - -def test_every_link_in_the_index_resolves(): - section = lab_section() - targets = linked_paths(section) - assert targets, "the index lists no navigable entry" - for target in targets: - assert (REPO_ROOT / target.strip()).exists(), target - - -def test_each_row_states_the_cost_and_the_time_commitment(): - """Both labs cost money and take time; a reader decides on those two - facts before opening anything.""" - section = lab_section() - header = next(line for line in section.splitlines() if line.startswith("|")) - for column in ("소요", "과금"): - assert column in header, column - - rows = [ - line - for line in section.splitlines() - if line.startswith("|") and not set(line) <= set("|- ") - ][1:] - assert rows, "the index has a header but no entries" - for row in rows: - cells = [cell.strip() for cell in row.strip("|").split("|")] - assert all(cells), row - - -def test_the_index_explains_how_to_add_a_lab(): - """The list only stays current if the next person knows it exists.""" - section = lab_section() - assert "azure.yaml" in section, ( - "the section must state what qualifies as a lab, so a new one is added here" - ) diff --git a/project/plans/2026-09-12-public-github-pages.md b/project/plans/2026-09-12-public-github-pages.md new file mode 100644 index 00000000..6134063c --- /dev/null +++ b/project/plans/2026-09-12-public-github-pages.md @@ -0,0 +1,388 @@ +# Public GitHub Pages Implementation Plan + +> 이 파일은 구현 이력이다. 현재 문서 작성 계약은 `AGENTS.md`와 +> `docs/contributing/index.md`를 따른다. 아래 초기 작업의 파일 목록보다 +> 마지막 정리 작업에서 확정한 최소 테스트 구성이 우선한다. + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Publish the repository as a searchable public Material for MkDocs site whose navigation and indexes are generated from folders and front matter, with Microsoft Learn MCP-based technical verification guidance. + +**Architecture:** Markdown page bundles under `docs/` are the only public content source. A shared Python content model validates metadata and official sources, generates collection/service indexes at build time, and feeds both pull-request checks and Pages deployment. A workspace MCP connection supplies Microsoft Learn tools; a repository skill defines the semantic review procedure while deterministic CI validates its recorded evidence. + +**Tech Stack:** Python 3.13, pytest 9.1.1, PyYAML 6.0.3, Material for MkDocs 9.7.7, mkdocs-awesome-nav 3.3.0, mkdocs-gen-files 0.6.1, GitHub Actions, GitHub Pages + +## Global Constraints + +- The site is public and must not expose customer identifiers, credentials, internal hosts, subscriptions, tenants, or unapproved screenshots. +- Public technical pages live only under `docs/cases`, `docs/guides`, `docs/labs`, or `docs/research`. +- Adding a valid page bundle must not require editing `mkdocs.yml`, README, or a per-page navigation list. +- Full-text search must index Korean, English, headings, body text, and rendered tags. +- Every public technical page records at least one `https://learn.microsoft.com/` source and a source-check date. +- Microsoft Learn MCP tool names and parameter schemas are discovered at connection time, not hardcoded in application code. +- Existing URL compatibility is not required. +- Generated `site/`, generated index Markdown, and search index files are never committed. +- External official-site search results are optional and are not part of this implementation. +- This session executes inline because its policy does not authorize subagent dispatch. + +--- + +### Task 1: Define and test the content contract + +**Files:** +- Create: `docs-taxonomy.yml` +- Create: `scripts/docs/content.py` +- Create: `tests/docs/test_content.py` +- Create: `tests/docs/fixtures/valid-guide.md` +- Create: `tests/docs/fixtures/invalid-guide.md` +- Create: `requirements-docs.txt` + +**Interfaces:** +- Consumes: public document paths and YAML front matter +- Produces: `load_taxonomy(path)`, `load_document(path)`, `iter_public_documents(docs_dir, taxonomy)`, and `validate_document(document, taxonomy, today)` + +- [ ] **Step 1: Write failing parser and schema tests** + + Cover front matter parsing, folder/type agreement, kebab-case slugs, required common fields, type-specific fields, controlled taxonomy values, `verification_status`, source dates, and the mandatory Microsoft Learn host. + +- [ ] **Step 2: Run the tests and confirm RED** + + Run: `python -m pytest tests/docs/test_content.py -q` + + Expected: collection fails because `scripts.docs.content` does not exist. + +- [ ] **Step 3: Implement the shared model and declarative taxonomy** + + `docs-taxonomy.yml` is the only source for collection paths, document types, allowed statuses, service labels, technologies, tags, and official source hosts. `content.py` must derive path and enum validation from that data instead of duplicating lists in Python. + +- [ ] **Step 4: Run the tests and confirm GREEN** + + Run: `python -m pytest tests/docs/test_content.py -q` + + Expected: all content-contract tests pass. + +### Task 2: Build deterministic documentation checks + +**Files:** +- Create: `scripts/docs/validate_metadata.py` +- Create: `scripts/docs/validate_sources.py` +- Create: `scripts/docs/validate_links.py` +- Create: `tests/docs/test_validators.py` + +**Interfaces:** +- Consumes: Task 1 content model and repository root +- Produces: CLIs returning exit code 0 with a concise count, or exit code 1 with `path: message` diagnostics + +- [ ] **Step 1: Write failing CLI tests** + + Test valid repositories, missing front matter, wrong collection paths, missing Learn sources, non-HTTPS sources, future check dates, missing images, broken relative Markdown links, and images without alt text. + +- [ ] **Step 2: Run the tests and confirm RED** + + Run: `python -m pytest tests/docs/test_validators.py -q` + + Expected: failures report missing validator modules. + +- [ ] **Step 3: Implement minimal validator CLIs** + + All validation functions reuse `content.py`. Link checks ignore URL schemes and anchors, resolve local paths relative to each document, and report every failure in a single run. + +- [ ] **Step 4: Run the tests and confirm GREEN** + + Run: `python -m pytest tests/docs/test_validators.py -q` + + Expected: all validator tests pass. + +### Task 3: Generate a zero-maintenance site navigation and indexes + +**Files:** +- Create: `mkdocs.yml` +- Create: `docs/.nav.yml` +- Create: `docs/index.md` +- Create: `docs/tags.md` +- Create: `scripts/docs/generate_indexes.py` +- Create: `scripts/docs/hooks.py` +- Create: `docs/assets/stylesheets/extra.css` +- Create: `tests/docs/test_site_generation.py` + +**Interfaces:** +- Consumes: Task 1 documents and taxonomy +- Produces: virtual collection indexes, `services/index.md`, one virtual page per service, status/source callouts, Material tags, and browser-side search JSON + +- [ ] **Step 1: Write failing generation tests** + + Create a temporary docs tree, call the pure index-rendering functions, and assert that every valid page appears once in its collection and each declared service page. Assert that `mkdocs.yml` contains no top-level `nav:` and that `.nav.yml` ends with a catch-all `"*"` entry. + +- [ ] **Step 2: Run the tests and confirm RED** + + Run: `python -m pytest tests/docs/test_site_generation.py -q` + + Expected: generation module and MkDocs files are missing. + +- [ ] **Step 3: Implement generation and MkDocs configuration** + + Configure `search` with `ko` and `en`, Material `tags`, `gen-files` before `awesome-nav`, `use_directory_urls: true`, repository links, strict Markdown extensions, and the status/source hook. The tags page contains ``. The root `.nav.yml` explicitly orders only the stable top-level sections and includes `"*"` for unmatched future sections. + +- [ ] **Step 4: Run unit tests and a strict build** + + Run: `python -m pytest tests/docs/test_site_generation.py -q` + + Expected: tests pass. + + Run: `mkdocs build --strict` + + Expected: build completes and writes `site/search/search_index.json`. + +### Task 4: Add the Microsoft Learn verification harness + +**Files:** +- Create: `.vscode/mcp.json` +- Create: `.github/skills/verify-with-microsoft-learn/SKILL.md` +- Create: `tests/docs/test_microsoft_learn_skill.py` +- Modify: `.gitignore` + +**Interfaces:** +- Consumes: Microsoft Learn remote Streamable HTTP endpoint and changed technical pages +- Produces: a discoverable repository skill that requires claim extraction, search, full-page fetch, comparison, and front matter evidence updates + +- [ ] **Step 1: Write failing contract tests** + + Assert that the MCP JSON registers `https://learn.microsoft.com/api/mcp` as an HTTP server; the skill front matter name matches its folder; the description starts with `Use when`; and its body requires dynamic tool discovery, search, full-article fetch, mismatch handling, upstream checks, `official_sources`, `sources_checked_at`, and `verification_status`. + +- [ ] **Step 2: Run the tests and confirm RED** + + Run: `python -m pytest tests/docs/test_microsoft_learn_skill.py -q` + + Expected: tests fail because the MCP and skill files are absent. + +- [ ] **Step 3: Initialize and write the skill** + + Initialize `verify-with-microsoft-learn` with the system skill initializer under `.github/skills`, replace generated prose with a concise imperative workflow, and keep all procedural content in `SKILL.md`. Change `.gitignore` from ignoring the whole skills tree to ignoring every child except this named skill. + +- [ ] **Step 4: Validate the skill and MCP configuration** + + Run: `python -m pytest tests/docs/test_microsoft_learn_skill.py -q` + + Expected: the repository-owned skill and MCP configuration harness tests pass. + +### Task 5: Publish author and agent guidance + +**Files:** +- Replace: `README.md` +- Create: `CONTRIBUTING.md` +- Create: `AGENTS.md` +- Create: `docs/contributing/index.md` +- Create: `docs/contributing/case-template.md` +- Create: `docs/contributing/guide-template.md` +- Create: `docs/contributing/lab-template.md` +- Create: `docs/contributing/research-template.md` +- Create: `tests/docs/test_authoring_guidance.py` + +**Interfaces:** +- Consumes: content contract, validator commands, skill name, public Pages URL +- Produces: one detailed contributor source of truth, short GitHub/README entry points, and executable agent rules + +- [ ] **Step 1: Write failing guidance tests** + + Assert that README links the Pages URL and contribution entry point; AGENTS requires the Microsoft Learn skill and the three validation commands; every template uses the correct collection/type fields; and the contribution page explains folder-only automatic inclusion and case/guide separation. + +- [ ] **Step 2: Run the tests and confirm RED** + + Run: `python -m pytest tests/docs/test_authoring_guidance.py -q` + + Expected: required files and statements are missing. + +- [ ] **Step 3: Write guidance and templates** + + Keep README and root CONTRIBUTING concise. Put explanations and examples in the Pages contribution document. Put only mandatory decisions and commands in AGENTS so future rule changes have one descriptive source. + +- [ ] **Step 4: Run the tests and confirm GREEN** + + Run: `python -m pytest tests/docs/test_authoring_guidance.py -q` + + Expected: all authoring guidance tests pass. + +### Task 6: Migrate curated content and separate samples + +**Files:** +- Create: `scripts/docs/migrate_content.py` +- Create: `scripts/docs/migration_manifest.yml` +- Create: `tests/docs/test_migration.py` +- Move: public Markdown into `docs/{cases,guides,labs,research}///index.md` +- Move: executable projects and manifests into `samples///` +- Move: page images into each page bundle's `images/` + +**Interfaces:** +- Consumes: explicit old-path-to-document metadata manifest and the Task 1 taxonomy +- Produces: an idempotent migrated tree, rewritten site links, and GitHub source links for non-page artifacts + +- [ ] **Step 1: Write failing migration tests** + + In a temporary repository, verify that one guide, one case, one lab, and one research page move to page bundles; local images copy into `images/`; Markdown links become new site-relative links; code links become repository `samples/` links; evidence Markdown remains under samples; and a second run makes no changes. + +- [ ] **Step 2: Run the tests and confirm RED** + + Run: `python -m pytest tests/docs/test_migration.py -q` + + Expected: migration module and manifest do not exist. + +- [ ] **Step 3: Implement and run the migration** + + Classify incident timelines and observed failures as cases, reusable current procedures as guides, deploy-and-clean-up walkthroughs as labs, and comparisons or benchmarks as research. Seed migrated technical pages with their service's Microsoft Learn source and `verification_status: needs-review`; do not claim semantic verification that was not performed. + + Run: `python scripts/docs/migrate_content.py --apply` + + Expected: every manifest entry reports `migrated` or `already migrated`, and legacy executable trees move under `samples/`. + +- [ ] **Step 4: Run all document checks** + + Run: `python scripts/docs/validate_metadata.py` + + Expected: every published technical document satisfies its folder and metadata contract. + + Run: `python scripts/docs/validate_sources.py` + + Expected: every published technical document has a dated Microsoft Learn source; migrated unreviewed pages remain visibly marked `needs-review`. + + Run: `python scripts/docs/validate_links.py` + + Expected: no broken local page or image link and no missing image alt text. + +### Task 7: Add CI and Pages deployment + +**Files:** +- Create: `.github/workflows/docs-ci.yml` +- Create: `.github/workflows/pages.yml` +- Create: `.github/pull_request_template.md` +- Create: `tests/docs/test_workflows.py` + +**Interfaces:** +- Consumes: pinned dependencies, validator CLIs, MkDocs config +- Produces: read-only PR validation and artifact-based deployment to the `github-pages` environment + +- [ ] **Step 1: Write failing workflow tests** + + Parse workflow YAML with the GitHub `on` key preserved as a string and assert Python 3.13, least-privilege permissions, validation commands, strict build, Pages configure/upload/deploy actions, `main` trigger, concurrency, and no `gh-pages` branch write. + +- [ ] **Step 2: Run the tests and confirm RED** + + Run: `python -m pytest tests/docs/test_workflows.py -q` + + Expected: workflow files are missing. + +- [ ] **Step 3: Implement workflows and PR checklist** + + Pin document dependencies in `requirements-docs.txt`; use reviewed immutable action major versions; grant PR checks `contents: read`; and grant deployment only `contents: read`, `pages: write`, and `id-token: write`. + +- [ ] **Step 4: Run repository-wide verification** + + Run: `python -m pytest tests/docs -q` + + Expected: all documentation tests pass. + + Run: `python scripts/docs/validate_metadata.py && python scripts/docs/validate_sources.py && python scripts/docs/validate_links.py && mkdocs build --strict` + + Expected: every command exits 0 and `site/search/search_index.json` contains Korean body text, English product terms, and tag strings. + + Run: `git diff --check` + + Expected: no whitespace errors. + +### Task 8: Normalize service slugs and separate project records + +**Files:** +- Modify: `tests/docs/test_content.py` +- Modify: `tests/docs/test_authoring_guidance.py` +- Modify: `tests/docs/test_migration.py` +- Modify: `docs-taxonomy.yml` +- Modify: `scripts/docs/migration_manifest.yml` +- Modify: `docs/contributing/index.md` +- Modify: `AGENTS.md` +- Modify: `mkdocs.yml` +- Move: `docs/superpowers/specs/2026-09-12-public-github-pages-design.md` to `project/specs/2026-09-12-public-github-pages-design.md` +- Move: `docs/superpowers/plans/2026-09-12-public-github-pages.md` to `project/plans/2026-09-12-public-github-pages.md` +- Move: affected `docs///` and `samples//` directories + +**Interfaces:** +- Consumes: service identifiers from `docs-taxonomy.yml` and explicit migration entries +- Produces: provider-consistent public URLs, matching front matter and sample links, and project records outside the Pages source tree + +**Canonical service mappings:** +- `aks` → `azure-kubernetes-service` +- `application-gateway` → `azure-application-gateway` +- `azure-ai-foundry` → `microsoft-foundry` +- `azure-mysql` → `azure-database-for-mysql` +- `cosmos-db` → `azure-cosmos-db` +- `development` → `application-development` +- `hdinsight` → `azure-hdinsight` +- sample-only `app-service` → `azure-app-service` + +- [x] **Step 1: Write failing naming and placement tests** + + Assert that every taxonomy label beginning with `Azure ` has an `azure-` service slug, every label beginning with `Microsoft ` has a `microsoft-` slug, public documents use only those taxonomy slugs, migration destinations use the canonical mappings, and no `docs/superpowers/` project record remains. + +- [x] **Step 2: Run the focused tests and confirm RED** + + Run: `python -m pytest tests/docs/test_content.py tests/docs/test_authoring_guidance.py tests/docs/test_migration.py -q` + + Expected: failures identify the legacy service slugs and `docs/superpowers/` placement. + +- [x] **Step 3: Verify official product names** + + Use `.github/skills/verify-with-microsoft-learn/SKILL.md`: discover current MCP tools, search Microsoft Learn, fetch the selected product pages in full, and confirm each public product label before changing the taxonomy. + +- [x] **Step 4: Apply canonical directory and metadata moves** + + Move each affected service directory, update every document's `services` front matter, rewrite cross-document and GitHub sample links, and update the migration manifest so a dry run reports `0 planned changes`. + +- [x] **Step 5: Move project records and remove the Pages exclusion** + + Keep the design and implementation plan under `project/`, delete the now-empty `docs/superpowers/` tree, remove the obsolete `exclude_docs: superpowers/` configuration, and document the slug policy in `docs/contributing/index.md` and `AGENTS.md`. + +- [x] **Step 6: Run complete verification** + + Run: `python -m pytest tests/docs -q` + + Run all commands required by `AGENTS.md`, then run `python scripts/docs/migrate_content.py`, `python -m pip check`, and `git diff --check`. + + Expected: every command exits 0, the migration reports `0 planned changes`, and generated search entries contain only canonical service URLs. + +- [ ] **Step 7: Commit and update PR #59** + + Commit the reviewed changes without amending existing commits, push `docs/public-github-pages`, update the PR description, and wait for Documentation CI and Oryx checks to pass. + +### Task 9: Close review gaps in public validation and sample CI + +**Files:** +- Modify: `scripts/docs/validate_public_safety.py` +- Modify: `scripts/docs/validate_metadata.py` +- Modify: `.github/workflows/docs-ci.yml` +- Modify: `samples/azure-monitor/source-material/sre-agent-event-lab/app/requirements-dev.txt` +- Modify: `samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_dynamic_threshold_brief.py` +- Move: Dynamic Threshold chart into its page bundle +- Modify: Azure AI Search benchmark defaults and ConfigMap placeholders +- Add or modify: regression tests under `tests/docs/` + +- [ ] **Step 1: Prove each reported gap with a failing regression test** + + Cover SVG/XML scanning, invalid UTF-8 failure, stray collection Markdown, explicit Azure resource configuration, bundle-local images, sample dependency compatibility, and sample-suite execution in CI. + +- [ ] **Step 2: Apply the minimal validator, sample, bundle, and workflow fixes** + + Keep the published document schema unchanged and avoid adding manually maintained navigation entries. + +- [ ] **Step 3: Run focused tests and all repository-required validation** + + Run `python -m pytest tests/docs -q`, the Dynamic Threshold tests, every command required by `AGENTS.md`, dependency check, and whitespace check. + +- [ ] **Step 4: Update PR #59 and resolve the review threads** + + Push a new commit, wait for required GitHub checks, reply inline with the verified change for each comment, and resolve each thread only after CI succeeds. + +### Task 10: Remove one-time tooling and keep only contract tests + +- [x] Delete the completed content migration script, manifest, and migration tests. +- [x] Delete tests that pin README wording, workflow step layout, and relocated sample paths. +- [x] Keep focused coverage for the content contract, validators, generated site and search, + public-safety failures, and Microsoft Learn MCP registration. +- [x] Run the remaining tests and every validation command required by `AGENTS.md`. diff --git a/project/specs/2026-09-12-public-github-pages-design.md b/project/specs/2026-09-12-public-github-pages-design.md new file mode 100644 index 00000000..7ebf886e --- /dev/null +++ b/project/specs/2026-09-12-public-github-pages-design.md @@ -0,0 +1,637 @@ +# DevGuideSample 공개 GitHub Pages 설계 + +- 작성일: 2026-09-12 +- 상태: 구현 계획 수립 전 사용자 검토 +- 대상 저장소: `hellices/devguidesample` +- 기본 공개 URL: `https://hellices.github.io/devguidesample/` + +## 1. 배경 + +이 저장소는 Azure 관련 실제 문제 해결 사례, 일반 기술 가이드, 실행 가능한 실습, 비교·벤치마크·아키텍처 리서치를 함께 보관한다. 현재 `main` 기준으로 검색 가능한 Markdown 문서가 71개이며, 문서는 18개 최상위 영역에 분산되어 있다. + +현재 구조에서 확인한 주요 문제는 다음과 같다. + +- 문서에 일관된 front matter 메타데이터가 없다. +- 특정 시점의 장애 이력과 계속 갱신해야 할 일반 가이드가 한 문서에 섞여 있다. +- 문서, 실행 코드, 캡처 결과, 생성 산출물이 같은 디렉터리 트리에 혼재한다. +- 루트 README는 일부 실습만 색인하며 전체 문서의 탐색 진입점 역할을 하지 못한다. +- GitHub Pages와 검색용 사이트 구성은 아직 없다. +- 로컬 상대 링크 3개가 현재 깨져 있다. +- 저장소는 공개이지만 README에는 내부 지식 공유 목적이라는 표현이 남아 있어 공개 사이트 기준으로 문구와 안전 규칙을 정비해야 한다. + +문서 수와 변경 빈도가 빠르게 증가하고 있으므로 단순한 파일 목록이 아니라 문서 유형별 수명주기, 검색, 분류, 검증을 갖춘 공개 지식 사이트가 필요하다. + +## 2. 목표 + +1. 공개 GitHub Pages 사이트에서 한국어와 영어 기술 용어로 전체 문서를 검색할 수 있게 한다. +2. 문서를 문제 해결 사례, 지속 갱신형 가이드, 실습, 리서치의 네 모음으로 분리한다. +3. 서비스, 증상, 기술 스택, 문서 유형을 기준으로 검색 외 탐색 경로를 제공한다. +4. 일반 가이드에 적용 범위와 마지막 검증일을 명시해 제품 기능 변경에 맞춰 갱신할 수 있게 한다. +5. README, 기여 지침, `AGENTS.md`가 같은 작성 규칙을 가리키도록 한다. +6. Pull Request에서 사이트 빌드, 메타데이터, 내부 링크와 공개 안전성을 검증한다. +7. `main` 병합 후 생성 파일을 저장소에 커밋하지 않고 GitHub Pages에 자동 배포한다. +8. 새 문서는 정해진 폴더와 메타데이터 규칙만 지키면 별도 메뉴 편집 없이 사이트, 모음, 서비스 및 태그 색인에 자동 포함한다. +9. 공개 기술 문서의 제품 동작, 지원 상태와 절차를 Microsoft Learn 공식 문서에 대조하는 검증 하네스를 제공한다. + +## 3. 비목표 + +초기 구현에서는 다음 항목을 포함하지 않는다. + +- 로그인 또는 CSA 팀 전용 접근 제어 +- 서버 측 검색, 벡터 검색, RAG 또는 질의응답 챗봇 +- 사용자 댓글, 평가, 분석용 서버 +- 사용자 지정 도메인 +- 기존 Markdown 파일 경로 또는 GitHub 링크의 호환성 보존 +- 모든 코드 샘플을 사이트 안에서 직접 실행하는 기능 +- 로컬 검색 결과가 없을 때 외부 공식 사이트의 결과를 실시간으로 수집하는 서버 + +## 4. 핵심 결정 + +| 항목 | 결정 | 근거 | +|---|---|---| +| 사이트 생성기 | Material for MkDocs | 기존 Markdown과 호환되고 한국어를 지원하는 내장 클라이언트 검색을 제공한다. | +| 배포 | GitHub Actions 기반 GitHub Pages | 빌드 결과를 브랜치에 커밋하지 않고 Pages 아티팩트로 배포할 수 있다. | +| 검색 | MkDocs `search` 플러그인, `ko`와 `en` | 별도 외부 검색 서비스 없이 제목, 본문, 섹션을 브라우저에서 검색한다. | +| 분류 색인 | Material `tags`와 빌드 시 생성하는 서비스 색인 | front matter를 기준으로 문서를 자동 모아 수동 목록의 누락을 막는다. | +| 정보 구조 | 문서 유형 우선, 서비스와 태그는 교차 색인 | 문서 수명주기를 분명히 하면서 서비스별 탐색도 제공한다. | +| 내비게이션 | `mkdocs-awesome-nav`와 분산된 `.nav.yml` | 전체 문서 목록을 한 파일에 수동 관리하지 않고 새 문서를 자동 포함한다. | +| 문서 검증 근거 | Microsoft Learn MCP와 명시적 공식 출처 메타데이터 | 작성 에이전트가 최신 Microsoft 공식 문서를 검색하고 원문을 조회한 뒤 기술 주장을 교차 검증한다. | +| MCP 연동 | 워크스페이스 MCP 설정과 repository skill 분리 | 연결 정보와 검증 절차를 각각 관리하고 MCP 도구의 동적 변경에 대응한다. | +| 공개 방식 | 프로젝트 Pages 사이트 | 기본 URL은 `https://hellices.github.io/devguidesample/`이다. | +| 생성물 | `site/`은 Git에서 제외 | 소스 Markdown만 버전 관리한다. | + +## 5. 저장소 구조 + +```text +devguidesample/ +├── docs/ +│ ├── index.md +│ ├── cases/ +│ │ └── // +│ │ ├── index.md +│ │ └── images/ +│ ├── guides/ +│ │ └── // +│ │ ├── index.md +│ │ └── images/ +│ ├── labs/ +│ │ └── //index.md +│ ├── research/ +│ │ └── //index.md +│ ├── services/ +│ ├── tags.md +│ ├── overrides/ +│ ├── contributing/ +│ │ ├── index.md +│ │ ├── case-template.md +│ │ ├── guide-template.md +│ │ ├── lab-template.md +│ │ └── research-template.md +│ └── assets/ +├── samples/ +│ └── // +├── project/ +│ ├── specs/ +│ └── plans/ +├── scripts/docs/ +│ ├── generate_indexes.py +│ ├── validate_metadata.py +│ └── validate_sources.py +├── .vscode/ +│ └── mcp.json +├── .github/skills/ +│ └── verify-with-microsoft-learn/ +│ └── SKILL.md +├── .github/workflows/ +│ ├── docs-ci.yml +│ └── pages.yml +├── .github/pull_request_template.md +├── README.md +├── CONTRIBUTING.md +├── AGENTS.md +├── docs-taxonomy.yml +├── mkdocs.yml +└── requirements-docs.txt +``` + +각 공개 문서는 `index.md`와 그 문서 전용 `images/`를 함께 두는 page bundle 형태를 사용한다. 실행 코드, 배포 매니페스트, 대용량 샘플 데이터는 `samples/`에 두고 문서에서 GitHub 소스 링크로 연결한다. + +`project/specs/`와 `project/plans/`는 저장소 작업 기록용이며 공개 문서 소스인 `docs/`와 분리한다. `docs-taxonomy.yml`은 서비스, 기술, 태그, 문서 유형과 상태의 허용 식별자를 관리하는 단일 기준 파일이다. 생성기와 검증기는 이 파일을 읽으며 같은 목록을 코드에 다시 하드코딩하지 않는다. + +`.vscode/mcp.json`은 Microsoft Learn 원격 MCP 서버 연결만 정의한다. `.github/skills/verify-with-microsoft-learn/SKILL.md`는 연결된 MCP를 사용해 기술 문서를 검증하는 절차를 정의한다. 현재 `.gitignore`의 `.github/skills/` 전체 제외 규칙은 이 repository skill 디렉터리만 명시적으로 추적하도록 allowlist 형태로 바꾼다. 개인용 또는 생성된 다른 skill은 계속 무시한다. + +## 6. 사이트 정보 구조 + +### 6.1 전역 내비게이션 + +사이트는 다음 최상위 메뉴를 제공한다. + +1. 홈 +2. 문제 해결 사례 +3. 일반 가이드 +4. 실습 +5. 리서치 +6. 서비스별 찾기 +7. 태그 +8. 기여하기 + +모든 페이지의 상단에는 검색과 GitHub 저장소 링크를 배치한다. 문서 페이지 왼쪽에는 현재 모음과 서비스 기준 내비게이션을, 오른쪽에는 문서 내 목차를 표시한다. + +### 6.2 홈 + +홈은 저장소 파일 목록을 복제하지 않고 다음 진입점을 제공한다. + +- 가장 눈에 띄는 위치의 검색 +- 네 문서 모음 설명과 링크 +- AKS, Azure AI Search, Azure Monitor, Azure 데이터 서비스 등 주요 서비스 링크 +- 문서 유형 선택 안내 +- 기여 지침 링크 + +초기 버전에서는 동적 인기 순위나 사용 통계를 만들지 않는다. + +### 6.3 URL 규칙 + +URL과 디렉터리 이름은 소문자 kebab-case 영문을 사용한다. Azure 제품의 서비스 slug는 `azure-`와 공식 전체 제품명을 사용하고, Microsoft 제품과 오픈소스는 각각 공식 제품명과 프로젝트명을 그대로 kebab-case로 변환한다. + +```text +/cases/azure-kubernetes-service/pod-database-query-latency/ +/guides/azure-kubernetes-service/file-io-diagnosis/ +/labs/azure-monitor/sre-agent-event-lab/ +/research/azure-ai-search/chunking-strategies/ +``` + +제목과 본문은 한국어를 기본으로 하되 제품명, API, 오류 메시지 등 검색에 유용한 공식 영문 용어를 함께 표기한다. + +### 6.4 서비스와 태그 색인 생성 + +`scripts/docs/generate_indexes.py`는 공개 문서의 front matter를 읽어 서비스별 문서 목록을 빌드 시 생성한다. `mkdocs-gen-files`를 사용해 가상 Markdown 페이지를 만들기 때문에 생성된 색인 파일은 작업 트리나 Git에 남기지 않는다. 태그 색인은 Material의 `tags` 플러그인이 생성한다. + +새 문서는 올바른 `services`와 `tags`만 지정하면 해당 색인에 자동 포함된다. 사람이 서비스별 문서 목록을 중복 편집하지 않는다. + +### 6.5 자동 내비게이션 계약 + +`mkdocs.yml`에는 문서별 `nav` 목록을 두지 않는다. 루트 `docs/.nav.yml`은 홈, 네 모음, 생성 색인과 기여 페이지의 안정적인 최상위 순서만 선언하고 `"*"` glob 또는 `append_unmatched: true`로 나머지 디렉터리와 페이지를 자동 발견한다. 개별 문서 경로를 `.nav.yml`에 열거하지 않는다. + +새 공개 문서가 Pages와 메뉴에 포함되는 조건은 다음 세 가지뿐이다. + +1. `docs////index.md`에 둔다. +2. 유효한 공통 및 문서 유형별 front matter를 작성한다. +3. 서비스 또는 태그 식별자가 새 값이면 `docs-taxonomy.yml`에 한 번 등록한다. + +빌드 시 `mkdocs-awesome-nav`가 폴더 트리에서 메뉴를 만들고, `mkdocs-gen-files`가 같은 front matter에서 모음·서비스 색인을 생성하며, Material `tags` 플러그인이 태그 색인을 생성한다. 문서 추가를 위해 `mkdocs.yml`, 루트 내비게이션, README 또는 별도 문서 목록을 수정하지 않는다. + +폴더 또는 페이지의 표시 이름과 정렬을 특별히 바꿔야 할 때만 해당 폴더에 작은 `.nav.yml`을 둔다. 이 파일은 하위 파일 목록이 아니라 `title`, `order` 같은 표시 재정의만 담는다. 이 예외도 새 문서가 자동 포함되는 기본 동작을 막지 않아야 한다. + +## 7. 콘텐츠 모델 + +### 7.1 공통 front matter + +모든 공개 문서는 다음 필드를 가진다. + +```yaml +--- +title: AKS Pod 데이터베이스 지연 분석 +description: AKS에서 발생하는 데이터베이스 호출 지연의 진단과 해결 방법 +document_type: guide +services: + - azure-kubernetes-service + - azure-database-for-mysql +technologies: + - nodejs +tags: + - latency + - connection-pool + - networking +status: current +verification_status: verified +sources_checked_at: 2026-09-12 +official_sources: + - title: Azure Kubernetes Service documentation + url: https://learn.microsoft.com/azure/aks/ +--- +``` + +| 필드 | 규칙 | +|---|---| +| `title` | 한 페이지 안에서 내용을 명확히 구분하는 제목 | +| `description` | 검색 결과에 사용할 한 문장 요약 | +| `document_type` | `case`, `guide`, `lab`, `research` 중 하나 | +| `services` | 기여 지침에 정의한 통제된 서비스 식별자 목록 | +| `technologies` | 언어, SDK, 런타임, 도구 식별자 목록 | +| `tags` | 증상, 패턴, 작업 유형을 나타내는 통제된 소문자 kebab-case 목록 | +| `status` | 문서 유형별 허용 상태 중 하나 | +| `verification_status` | 공식 원문과 의미를 대조했으면 `verified`, 추가 검토가 필요하면 `needs-review` | +| `sources_checked_at` | 기술 주장을 공식 출처와 마지막으로 대조한 날짜 | +| `official_sources` | 제목과 HTTPS URL로 구성된 공식 근거 목록. 공개 기술 문서는 Microsoft Learn URL을 하나 이상 포함한다. | + +서비스, 기술과 태그 식별자는 `docs-taxonomy.yml`과 기여 지침에서 관리한다. 같은 대상을 `aks`, `azure-kubernetes-service`처럼 중복 생성하지 않는다. 새 식별자가 필요하면 문서와 같은 PR에서 taxonomy를 갱신한다. + +홈, 생성 색인, 기여 지침과 템플릿을 제외한 모든 공개 기술 문서는 `official_sources`를 가진다. 현재 저장소의 Azure 중심 범위에서는 Microsoft 제품 동작이나 지원 상태를 설명하는 모든 문서가 `learn.microsoft.com`의 관련 원문을 하나 이상 포함해야 한다. Kubernetes, CNCF 프로젝트 또는 SDK의 세부 동작처럼 Microsoft Learn이 최종 권위가 아닌 주장에는 `kubernetes.io`, `cncf.io` 또는 해당 프로젝트 공식 문서를 추가하되 Microsoft Learn 교차 검증을 생략하지 않는다. 사례의 환경별 관측값과 타임라인은 공식 문서가 증명하는 사실로 취급하지 않고 별도 증거로 구분한다. + +### 7.2 문제 해결 사례 + +사례 문서는 특정 시점의 사실과 의사결정을 보존한다. + +추가 필드: + +```yaml +document_type: case +occurred_at: 2026-03-10 +resolved_at: 2026-03-12 +status: resolved +related_guides: + - /guides/azure-kubernetes-service/file-io-diagnosis/ +``` + +허용 상태는 `unresolved`, `resolved`, `historical`이다. + +본문 순서: + +1. 요약 +2. 발생 환경과 영향 +3. 증상과 관측 데이터 +4. 조사 과정 또는 타임라인 +5. 근본 원인 +6. 해결 방법 +7. 검증 결과 +8. 재발 방지와 교훈 +9. 관련 최신 가이드 +10. 참고 자료 + +제품 동작이 바뀌더라도 당시 사실을 현재 동작에 맞게 다시 쓰지 않는다. 대신 상태를 `historical`로 변경하거나 상단 안내문과 최신 가이드 링크를 추가한다. + +### 7.3 일반 가이드 + +가이드는 현재 시점에 재사용할 수 있는 권장 절차를 제공하며 제품 기능 변경에 따라 같은 문서를 갱신한다. + +추가 필드: + +```yaml +document_type: guide +status: current +last_verified: 2026-09-12 +review_cycle_days: 180 +applies_to: + - AKS 1.34+ +related_cases: + - /cases/azure-kubernetes-service/pod-database-query-latency/ +``` + +허용 상태는 `current`, `needs-review`, `deprecated`이다. + +본문 순서: + +1. 목표 +2. 적용 범위와 지원 버전 +3. 사전 조건 +4. 권장 아키텍처 또는 선택 근거 +5. 구성·운영 절차 +6. 검증 방법 +7. 롤백과 트러블슈팅 +8. 제약 사항과 버전별 차이 +9. 관련 사례 +10. 공식 참고 자료 + +가이드가 대체되면 삭제하지 않고 `deprecated` 상태와 대체 문서 링크를 표시한다. `last_verified`는 실제 절차나 동작을 재검증했을 때만 갱신한다. + +### 7.4 실습 + +실습은 재현 가능한 실행 흐름과 비용·안전 경계를 가진다. + +추가 필드: + +```yaml +document_type: lab +status: verified +last_verified: 2026-09-12 +estimated_time: 45m +cost: paid +cleanup_required: true +``` + +허용 상태는 `verified`, `needs-review`, `broken`, `archived`이다. + +본문에는 목표, 사전 조건, 비용과 안전, 배포, 시나리오, 예상 결과, 검증, 정리, 문제 해결을 포함한다. + +### 7.5 리서치 + +리서치는 조사 기준 시점과 근거를 명시한다. + +추가 필드: + +```yaml +document_type: research +status: current +published_at: 2026-08-24 +sources_checked_at: 2026-08-24 +official_sources: + - title: Microsoft Learn article title + url: https://learn.microsoft.com/... +``` + +허용 상태는 `current`, `superseded`, `archived`이다. + +본문에는 질문, 조사 범위와 기준일, 방법, 결과, 권고, 한계, 출처를 포함한다. + +### 7.6 혼합 문서 분리 + +실제 장애 이력과 재사용 가능한 해결 절차가 섞인 기존 문서는 두 문서로 분리한다. + +```text +cases/azure-kubernetes-service/netapp-file-io-wait/ +└── 당시 환경, 관측값, 조사, 근본 원인과 해결 결과 + +guides/azure-kubernetes-service/file-io-diagnosis/ +└── 현재 유효한 진단 절차, 권장 설정, 검증과 롤백 +``` + +두 페이지는 `related_guides`와 `related_cases`로 연결한다. 사례는 이력을 보존하고 가이드는 제품 변경에 맞춰 갱신한다. + +## 8. 검색과 분류 + +Material for MkDocs의 내장 `search` 플러그인을 사용한다. + +- 검색 언어는 `ko`와 `en`을 활성화한다. +- 제목, 본문, 섹션 제목을 검색 결과에 포함한다. `description`은 페이지 상단 요약으로 렌더링해 검색 인덱스에도 포함한다. +- 검색 결과는 해당 문서 섹션 앵커로 이동한다. +- 검색 제안과 결과 강조를 활성화한다. +- 태그는 각 문서에 렌더링해 전체 텍스트 인덱스에도 들어가게 하고, 별도 태그 색인에서 태그별 문서를 탐색할 수 있게 한다. +- 서비스와 태그는 별도 색인 페이지에서 교차 탐색에 사용한다. +- 설계 문서, 원시 캡처 타임라인, 알림 원문, 템플릿 내부 예제는 검색에서 제외한다. +- 검색 인덱스는 빌드 결과에 포함되며 브라우저에서 동작한다. + +현재 문서 규모에서는 외부 Algolia, Typesense 또는 서버 측 검색을 도입하지 않는다. + +따라서 초기 버전의 필수 검색 범위는 다음과 같다. + +- 제목, 본문과 섹션의 한국어·영어 전체 텍스트 검색 +- 태그 이름 검색 및 태그별 문서 목록 탐색 +- 서비스별 문서 목록 탐색 + +### 8.1 선택적 공식 사이트 검색 연결 + +로컬 결과가 없을 때 Microsoft Learn, CNCF와 Kubernetes 공식 사이트로 탐색을 이어 주는 기능은 초기 완료 조건에 포함하지 않는다. 정적 Pages 브라우저에서 MCP를 직접 호출하거나 각 사이트를 크롤링하지 않는다. + +후속 기능으로 구현할 때는 `docs-search-providers.yml`에 제공자 이름, 공식 도메인과 검색 URL 템플릿을 선언하고, 검색 UI는 이 데이터에서 외부 검색 링크를 만든다. 제공자별 URL을 JavaScript에 하드코딩하지 않는다. 실제 외부 문서 제목과 직접 링크까지 로컬 결과처럼 보여 주려면 각 사이트의 이용 정책과 공식 색인 제공 여부를 확인한 뒤 예약 빌드에서 제목·URL 전용 색인을 생성하는 별도 설계를 거친다. CORS를 우회하기 위한 비공식 프록시나 검색 결과 스크래핑은 사용하지 않는다. + +## 9. Microsoft Learn 기반 문서 검증 하네스 + +### 9.1 MCP 연결 + +Microsoft 공식 문서가 안내하는 원격 Streamable HTTP 엔드포인트를 워크스페이스 MCP 설정에 등록한다. + +```json +{ + "servers": { + "microsoft-learn": { + "type": "http", + "url": "https://learn.microsoft.com/api/mcp" + } + } +} +``` + +이 연결은 인증 없이 사용할 수 있는 공개 Microsoft Learn 콘텐츠를 대상으로 한다. MCP 엔드포인트는 일반 REST API가 아니므로 스크립트가 HTTP 응답 형식을 직접 가정하지 않는다. 클라이언트는 세션 시작 시 `tools/list`로 현재 도구와 스키마를 조회하고, repository skill은 도구의 설명을 기준으로 검색과 원문 조회를 선택한다. 알려진 도구 이름은 설명과 예시에만 사용할 수 있으며 하네스 코드의 고정 라우팅 조건으로 사용하지 않는다. + +### 9.2 Repository skill + +`.github/skills/verify-with-microsoft-learn/SKILL.md`는 공개 기술 문서를 새로 만들거나 기술 내용을 변경할 때 적용한다. skill은 다음 순서를 강제한다. + +1. 변경 문서에서 제품 동작, 지원 여부, 제한, 버전, 구성 단계와 CLI/API 사용법 같은 검증 대상 주장을 추출한다. +2. 연결 시 발견한 Microsoft Learn 검색 도구로 제품명과 핵심 주장별 문서를 찾는다. +3. 검색 요약만 신뢰하지 않고 선택한 Microsoft Learn 원문을 전체 조회한다. +4. 문서의 주장과 공식 원문의 적용 범위, 버전, 전제 조건 및 지원 상태를 비교한다. +5. 불일치하면 현재 문서를 수정하거나, 의도적인 역사 기록이면 사례 문서에 시점과 차이를 명시한다. +6. 사용한 원문의 제목과 정규 URL을 `official_sources`에 기록하고 `sources_checked_at`을 실제 확인일로 갱신한다. +7. Microsoft Learn만으로 판단할 수 없는 Kubernetes·CNCF·오픈소스 세부 사항은 해당 프로젝트 공식 원문으로 한 번 더 검증한다. +8. 검증하지 못한 주장을 통과한 것으로 표시하지 않고 `verification_status: needs-review`와 이유를 보고한다. + +skill은 검색 결과의 내용을 그대로 문서에 복사하지 않으며, 출처 URL만 추가하는 형식적 검사를 검증 완료로 간주하지 않는다. 사람과 에이전트가 같은 규칙을 사용하도록 기여 페이지와 루트 `AGENTS.md`에서 이 skill을 필수 절차로 연결한다. + +### 9.3 결정적 CI와 의미 검증의 경계 + +`validate_sources.py`와 GitHub Actions는 다음 항목을 결정적으로 검사한다. + +- 공개 기술 문서에 `sources_checked_at`과 `official_sources`가 있는지 +- 모든 공개 기술 문서에 유효한 `https://learn.microsoft.com/` URL이 하나 이상 있는지 +- 출처 항목에 제목이 있고 URL의 호스트가 선언된 공식 출처 정책에 맞는지 +- 출처 확인일이 미래가 아니며 `verification_status`와 검토 주기 규칙에 맞는지 +- 변경된 기술 문서가 필요한 검증 메타데이터를 함께 갱신했는지 + +CI가 출처 URL 존재만으로 본문의 의미적 정확성을 보장한다고 주장하지 않는다. 실제 주장 비교는 MCP를 사용할 수 있는 작성·리뷰 에이전트가 repository skill에 따라 수행하고, CI는 그 결과가 남긴 구조화된 메타데이터와 문서 상태를 검사한다. 향후 GitHub Actions에서 인증된 에이전트 실행 환경을 안정적으로 제공할 수 있을 때만 의미 검증을 필수 원격 잡으로 승격한다. + +## 10. 빌드와 배포 + +### 10.1 Pull Request 검사 + +`.github/workflows/docs-ci.yml`은 문서 또는 사이트 구성 변경이 포함된 Pull Request에서 다음 순서로 실행한다. 같은 워크플로의 예약 실행은 배포 없이 문서 신선도 보고만 수행한다. + +1. 저장소 체크아웃 +2. 고정된 문서 빌드 의존성 설치 +3. front matter 스키마와 통제 어휘 검사 +4. 공식 출처 필드와 Microsoft Learn URL 정책 검사 +5. 내부 문서 링크와 이미지 경로 검사 +6. 이미지 대체 텍스트 검사 +7. 생성 내비게이션과 색인에 변경 문서가 포함되는지 검사 +8. `mkdocs build --strict` + +PR 검사는 Pages 배포 권한을 갖지 않는다. + +### 10.2 `main` 배포 + +`.github/workflows/pages.yml`은 `main`에 문서 또는 사이트 구성 변경이 병합될 때 실행한다. + +```text +checkout + -> dependency install + -> metadata/link validation + -> mkdocs build --strict + -> configure Pages + -> upload site/ as Pages artifact + -> deploy to github-pages environment +``` + +워크플로 권한은 최소 범위로 제한한다. + +```yaml +permissions: + contents: read + pages: write + id-token: write +``` + +동시 배포는 Pages 전용 concurrency group으로 직렬화한다. 생성된 `site/` 디렉터리와 검색 인덱스는 커밋하지 않는다. 별도 `gh-pages` 브랜치도 사용하지 않는다. + +### 10.3 재현성 + +- `requirements-docs.txt`에 문서 도구 버전을 고정한다. +- GitHub Actions는 검토된 버전 또는 변경 불가능한 커밋 참조를 사용한다. +- 로컬과 CI 모두 같은 `mkdocs build --strict` 명령을 사용한다. + +### 10.4 최소 테스트 원칙 + +실제 저장소를 대상으로 실행하는 메타데이터, 출처, 링크, 공개 안전성, +strict build와 검색 색인 검증을 기본 품질 게이트로 사용한다. 단위 테스트는 +현재 문서만 검사해서는 확인할 수 없는 실패 경로와 사용자 데이터 노출 방지처럼 +회귀 비용이 큰 계약에만 둔다. README 문구, 워크플로 단계의 배열 순서와 같은 +구현 세부를 고정하는 테스트는 두지 않으며, 완료된 일회성 마이그레이션 도구와 +그 테스트는 저장소에서 제거한다. + +## 11. 문서 신선도 + +가이드와 실습은 `last_verified`와 `review_cycle_days`를 사용한다. + +- 새 가이드와 실습은 두 값을 반드시 포함한다. +- 사이트는 모든 가이드와 실습에 현재 `status`와 `last_verified`를 표시한다. +- `status: needs-review`인 문서는 페이지 상단에 검토 필요 안내를 표시한다. +- 오래된 문서 한 개 때문에 전체 Pages 배포를 차단하지 않는다. +- 변경된 문서의 필수 값 누락이나 잘못된 날짜는 PR을 차단한다. +- `docs-ci.yml`의 예약 실행은 주기적으로 검토 기한이 지난 문서 목록을 Actions 요약에 출력한다. +- 검토 기한이 지난 문서를 확인한 유지관리자는 실제 검증을 수행하거나 `status: needs-review`로 변경한다. +- 제품 동작을 재검증하지 않은 단순 문구 수정은 `last_verified`를 갱신하지 않는다. + +초기 구현에서는 자동 이슈 생성이나 담당자 알림 시스템을 추가하지 않는다. + +## 12. README, 기여 지침과 에이전트 규칙 + +### 12.1 README + +루트 README는 다음 내용만 간결하게 제공한다. + +- 저장소와 공개 사이트의 목적 +- GitHub Pages 사이트 링크 +- 네 문서 모음 설명 +- 로컬 미리보기 명령 +- 기여 지침 링크 +- 공개 정보 취급 주의사항 + +전체 문서 목록은 README에 수동으로 복제하지 않는다. + +### 12.2 기여 지침 + +사이트의 `docs/contributing/index.md`를 상세 작성 규칙의 기준 문서로 사용한다. 루트 `CONTRIBUTING.md`는 GitHub에서 쉽게 찾을 수 있는 짧은 진입점이며 상세 페이지를 연결한다. + +기여 지침에는 다음을 포함한다. + +- 문서 유형 선택 기준 +- 유형별 front matter와 본문 템플릿 +- 서비스와 태그 통제 어휘 +- 파일명, URL, 이미지, 링크 규칙 +- 코드 블록, Mermaid, 표와 접기 영역 규칙 +- 이미지 대체 텍스트와 접근성 기준 +- 로컬 미리보기와 검증 명령 +- 사례와 일반 가이드를 분리하는 기준 +- 공개 전 익명화와 보안 체크리스트 +- Microsoft Learn MCP 검증 절차와 공식 출처 작성 규칙 +- 문서를 올바른 폴더에 추가한 뒤 자동 메뉴·색인 포함을 확인하는 방법 + +### 12.3 `AGENTS.md` + +루트 `AGENTS.md`는 자동화 에이전트가 따라야 할 실행 가능한 규칙을 담는다. + +- 새 문서 작성 전 `document_type` 결정 +- 올바른 모음과 서비스 디렉터리 선택 +- 유형별 필수 메타데이터와 섹션 준수 +- 사례의 역사적 사실과 가이드의 현재 권고 분리 +- 문서 전용 이미지는 page bundle의 `images/`에 저장 +- 실행 코드는 `samples/`에 저장 +- 상대 링크와 이미지 대체 텍스트 사용 +- 공개 부적합 정보 제거 +- 기술 주장 변경 시 `verify-with-microsoft-learn` skill을 사용하고 공식 출처 메타데이터 갱신 +- 변경 후 검증 명령 실행 +- 상세 규칙의 기준 문서인 기여 페이지 참조 + +기여 지침과 `AGENTS.md`에 동일한 긴 내용을 중복하지 않는다. `AGENTS.md`는 필수 계약과 명령을 요약하고, 설명과 예시는 기여 페이지에서 관리한다. + +## 13. 공개 안전성 + +공개 사이트에 게시하는 모든 콘텐츠는 다음 정보를 포함하지 않아야 한다. + +- 고객·조직·사용자 실명 +- 구독 ID, 테넌트 ID, 리소스의 실제 전체 식별자 +- 액세스 키, 토큰, 인증서, 연결 문자열 +- 내부 전용 URL, IP 주소, 호스트명 +- 원문 로그에 포함된 개인정보나 기밀 데이터 +- 공개 승인을 받지 않은 고객 구성도와 화면 캡처 + +예시는 문서 전반에서 일관된 가상 값으로 치환한다. 자동 비밀 탐지와 함께 사람의 공개 안전성 검토를 PR 체크리스트에 포함한다. 자동 검사만으로 익명화를 보장할 수 있다고 간주하지 않는다. + +## 14. 기존 콘텐츠 마이그레이션 + +마이그레이션은 다음 원칙을 따른다. + +1. 기존 문서를 사례, 가이드, 실습, 리서치로 분류한다. +2. 단일 유형 문서는 가능한 한 `git mv`로 이동해 이력을 보존한다. +3. 혼합 문서는 사례와 일반 가이드로 분리하고 상호 링크한다. +4. 문서 전용 이미지는 page bundle로 이동한다. +5. 실행 코드와 인프라 파일은 `samples/`로 이동하고 사이트에서 GitHub 링크로 연결한다. +6. 캡처 원시 산출물과 생성 로그는 내비게이션과 검색에서 제외한다. +7. 외부 공개에 부적합한 식별자와 화면을 익명화한다. +8. 기존 URL 호환성은 유지하지 않으며 새로운 사이트 URL을 기준으로 링크를 갱신한다. + +현재 확인된 깨진 링크도 마이그레이션 중 처리한다. + +| 원본 문서 | 깨진 대상 | +|---|---| +| `aisearch/custom_vectorization/01_custom_embedding_guide.md` | `indexer_vs_pushapi.png` | +| `aks/spot_gpu_kaito_llm.md` | `workspace-phi4-mini.yaml` | +| `aks/workload_identity_databricks_keyless.md` | `../apim/databricks_keyless_managed_identity.md` | + +`.azure/`, `.devcontainer/`, 설계 기록과 저장소 운영 문서는 공개 가이드 모음에 자동 포함하지 않는다. + +## 15. 검증 기준 + +다음 조건을 모두 만족하면 초기 사이트 구축이 완료된 것으로 본다. + +1. `https://hellices.github.io/devguidesample/`에서 사이트가 공개된다. +2. 한국어 문구와 영문 제품명으로 검색했을 때 관련 제목과 본문 섹션이 노출된다. +3. 사례, 가이드, 실습, 리서치의 네 모음과 서비스·태그 색인이 제공된다. +4. 공개 문서가 유형에 맞는 필수 front matter를 가진다. +5. 사례형과 가이드형 템플릿이 분명히 다르며 혼합 문서는 분리된다. +6. README, 기여 페이지와 `AGENTS.md`가 사이트 구조와 작성 규칙을 안내한다. +7. Pull Request에서 메타데이터, 링크, 이미지 대체 텍스트와 엄격 빌드가 검사된다. +8. `main` 병합 시 Pages 아티팩트가 자동 배포된다. +9. 저장소에 생성된 `site/` 또는 검색 인덱스가 추적되지 않는다. +10. 공개 사이트에 알려진 비밀이나 실제 고객 식별 정보가 포함되지 않는다. +11. 새 문서를 규칙에 맞는 폴더에 추가하면 문서별 nav 수정 없이 메뉴, 모음, 서비스 및 태그 색인에 나타난다. +12. 태그 이름과 한국어·영어 본문으로 관련 문서를 찾을 수 있다. +13. 워크스페이스에서 Microsoft Learn MCP를 연결할 수 있고 repository skill이 검색, 원문 조회, 주장 비교와 출처 기록을 안내한다. +14. 공개 기술 문서는 Microsoft Learn 공식 근거와 마지막 확인일을 가지며 CI가 이를 검사한다. + +## 16. 위험과 대응 + +| 위험 | 대응 | +|---|---| +| 공개 과정에서 민감 정보 노출 | 자동 비밀 탐지와 사람의 익명화 검토를 모두 요구한다. | +| 대량 이동으로 링크 손상 | 마이그레이션 단위마다 엄격 빌드와 링크 검사를 실행한다. | +| 문서 분류가 모호함 | 사건 시점의 사실이면 사례, 현재 재사용할 절차이면 가이드로 분리하고 둘 다 필요하면 두 페이지로 만든다. | +| 가이드가 제품 변화에 뒤처짐 | 검증일, 검토 주기, 상태 배너와 예약 보고를 사용한다. | +| 전체 nav 수동 관리 비용 | `mkdocs-awesome-nav`의 분산 `.nav.yml`과 자동 포함을 사용한다. | +| 검색 인덱스 크기 증가 | 초기에는 로컬 검색을 사용하고 실제 성능 문제가 관측될 때만 외부 검색을 평가한다. | +| MCP 도구 이름이나 스키마 변경 | 연결 시 `tools/list`로 동적 발견하고 도구별 요청 형식을 코드에 고정하지 않는다. | +| URL만 추가하고 실제 내용을 검증하지 않음 | skill이 검색 후 원문 조회와 주장 비교를 요구하며 CI 메타데이터 검사는 의미 검증을 대체하지 않는다고 명시한다. | +| 외부 공식 검색을 정적 사이트에서 무리하게 통합 | 초기 범위에서는 공식 검색 링크만 확장점으로 설계하고, 제목 색인은 정책 확인 후 예약 빌드 방식으로 별도 구현한다. | + +## 17. 공식 설계 근거 + +- [Microsoft Learn MCP Server overview](https://learn.microsoft.com/en-us/training/support/mcp): 공개 원격 서버, Streamable HTTP 엔드포인트, 인증 요구 사항과 제한을 확인했다. +- [Get started with the Microsoft Learn MCP Server](https://learn.microsoft.com/en-us/training/support/mcp-get-started): 워크스페이스 연결, 권장 에이전트 지침과 검색·원문·코드 샘플 도구 사용 방식을 확인했다. +- [Microsoft Learn MCP Server developer reference](https://learn.microsoft.com/en-us/training/support/mcp-developer-reference): 엔드포인트 구성 형식과 도구 목록을 세션마다 조회해야 한다는 계약을 확인했다. +- [Best practices for using the Microsoft Learn MCP Server](https://learn.microsoft.com/en-us/training/support/mcp-best-practices): 도구 이름, 매개변수와 동작을 하드코딩하지 않고 `tools/list`로 동적 발견해야 한다는 지침을 확인했다. +- [Add and manage MCP servers in VS Code](https://code.visualstudio.com/docs/agent-customization/mcp-servers): `.vscode/mcp.json`의 `servers` 구조, 소스 관리 가능한 워크스페이스 설정과 Agent Host 전달 동작을 확인했다. + +## 18. 확정된 사용자 결정 + +- 사이트는 외부 공개용이다. +- 기존 Markdown 경로를 보존할 필요가 없다. +- 루트 README에서 새 GitHub Pages 사이트를 소개한다. +- 기여 지침을 별도 사이트 페이지로 제공한다. +- Markdown 작성 규칙을 `AGENTS.md`에도 실행 가능한 형태로 정리한다. +- 실제 문제 해결 이력과 지속 갱신형 일반 가이드를 별도 모음과 문서 형식으로 관리한다. +- GitHub Pages는 Material for MkDocs와 GitHub Actions 기반으로 구성한다. +- 문서와 메뉴는 폴더·메타데이터 기반으로 자동 포함하며 문서별 하드코딩을 최소화한다. +- 태그 기반 탐색과 한국어·영어 전체 텍스트 검색을 필수로 제공한다. +- 문서 검증 하네스는 Microsoft Learn 공식 문서를 반드시 참조하고 이를 위한 MCP 연결과 repository skill을 제공한다. +- Azure 서비스 폴더는 `azure-`와 공식 전체 제품명으로 통일하고, Microsoft 제품과 오픈소스는 공식 명칭을 사용한다. +- 내부 설계와 구현 계획은 `project/specs/`와 `project/plans/`에 두어 공개 Pages 소스와 분리한다. +- 로컬 검색 결과가 없을 때 Microsoft Learn, CNCF와 Kubernetes 공식 검색으로 연결하는 기능은 선택 사항이다. diff --git a/ptu_lb/README.md b/ptu_lb/README.md deleted file mode 100644 index 1303429b..00000000 --- a/ptu_lb/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# PTU LB Test Docs - -메인 문서는 [ptu-lb-test.md](ptu-lb-test.md) 입니다. diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 00000000..ffed3380 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,7 @@ +mkdocs==1.6.1 +mkdocs-material==9.7.7 +mkdocs-awesome-nav==3.3.0 +mkdocs-gen-files==0.6.1 +pymdown-extensions==11.0.2 +PyYAML==6.0.3 +pytest==9.1.1 diff --git a/aisearch-v2/.env.example b/samples/azure-ai-search/analyzer-comparison/.env.example similarity index 68% rename from aisearch-v2/.env.example rename to samples/azure-ai-search/analyzer-comparison/.env.example index cd4da848..ae9caa09 100644 --- a/aisearch-v2/.env.example +++ b/samples/azure-ai-search/analyzer-comparison/.env.example @@ -1,5 +1,5 @@ # Azure AI Search 연결 정보 (복사해서 .env 로 저장) -SEARCH_ENDPOINT=https://onnuri-search-59018.search.windows.net +SEARCH_ENDPOINT=https://search-example-koreacentral-02.search.windows.net SEARCH_ADMIN_KEY= INDEX_NAME=analyzer-compare-idx INDEX_NAME_V2=analyzer-compare-idx-v2 diff --git a/aisearch-v2/.gitignore b/samples/azure-ai-search/analyzer-comparison/.gitignore similarity index 100% rename from aisearch-v2/.gitignore rename to samples/azure-ai-search/analyzer-comparison/.gitignore diff --git a/aisearch-v2/analyzer_test.py b/samples/azure-ai-search/analyzer-comparison/analyzer_test.py similarity index 100% rename from aisearch-v2/analyzer_test.py rename to samples/azure-ai-search/analyzer-comparison/analyzer_test.py diff --git a/aisearch-v2/images/.gitkeep b/samples/azure-ai-search/analyzer-comparison/images/.gitkeep similarity index 100% rename from aisearch-v2/images/.gitkeep rename to samples/azure-ai-search/analyzer-comparison/images/.gitkeep diff --git a/aisearch-v2/images/index_analyzer_type.png b/samples/azure-ai-search/analyzer-comparison/images/index_analyzer_type.png similarity index 100% rename from aisearch-v2/images/index_analyzer_type.png rename to samples/azure-ai-search/analyzer-comparison/images/index_analyzer_type.png diff --git a/samples/azure-ai-search/analyzer-comparison/images/portal_ko_microsoft.png b/samples/azure-ai-search/analyzer-comparison/images/portal_ko_microsoft.png new file mode 100644 index 00000000..cc5767c4 Binary files /dev/null and b/samples/azure-ai-search/analyzer-comparison/images/portal_ko_microsoft.png differ diff --git a/samples/azure-ai-search/analyzer-comparison/images/portal_standard_lucene.png b/samples/azure-ai-search/analyzer-comparison/images/portal_standard_lucene.png new file mode 100644 index 00000000..23d208c9 Binary files /dev/null and b/samples/azure-ai-search/analyzer-comparison/images/portal_standard_lucene.png differ diff --git a/aisearch-v2/requirements.txt b/samples/azure-ai-search/analyzer-comparison/requirements.txt similarity index 100% rename from aisearch-v2/requirements.txt rename to samples/azure-ai-search/analyzer-comparison/requirements.txt diff --git a/aisearch-v2/sample_data.json b/samples/azure-ai-search/analyzer-comparison/sample_data.json similarity index 100% rename from aisearch-v2/sample_data.json rename to samples/azure-ai-search/analyzer-comparison/sample_data.json diff --git a/aisearch-v2/synonym_test.py b/samples/azure-ai-search/analyzer-comparison/synonym_test.py similarity index 100% rename from aisearch-v2/synonym_test.py rename to samples/azure-ai-search/analyzer-comparison/synonym_test.py diff --git a/aisearch/custom_vectorization/bench/Dockerfile b/samples/azure-ai-search/source-material/custom_vectorization/bench/Dockerfile similarity index 100% rename from aisearch/custom_vectorization/bench/Dockerfile rename to samples/azure-ai-search/source-material/custom_vectorization/bench/Dockerfile diff --git a/aisearch/custom_vectorization/bench/bench_embed_gpu.py b/samples/azure-ai-search/source-material/custom_vectorization/bench/bench_embed_gpu.py similarity index 100% rename from aisearch/custom_vectorization/bench/bench_embed_gpu.py rename to samples/azure-ai-search/source-material/custom_vectorization/bench/bench_embed_gpu.py diff --git a/aisearch/custom_vectorization/bench/bench_qps_sweep.py b/samples/azure-ai-search/source-material/custom_vectorization/bench/bench_qps_sweep.py similarity index 100% rename from aisearch/custom_vectorization/bench/bench_qps_sweep.py rename to samples/azure-ai-search/source-material/custom_vectorization/bench/bench_qps_sweep.py diff --git a/aisearch/custom_vectorization/bench/bench_runner.py b/samples/azure-ai-search/source-material/custom_vectorization/bench/bench_runner.py similarity index 99% rename from aisearch/custom_vectorization/bench/bench_runner.py rename to samples/azure-ai-search/source-material/custom_vectorization/bench/bench_runner.py index de56eb78..cbcb1058 100644 --- a/aisearch/custom_vectorization/bench/bench_runner.py +++ b/samples/azure-ai-search/source-material/custom_vectorization/bench/bench_runner.py @@ -22,12 +22,12 @@ from azure.storage.blob import BlobServiceClient # ── Config (from env) ───────────────────────────────────────────────────────── -SEARCH_URL = os.environ.get("SEARCH_URL", "https://ais-aiplay-krc-01.search.windows.net") +SEARCH_URL = os.environ["SEARCH_URL"] SEARCH_API_KEY = os.environ.get("SEARCH_API_KEY", "") VLLM_EMBED_URL = os.environ.get("VLLM_EMBED_URL", "http://vllm-embedding-svc:8081") -EMBED_SKILL_URI = os.environ.get("EMBED_SKILL_URI", "https://embed.20.249.162.81.nip.io/api/embed") -STORAGE_ACCOUNT = os.environ.get("STORAGE_ACCOUNT", "saidxtest44159") -STORAGE_RG = os.environ.get("STORAGE_RG", "rg-aiplay-krc-01") +EMBED_SKILL_URI = os.environ.get("EMBED_SKILL_URI", "https://embed.example.com/api/embed") +STORAGE_ACCOUNT = os.environ["STORAGE_ACCOUNT"] +STORAGE_RG = os.environ["STORAGE_RG"] BLOB_CONTAINER = os.environ.get("BLOB_CONTAINER", "bench-docs") EMBED_MODEL = os.environ.get("EMBED_MODEL", "Qwen/Qwen3-Embedding-4B") diff --git a/aisearch/custom_vectorization/bench/k8s/configmap.yaml b/samples/azure-ai-search/source-material/custom_vectorization/bench/k8s/configmap.yaml similarity index 57% rename from aisearch/custom_vectorization/bench/k8s/configmap.yaml rename to samples/azure-ai-search/source-material/custom_vectorization/bench/k8s/configmap.yaml index fef374a8..e5d04f56 100644 --- a/aisearch/custom_vectorization/bench/k8s/configmap.yaml +++ b/samples/azure-ai-search/source-material/custom_vectorization/bench/k8s/configmap.yaml @@ -4,15 +4,15 @@ kind: ConfigMap metadata: name: bench-env data: - SEARCH_URL: "https://ais-aiplay-krc-01.search.windows.net" + SEARCH_URL: "https://.search.windows.net" VLLM_EMBED_URL: "http://vllm-embedding-svc:8081" - EMBED_SKILL_URI: "https://embed.20.249.162.81.nip.io/api/embed" - STORAGE_ACCOUNT: "saidxtest44159" - STORAGE_RG: "rg-aiplay-krc-01" + EMBED_SKILL_URI: "https://embed.example.com/api/embed" + STORAGE_ACCOUNT: "" + STORAGE_RG: "" BLOB_CONTAINER: "bench-docs" EMBED_MODEL: "Qwen/Qwen3-Embedding-4B" VECTOR_DIM: "2560" - STORAGE_RESOURCE_ID: "/subscriptions/b219ebf4-b544-46b1-b7ce-9c9b25ea826c/resourceGroups/rg-aiplay-krc-01/providers/Microsoft.Storage/storageAccounts/saidxtest44159" + STORAGE_RESOURCE_ID: "/subscriptions//resourceGroups//providers/Microsoft.Storage/storageAccounts/" --- # Secret — API key (create manually: kubectl create secret generic bench-secrets --from-literal=SEARCH_API_KEY=xxx) # apiVersion: v1 diff --git a/aisearch/custom_vectorization/bench/k8s/job-bench-concurrency.yaml b/samples/azure-ai-search/source-material/custom_vectorization/bench/k8s/job-bench-concurrency.yaml similarity index 92% rename from aisearch/custom_vectorization/bench/k8s/job-bench-concurrency.yaml rename to samples/azure-ai-search/source-material/custom_vectorization/bench/k8s/job-bench-concurrency.yaml index 7bf98a15..9a9bcc98 100644 --- a/aisearch/custom_vectorization/bench/k8s/job-bench-concurrency.yaml +++ b/samples/azure-ai-search/source-material/custom_vectorization/bench/k8s/job-bench-concurrency.yaml @@ -10,7 +10,7 @@ spec: restartPolicy: Never containers: - name: bench - image: acrcustomvec01.azurecr.io/bench-runner:latest + image: acrexamplekrc01.azurecr.io/bench-runner:latest args: ["bench-concurrency"] envFrom: - configMapRef: diff --git a/aisearch/custom_vectorization/bench/k8s/job-bench-indexer.yaml b/samples/azure-ai-search/source-material/custom_vectorization/bench/k8s/job-bench-indexer.yaml similarity index 93% rename from aisearch/custom_vectorization/bench/k8s/job-bench-indexer.yaml rename to samples/azure-ai-search/source-material/custom_vectorization/bench/k8s/job-bench-indexer.yaml index be34276f..23f1c81d 100644 --- a/aisearch/custom_vectorization/bench/k8s/job-bench-indexer.yaml +++ b/samples/azure-ai-search/source-material/custom_vectorization/bench/k8s/job-bench-indexer.yaml @@ -10,7 +10,7 @@ spec: restartPolicy: Never containers: - name: bench - image: acrcustomvec01.azurecr.io/bench-runner:latest + image: acrexamplekrc01.azurecr.io/bench-runner:latest args: ["indexer"] envFrom: - configMapRef: diff --git a/aisearch/custom_vectorization/bench/k8s/job-bench-push.yaml b/samples/azure-ai-search/source-material/custom_vectorization/bench/k8s/job-bench-push.yaml similarity index 93% rename from aisearch/custom_vectorization/bench/k8s/job-bench-push.yaml rename to samples/azure-ai-search/source-material/custom_vectorization/bench/k8s/job-bench-push.yaml index b3f70de1..876df79c 100644 --- a/aisearch/custom_vectorization/bench/k8s/job-bench-push.yaml +++ b/samples/azure-ai-search/source-material/custom_vectorization/bench/k8s/job-bench-push.yaml @@ -10,7 +10,7 @@ spec: restartPolicy: Never containers: - name: bench - image: acrcustomvec01.azurecr.io/bench-runner:latest + image: acrexamplekrc01.azurecr.io/bench-runner:latest args: ["push"] envFrom: - configMapRef: diff --git a/aisearch/custom_vectorization/bench/requirements.txt b/samples/azure-ai-search/source-material/custom_vectorization/bench/requirements.txt similarity index 100% rename from aisearch/custom_vectorization/bench/requirements.txt rename to samples/azure-ai-search/source-material/custom_vectorization/bench/requirements.txt diff --git a/aisearch/custom_vectorization/embedding-api/Dockerfile b/samples/azure-ai-search/source-material/custom_vectorization/embedding-api/Dockerfile similarity index 100% rename from aisearch/custom_vectorization/embedding-api/Dockerfile rename to samples/azure-ai-search/source-material/custom_vectorization/embedding-api/Dockerfile diff --git a/aisearch/custom_vectorization/embedding-api/app.py b/samples/azure-ai-search/source-material/custom_vectorization/embedding-api/app.py similarity index 100% rename from aisearch/custom_vectorization/embedding-api/app.py rename to samples/azure-ai-search/source-material/custom_vectorization/embedding-api/app.py diff --git a/aisearch/custom_vectorization/embedding-api/requirements.txt b/samples/azure-ai-search/source-material/custom_vectorization/embedding-api/requirements.txt similarity index 100% rename from aisearch/custom_vectorization/embedding-api/requirements.txt rename to samples/azure-ai-search/source-material/custom_vectorization/embedding-api/requirements.txt diff --git a/aisearch/custom_vectorization/images/architecture.excalidraw b/samples/azure-ai-search/source-material/custom_vectorization/images/architecture.excalidraw similarity index 99% rename from aisearch/custom_vectorization/images/architecture.excalidraw rename to samples/azure-ai-search/source-material/custom_vectorization/images/architecture.excalidraw index 1755e4cd..2e455e31 100644 --- a/aisearch/custom_vectorization/images/architecture.excalidraw +++ b/samples/azure-ai-search/source-material/custom_vectorization/images/architecture.excalidraw @@ -680,13 +680,13 @@ "updated": 1716300000000, "link": null, "locked": false, - "text": "Azure Container Registry\nacrcustomvec01", + "text": "Azure Container Registry\nacrexamplekrc01", "fontSize": 16, "fontFamily": 5, "textAlign": "center", "verticalAlign": "middle", "containerId": "acr_rect", - "originalText": "Azure Container Registry\nacrcustomvec01", + "originalText": "Azure Container Registry\nacrexamplekrc01", "autoResize": true, "lineHeight": 1.25 }, diff --git a/samples/azure-ai-search/source-material/custom_vectorization/images/architecture.png b/samples/azure-ai-search/source-material/custom_vectorization/images/architecture.png new file mode 100644 index 00000000..933ef85c Binary files /dev/null and b/samples/azure-ai-search/source-material/custom_vectorization/images/architecture.png differ diff --git a/aisearch/custom_vectorization/images/architecture_lite.png b/samples/azure-ai-search/source-material/custom_vectorization/images/architecture_lite.png similarity index 100% rename from aisearch/custom_vectorization/images/architecture_lite.png rename to samples/azure-ai-search/source-material/custom_vectorization/images/architecture_lite.png diff --git a/aisearch/custom_vectorization/images/gpu_vllm_rag_architecture.excalidraw b/samples/azure-ai-search/source-material/custom_vectorization/images/gpu_vllm_rag_architecture.excalidraw similarity index 100% rename from aisearch/custom_vectorization/images/gpu_vllm_rag_architecture.excalidraw rename to samples/azure-ai-search/source-material/custom_vectorization/images/gpu_vllm_rag_architecture.excalidraw diff --git a/samples/azure-ai-search/source-material/custom_vectorization/images/gpu_vllm_rag_architecture.png b/samples/azure-ai-search/source-material/custom_vectorization/images/gpu_vllm_rag_architecture.png new file mode 100644 index 00000000..e5465d5d Binary files /dev/null and b/samples/azure-ai-search/source-material/custom_vectorization/images/gpu_vllm_rag_architecture.png differ diff --git a/samples/azure-ai-search/source-material/custom_vectorization/images/indexer_vs_pushapi.png b/samples/azure-ai-search/source-material/custom_vectorization/images/indexer_vs_pushapi.png new file mode 100644 index 00000000..acfdae0b Binary files /dev/null and b/samples/azure-ai-search/source-material/custom_vectorization/images/indexer_vs_pushapi.png differ diff --git a/aisearch/custom_vectorization/indexer_vs_pushapi.excalidraw b/samples/azure-ai-search/source-material/custom_vectorization/indexer_vs_pushapi.excalidraw similarity index 100% rename from aisearch/custom_vectorization/indexer_vs_pushapi.excalidraw rename to samples/azure-ai-search/source-material/custom_vectorization/indexer_vs_pushapi.excalidraw diff --git a/aisearch/custom_vectorization/k8s/cluster-issuer.yaml b/samples/azure-ai-search/source-material/custom_vectorization/k8s/cluster-issuer.yaml similarity index 100% rename from aisearch/custom_vectorization/k8s/cluster-issuer.yaml rename to samples/azure-ai-search/source-material/custom_vectorization/k8s/cluster-issuer.yaml diff --git a/aisearch/custom_vectorization/k8s/deployment.yaml b/samples/azure-ai-search/source-material/custom_vectorization/k8s/deployment.yaml similarity index 93% rename from aisearch/custom_vectorization/k8s/deployment.yaml rename to samples/azure-ai-search/source-material/custom_vectorization/k8s/deployment.yaml index 4b35a987..08f43091 100644 --- a/aisearch/custom_vectorization/k8s/deployment.yaml +++ b/samples/azure-ai-search/source-material/custom_vectorization/k8s/deployment.yaml @@ -16,7 +16,7 @@ spec: spec: containers: - name: embedding-api - image: acrcustomvec01.azurecr.io/bge-m3-embedding:latest + image: acrexamplekrc01.azurecr.io/bge-m3-embedding:latest ports: - containerPort: 8000 resources: diff --git a/aisearch/custom_vectorization/k8s/ingress.yaml b/samples/azure-ai-search/source-material/custom_vectorization/k8s/ingress.yaml similarity index 100% rename from aisearch/custom_vectorization/k8s/ingress.yaml rename to samples/azure-ai-search/source-material/custom_vectorization/k8s/ingress.yaml diff --git a/aisearch/custom_vectorization/reranker-adapter/Dockerfile b/samples/azure-ai-search/source-material/custom_vectorization/reranker-adapter/Dockerfile similarity index 100% rename from aisearch/custom_vectorization/reranker-adapter/Dockerfile rename to samples/azure-ai-search/source-material/custom_vectorization/reranker-adapter/Dockerfile diff --git a/aisearch/custom_vectorization/reranker-adapter/app.py b/samples/azure-ai-search/source-material/custom_vectorization/reranker-adapter/app.py similarity index 100% rename from aisearch/custom_vectorization/reranker-adapter/app.py rename to samples/azure-ai-search/source-material/custom_vectorization/reranker-adapter/app.py diff --git a/aisearch/custom_vectorization/reranker-adapter/requirements.txt b/samples/azure-ai-search/source-material/custom_vectorization/reranker-adapter/requirements.txt similarity index 100% rename from aisearch/custom_vectorization/reranker-adapter/requirements.txt rename to samples/azure-ai-search/source-material/custom_vectorization/reranker-adapter/requirements.txt diff --git a/aisearch/custom_vectorization/tei-adapter/Dockerfile b/samples/azure-ai-search/source-material/custom_vectorization/tei-adapter/Dockerfile similarity index 100% rename from aisearch/custom_vectorization/tei-adapter/Dockerfile rename to samples/azure-ai-search/source-material/custom_vectorization/tei-adapter/Dockerfile diff --git a/aisearch/custom_vectorization/tei-adapter/app.py b/samples/azure-ai-search/source-material/custom_vectorization/tei-adapter/app.py similarity index 100% rename from aisearch/custom_vectorization/tei-adapter/app.py rename to samples/azure-ai-search/source-material/custom_vectorization/tei-adapter/app.py diff --git a/aisearch/custom_vectorization/tei-adapter/k8s/deployment.yaml b/samples/azure-ai-search/source-material/custom_vectorization/tei-adapter/k8s/deployment.yaml similarity index 97% rename from aisearch/custom_vectorization/tei-adapter/k8s/deployment.yaml rename to samples/azure-ai-search/source-material/custom_vectorization/tei-adapter/k8s/deployment.yaml index 24289138..8e279d99 100644 --- a/aisearch/custom_vectorization/tei-adapter/k8s/deployment.yaml +++ b/samples/azure-ai-search/source-material/custom_vectorization/tei-adapter/k8s/deployment.yaml @@ -52,7 +52,7 @@ spec: # Adapter — AI Search contract translation (sidecar) - name: adapter - image: acrcustomvec01.azurecr.io/tei-adapter:latest + image: acrexamplekrc01.azurecr.io/tei-adapter:latest env: - name: TEI_URL value: "http://localhost:8080" diff --git a/aisearch/custom_vectorization/tei-adapter/requirements.txt b/samples/azure-ai-search/source-material/custom_vectorization/tei-adapter/requirements.txt similarity index 100% rename from aisearch/custom_vectorization/tei-adapter/requirements.txt rename to samples/azure-ai-search/source-material/custom_vectorization/tei-adapter/requirements.txt diff --git a/aisearch/custom_vectorization/vllm-adapter/Dockerfile b/samples/azure-ai-search/source-material/custom_vectorization/vllm-adapter/Dockerfile similarity index 100% rename from aisearch/custom_vectorization/vllm-adapter/Dockerfile rename to samples/azure-ai-search/source-material/custom_vectorization/vllm-adapter/Dockerfile diff --git a/aisearch/custom_vectorization/vllm-adapter/app.py b/samples/azure-ai-search/source-material/custom_vectorization/vllm-adapter/app.py similarity index 100% rename from aisearch/custom_vectorization/vllm-adapter/app.py rename to samples/azure-ai-search/source-material/custom_vectorization/vllm-adapter/app.py diff --git a/aisearch/custom_vectorization/vllm-adapter/requirements.txt b/samples/azure-ai-search/source-material/custom_vectorization/vllm-adapter/requirements.txt similarity index 100% rename from aisearch/custom_vectorization/vllm-adapter/requirements.txt rename to samples/azure-ai-search/source-material/custom_vectorization/vllm-adapter/requirements.txt diff --git a/aisearch/repro.py b/samples/azure-ai-search/source-material/repro.py similarity index 100% rename from aisearch/repro.py rename to samples/azure-ai-search/source-material/repro.py diff --git a/oryx-test/app.py b/samples/azure-app-service/oryx-test/app.py similarity index 100% rename from oryx-test/app.py rename to samples/azure-app-service/oryx-test/app.py diff --git a/oryx-test/requirements.txt b/samples/azure-app-service/oryx-test/requirements.txt similarity index 100% rename from oryx-test/requirements.txt rename to samples/azure-app-service/oryx-test/requirements.txt diff --git a/mysql/nodejs/simulation/app/Dockerfile b/samples/azure-database-for-mysql/source-material/nodejs/simulation/app/Dockerfile similarity index 100% rename from mysql/nodejs/simulation/app/Dockerfile rename to samples/azure-database-for-mysql/source-material/nodejs/simulation/app/Dockerfile diff --git a/mysql/nodejs/simulation/app/dbHandler.js b/samples/azure-database-for-mysql/source-material/nodejs/simulation/app/dbHandler.js similarity index 100% rename from mysql/nodejs/simulation/app/dbHandler.js rename to samples/azure-database-for-mysql/source-material/nodejs/simulation/app/dbHandler.js diff --git a/mysql/nodejs/simulation/app/package.json b/samples/azure-database-for-mysql/source-material/nodejs/simulation/app/package.json similarity index 100% rename from mysql/nodejs/simulation/app/package.json rename to samples/azure-database-for-mysql/source-material/nodejs/simulation/app/package.json diff --git a/mysql/nodejs/simulation/app/server.js b/samples/azure-database-for-mysql/source-material/nodejs/simulation/app/server.js similarity index 100% rename from mysql/nodejs/simulation/app/server.js rename to samples/azure-database-for-mysql/source-material/nodejs/simulation/app/server.js diff --git a/mysql/nodejs/simulation/infra/main.bicep b/samples/azure-database-for-mysql/source-material/nodejs/simulation/infra/main.bicep similarity index 100% rename from mysql/nodejs/simulation/infra/main.bicep rename to samples/azure-database-for-mysql/source-material/nodejs/simulation/infra/main.bicep diff --git a/mysql/nodejs/simulation/infra/main.bicepparam b/samples/azure-database-for-mysql/source-material/nodejs/simulation/infra/main.bicepparam similarity index 100% rename from mysql/nodejs/simulation/infra/main.bicepparam rename to samples/azure-database-for-mysql/source-material/nodejs/simulation/infra/main.bicepparam diff --git a/mysql/nodejs/simulation/k8s/deployment.yaml b/samples/azure-database-for-mysql/source-material/nodejs/simulation/k8s/deployment.yaml similarity index 100% rename from mysql/nodejs/simulation/k8s/deployment.yaml rename to samples/azure-database-for-mysql/source-material/nodejs/simulation/k8s/deployment.yaml diff --git a/mysql/nodejs/simulation/mysql_change.png b/samples/azure-database-for-mysql/source-material/nodejs/simulation/mysql_change.png similarity index 100% rename from mysql/nodejs/simulation/mysql_change.png rename to samples/azure-database-for-mysql/source-material/nodejs/simulation/mysql_change.png diff --git a/mysql/nodejs/simulation/scripts/01-deploy-infra.ps1 b/samples/azure-database-for-mysql/source-material/nodejs/simulation/scripts/01-deploy-infra.ps1 similarity index 100% rename from mysql/nodejs/simulation/scripts/01-deploy-infra.ps1 rename to samples/azure-database-for-mysql/source-material/nodejs/simulation/scripts/01-deploy-infra.ps1 diff --git a/mysql/nodejs/simulation/scripts/02-setup-db.ps1 b/samples/azure-database-for-mysql/source-material/nodejs/simulation/scripts/02-setup-db.ps1 similarity index 100% rename from mysql/nodejs/simulation/scripts/02-setup-db.ps1 rename to samples/azure-database-for-mysql/source-material/nodejs/simulation/scripts/02-setup-db.ps1 diff --git a/mysql/nodejs/simulation/scripts/02b-setup-replication.ps1 b/samples/azure-database-for-mysql/source-material/nodejs/simulation/scripts/02b-setup-replication.ps1 similarity index 100% rename from mysql/nodejs/simulation/scripts/02b-setup-replication.ps1 rename to samples/azure-database-for-mysql/source-material/nodejs/simulation/scripts/02b-setup-replication.ps1 diff --git a/mysql/nodejs/simulation/scripts/03-build-push.ps1 b/samples/azure-database-for-mysql/source-material/nodejs/simulation/scripts/03-build-push.ps1 similarity index 100% rename from mysql/nodejs/simulation/scripts/03-build-push.ps1 rename to samples/azure-database-for-mysql/source-material/nodejs/simulation/scripts/03-build-push.ps1 diff --git a/mysql/nodejs/simulation/scripts/04-watch-logs.ps1 b/samples/azure-database-for-mysql/source-material/nodejs/simulation/scripts/04-watch-logs.ps1 similarity index 100% rename from mysql/nodejs/simulation/scripts/04-watch-logs.ps1 rename to samples/azure-database-for-mysql/source-material/nodejs/simulation/scripts/04-watch-logs.ps1 diff --git a/mysql/nodejs/simulation/scripts/05-cutover.ps1 b/samples/azure-database-for-mysql/source-material/nodejs/simulation/scripts/05-cutover.ps1 similarity index 100% rename from mysql/nodejs/simulation/scripts/05-cutover.ps1 rename to samples/azure-database-for-mysql/source-material/nodejs/simulation/scripts/05-cutover.ps1 diff --git a/mysql/nodejs/simulation/scripts/06-rollback-or-cleanup.ps1 b/samples/azure-database-for-mysql/source-material/nodejs/simulation/scripts/06-rollback-or-cleanup.ps1 similarity index 100% rename from mysql/nodejs/simulation/scripts/06-rollback-or-cleanup.ps1 rename to samples/azure-database-for-mysql/source-material/nodejs/simulation/scripts/06-rollback-or-cleanup.ps1 diff --git a/mysql/nodejs/simulation/scripts/run-3-cutovers.ps1 b/samples/azure-database-for-mysql/source-material/nodejs/simulation/scripts/run-3-cutovers.ps1 similarity index 100% rename from mysql/nodejs/simulation/scripts/run-3-cutovers.ps1 rename to samples/azure-database-for-mysql/source-material/nodejs/simulation/scripts/run-3-cutovers.ps1 diff --git a/mysql/nodejs/simulation/simulation-architecture.excalidraw b/samples/azure-database-for-mysql/source-material/nodejs/simulation/simulation-architecture.excalidraw similarity index 100% rename from mysql/nodejs/simulation/simulation-architecture.excalidraw rename to samples/azure-database-for-mysql/source-material/nodejs/simulation/simulation-architecture.excalidraw diff --git a/mysql/nodejs/simulation/simulation-cutover.excalidraw b/samples/azure-database-for-mysql/source-material/nodejs/simulation/simulation-cutover.excalidraw similarity index 100% rename from mysql/nodejs/simulation/simulation-cutover.excalidraw rename to samples/azure-database-for-mysql/source-material/nodejs/simulation/simulation-cutover.excalidraw diff --git a/aks/local_dev_loop/k8s/echo-service.yaml b/samples/azure-kubernetes-service/source-material/local_dev_loop/k8s/echo-service.yaml similarity index 100% rename from aks/local_dev_loop/k8s/echo-service.yaml rename to samples/azure-kubernetes-service/source-material/local_dev_loop/k8s/echo-service.yaml diff --git a/aks/local_dev_loop/local/local-server.py b/samples/azure-kubernetes-service/source-material/local_dev_loop/local/local-server.py similarity index 100% rename from aks/local_dev_loop/local/local-server.py rename to samples/azure-kubernetes-service/source-material/local_dev_loop/local/local-server.py diff --git a/aks/local_dev_loop/local/mirrord.json b/samples/azure-kubernetes-service/source-material/local_dev_loop/local/mirrord.json similarity index 100% rename from aks/local_dev_loop/local/mirrord.json rename to samples/azure-kubernetes-service/source-material/local_dev_loop/local/mirrord.json diff --git a/aks/memray-leak-profiling/Dockerfile b/samples/azure-kubernetes-service/source-material/memray-leak-profiling/Dockerfile similarity index 100% rename from aks/memray-leak-profiling/Dockerfile rename to samples/azure-kubernetes-service/source-material/memray-leak-profiling/Dockerfile diff --git a/aks/memray-leak-profiling/app.py b/samples/azure-kubernetes-service/source-material/memray-leak-profiling/app.py similarity index 100% rename from aks/memray-leak-profiling/app.py rename to samples/azure-kubernetes-service/source-material/memray-leak-profiling/app.py diff --git a/aks/memray-leak-profiling/debug/Dockerfile b/samples/azure-kubernetes-service/source-material/memray-leak-profiling/debug/Dockerfile similarity index 100% rename from aks/memray-leak-profiling/debug/Dockerfile rename to samples/azure-kubernetes-service/source-material/memray-leak-profiling/debug/Dockerfile diff --git a/aks/memray-leak-profiling/k8s/leaky-agent.yaml b/samples/azure-kubernetes-service/source-material/memray-leak-profiling/k8s/leaky-agent.yaml similarity index 95% rename from aks/memray-leak-profiling/k8s/leaky-agent.yaml rename to samples/azure-kubernetes-service/source-material/memray-leak-profiling/k8s/leaky-agent.yaml index b4a4028d..1141a00f 100644 --- a/aks/memray-leak-profiling/k8s/leaky-agent.yaml +++ b/samples/azure-kubernetes-service/source-material/memray-leak-profiling/k8s/leaky-agent.yaml @@ -22,7 +22,7 @@ spec: spec: containers: - name: leaky-agent - image: acrmemrayb219eb.azurecr.io/leaky-agent:v2 + image: acrexamplememray01.azurecr.io/leaky-agent:v2 ports: - containerPort: 8000 resources: diff --git a/aks/memray-leak-profiling/k8s/loadgen.yaml b/samples/azure-kubernetes-service/source-material/memray-leak-profiling/k8s/loadgen.yaml similarity index 91% rename from aks/memray-leak-profiling/k8s/loadgen.yaml rename to samples/azure-kubernetes-service/source-material/memray-leak-profiling/k8s/loadgen.yaml index d8775484..bfbfe2a1 100644 --- a/aks/memray-leak-profiling/k8s/loadgen.yaml +++ b/samples/azure-kubernetes-service/source-material/memray-leak-profiling/k8s/loadgen.yaml @@ -17,7 +17,7 @@ spec: spec: containers: - name: loadgen - image: acrmemrayb219eb.azurecr.io/leaky-agent:v2 + image: acrexamplememray01.azurecr.io/leaky-agent:v2 command: ["python", "loadgen.py"] env: - name: TARGET_URL diff --git a/aks/memray-leak-profiling/loadgen.py b/samples/azure-kubernetes-service/source-material/memray-leak-profiling/loadgen.py similarity index 100% rename from aks/memray-leak-profiling/loadgen.py rename to samples/azure-kubernetes-service/source-material/memray-leak-profiling/loadgen.py diff --git a/aks/memray-leak-profiling/reports/leak-flamegraph.html b/samples/azure-kubernetes-service/source-material/memray-leak-profiling/reports/leak-flamegraph.html similarity index 100% rename from aks/memray-leak-profiling/reports/leak-flamegraph.html rename to samples/azure-kubernetes-service/source-material/memray-leak-profiling/reports/leak-flamegraph.html diff --git a/aks/memray-leak-profiling/reports/leak-leaks.html b/samples/azure-kubernetes-service/source-material/memray-leak-profiling/reports/leak-leaks.html similarity index 100% rename from aks/memray-leak-profiling/reports/leak-leaks.html rename to samples/azure-kubernetes-service/source-material/memray-leak-profiling/reports/leak-leaks.html diff --git a/samples/azure-kubernetes-service/source-material/memray-leak-profiling/reports/leak-leaks.png b/samples/azure-kubernetes-service/source-material/memray-leak-profiling/reports/leak-leaks.png new file mode 100644 index 00000000..01db9d17 Binary files /dev/null and b/samples/azure-kubernetes-service/source-material/memray-leak-profiling/reports/leak-leaks.png differ diff --git a/aks/memray-leak-profiling/requirements.txt b/samples/azure-kubernetes-service/source-material/memray-leak-profiling/requirements.txt similarity index 100% rename from aks/memray-leak-profiling/requirements.txt rename to samples/azure-kubernetes-service/source-material/memray-leak-profiling/requirements.txt diff --git a/samples/azure-kubernetes-service/source-material/pod_db_query_latency/01_query_latency.png b/samples/azure-kubernetes-service/source-material/pod_db_query_latency/01_query_latency.png new file mode 100644 index 00000000..79dbc49e Binary files /dev/null and b/samples/azure-kubernetes-service/source-material/pod_db_query_latency/01_query_latency.png differ diff --git a/samples/azure-kubernetes-service/source-material/pod_db_query_latency/02_pod_runtime.png b/samples/azure-kubernetes-service/source-material/pod_db_query_latency/02_pod_runtime.png new file mode 100644 index 00000000..2ff280d3 Binary files /dev/null and b/samples/azure-kubernetes-service/source-material/pod_db_query_latency/02_pod_runtime.png differ diff --git a/aks/pod_db_query_latency/pod_db_query_latency.excalidraw b/samples/azure-kubernetes-service/source-material/pod_db_query_latency/pod_db_query_latency.excalidraw similarity index 100% rename from aks/pod_db_query_latency/pod_db_query_latency.excalidraw rename to samples/azure-kubernetes-service/source-material/pod_db_query_latency/pod_db_query_latency.excalidraw diff --git a/samples/azure-kubernetes-service/source-material/pod_db_query_latency/pod_db_query_latency.png b/samples/azure-kubernetes-service/source-material/pod_db_query_latency/pod_db_query_latency.png new file mode 100644 index 00000000..4b54a691 Binary files /dev/null and b/samples/azure-kubernetes-service/source-material/pod_db_query_latency/pod_db_query_latency.png differ diff --git a/samples/azure-kubernetes-service/source-material/pod_db_query_latency/result_01.png b/samples/azure-kubernetes-service/source-material/pod_db_query_latency/result_01.png new file mode 100644 index 00000000..a0bb6906 Binary files /dev/null and b/samples/azure-kubernetes-service/source-material/pod_db_query_latency/result_01.png differ diff --git a/samples/azure-kubernetes-service/source-material/pod_db_query_latency/result_02.png b/samples/azure-kubernetes-service/source-material/pod_db_query_latency/result_02.png new file mode 100644 index 00000000..aef1e668 Binary files /dev/null and b/samples/azure-kubernetes-service/source-material/pod_db_query_latency/result_02.png differ diff --git a/samples/azure-kubernetes-service/source-material/pod_db_query_latency/result_pod_runtime.png b/samples/azure-kubernetes-service/source-material/pod_db_query_latency/result_pod_runtime.png new file mode 100644 index 00000000..c5b73fd6 Binary files /dev/null and b/samples/azure-kubernetes-service/source-material/pod_db_query_latency/result_pod_runtime.png differ diff --git a/aks/pyroscope-chat-profiling/Dockerfile b/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/Dockerfile similarity index 100% rename from aks/pyroscope-chat-profiling/Dockerfile rename to samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/Dockerfile diff --git a/aks/pyroscope-chat-profiling/app.py b/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/app.py similarity index 100% rename from aks/pyroscope-chat-profiling/app.py rename to samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/app.py diff --git a/aks/pyroscope-chat-profiling/deploy.sh b/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/deploy.sh old mode 100755 new mode 100644 similarity index 100% rename from aks/pyroscope-chat-profiling/deploy.sh rename to samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/deploy.sh diff --git a/aks/pyroscope-chat-profiling/deployment.yaml b/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/deployment.yaml similarity index 100% rename from aks/pyroscope-chat-profiling/deployment.yaml rename to samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/deployment.yaml diff --git a/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/images/anf-create-bucket.png b/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/images/anf-create-bucket.png new file mode 100644 index 00000000..3e8b70b3 Binary files /dev/null and b/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/images/anf-create-bucket.png differ diff --git a/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/images/anf-create-certificate.png b/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/images/anf-create-certificate.png new file mode 100644 index 00000000..c41c05c1 Binary files /dev/null and b/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/images/anf-create-certificate.png differ diff --git a/aks/pyroscope-chat-profiling/images/architecture.excalidraw b/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/images/architecture.excalidraw similarity index 100% rename from aks/pyroscope-chat-profiling/images/architecture.excalidraw rename to samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/images/architecture.excalidraw diff --git a/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/images/architecture.png b/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/images/architecture.png new file mode 100644 index 00000000..80be1053 Binary files /dev/null and b/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/images/architecture.png differ diff --git a/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/images/pyroscope_ui.png b/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/images/pyroscope_ui.png new file mode 100644 index 00000000..942a3565 Binary files /dev/null and b/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/images/pyroscope_ui.png differ diff --git a/aks/pyroscope-chat-profiling/requirements.txt b/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/requirements.txt similarity index 100% rename from aks/pyroscope-chat-profiling/requirements.txt rename to samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/requirements.txt diff --git a/aks/pyroscope-chat-profiling/test.py b/samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/test.py similarity index 100% rename from aks/pyroscope-chat-profiling/test.py rename to samples/azure-kubernetes-service/source-material/pyroscope-chat-profiling/test.py diff --git a/samples/azure-load-testing/source-material/images/01-test-list.png b/samples/azure-load-testing/source-material/images/01-test-list.png new file mode 100644 index 00000000..6b7450c6 Binary files /dev/null and b/samples/azure-load-testing/source-material/images/01-test-list.png differ diff --git a/samples/azure-load-testing/source-material/images/02-run-summary.png b/samples/azure-load-testing/source-material/images/02-run-summary.png new file mode 100644 index 00000000..0b982633 Binary files /dev/null and b/samples/azure-load-testing/source-material/images/02-run-summary.png differ diff --git a/samples/azure-load-testing/source-material/images/03-request-statistics.png b/samples/azure-load-testing/source-material/images/03-request-statistics.png new file mode 100644 index 00000000..ca9468cc Binary files /dev/null and b/samples/azure-load-testing/source-material/images/03-request-statistics.png differ diff --git a/samples/azure-load-testing/source-material/images/04-client-side-metrics.png b/samples/azure-load-testing/source-material/images/04-client-side-metrics.png new file mode 100644 index 00000000..68ceb1d7 Binary files /dev/null and b/samples/azure-load-testing/source-material/images/04-client-side-metrics.png differ diff --git a/samples/azure-load-testing/source-material/images/05-server-side-metrics.png b/samples/azure-load-testing/source-material/images/05-server-side-metrics.png new file mode 100644 index 00000000..062b03c4 Binary files /dev/null and b/samples/azure-load-testing/source-material/images/05-server-side-metrics.png differ diff --git a/samples/azure-load-testing/source-material/images/06-engine-health.png b/samples/azure-load-testing/source-material/images/06-engine-health.png new file mode 100644 index 00000000..5fbfa35c Binary files /dev/null and b/samples/azure-load-testing/source-material/images/06-engine-health.png differ diff --git a/samples/azure-load-testing/source-material/images/07-compare-select.png b/samples/azure-load-testing/source-material/images/07-compare-select.png new file mode 100644 index 00000000..1173952a Binary files /dev/null and b/samples/azure-load-testing/source-material/images/07-compare-select.png differ diff --git a/samples/azure-load-testing/source-material/images/08-compare-result.png b/samples/azure-load-testing/source-material/images/08-compare-result.png new file mode 100644 index 00000000..a7bb92da Binary files /dev/null and b/samples/azure-load-testing/source-material/images/08-compare-result.png differ diff --git a/redis/nodejs/cluster_example/README.md b/samples/azure-managed-redis/source-material/nodejs/cluster_example/README.md similarity index 100% rename from redis/nodejs/cluster_example/README.md rename to samples/azure-managed-redis/source-material/nodejs/cluster_example/README.md diff --git a/redis/nodejs/cluster_example/healthCheck.js b/samples/azure-managed-redis/source-material/nodejs/cluster_example/healthCheck.js similarity index 100% rename from redis/nodejs/cluster_example/healthCheck.js rename to samples/azure-managed-redis/source-material/nodejs/cluster_example/healthCheck.js diff --git a/redis/nodejs/cluster_example/package-lock.json b/samples/azure-managed-redis/source-material/nodejs/cluster_example/package-lock.json similarity index 100% rename from redis/nodejs/cluster_example/package-lock.json rename to samples/azure-managed-redis/source-material/nodejs/cluster_example/package-lock.json diff --git a/redis/nodejs/cluster_example/package.json b/samples/azure-managed-redis/source-material/nodejs/cluster_example/package.json similarity index 100% rename from redis/nodejs/cluster_example/package.json rename to samples/azure-managed-redis/source-material/nodejs/cluster_example/package.json diff --git a/redis/nodejs/cluster_example/redisClient.js b/samples/azure-managed-redis/source-material/nodejs/cluster_example/redisClient.js similarity index 100% rename from redis/nodejs/cluster_example/redisClient.js rename to samples/azure-managed-redis/source-material/nodejs/cluster_example/redisClient.js diff --git a/redis/nodejs/cluster_example/routes.js b/samples/azure-managed-redis/source-material/nodejs/cluster_example/routes.js similarity index 100% rename from redis/nodejs/cluster_example/routes.js rename to samples/azure-managed-redis/source-material/nodejs/cluster_example/routes.js diff --git a/redis/nodejs/cluster_example/server.js b/samples/azure-managed-redis/source-material/nodejs/cluster_example/server.js similarity index 100% rename from redis/nodejs/cluster_example/server.js rename to samples/azure-managed-redis/source-material/nodejs/cluster_example/server.js diff --git a/redis/nodejs/cluster_failover_recovery.excalidraw b/samples/azure-managed-redis/source-material/nodejs/cluster_failover_recovery.excalidraw similarity index 100% rename from redis/nodejs/cluster_failover_recovery.excalidraw rename to samples/azure-managed-redis/source-material/nodejs/cluster_failover_recovery.excalidraw diff --git a/monitor/assets/hdinsight-kafka-dashboard.properties.template.json b/samples/azure-monitor/source-material/assets/hdinsight-kafka-dashboard.properties.template.json similarity index 100% rename from monitor/assets/hdinsight-kafka-dashboard.properties.template.json rename to samples/azure-monitor/source-material/assets/hdinsight-kafka-dashboard.properties.template.json diff --git a/monitor/assets/hdinsight-kafka-workbook.template.json b/samples/azure-monitor/source-material/assets/hdinsight-kafka-workbook.template.json similarity index 100% rename from monitor/assets/hdinsight-kafka-workbook.template.json rename to samples/azure-monitor/source-material/assets/hdinsight-kafka-workbook.template.json diff --git a/samples/azure-monitor/source-material/assets/images/hdinsight_monitor_integration_1.png b/samples/azure-monitor/source-material/assets/images/hdinsight_monitor_integration_1.png new file mode 100644 index 00000000..adf48331 Binary files /dev/null and b/samples/azure-monitor/source-material/assets/images/hdinsight_monitor_integration_1.png differ diff --git a/samples/azure-monitor/source-material/assets/images/kafka-exporter-adv.png b/samples/azure-monitor/source-material/assets/images/kafka-exporter-adv.png new file mode 100644 index 00000000..270ae9a6 Binary files /dev/null and b/samples/azure-monitor/source-material/assets/images/kafka-exporter-adv.png differ diff --git a/samples/azure-monitor/source-material/assets/images/kafka-exporter-overview.png b/samples/azure-monitor/source-material/assets/images/kafka-exporter-overview.png new file mode 100644 index 00000000..cee3fb21 Binary files /dev/null and b/samples/azure-monitor/source-material/assets/images/kafka-exporter-overview.png differ diff --git a/samples/azure-monitor/source-material/assets/images/kafka-jmx-broker.png b/samples/azure-monitor/source-material/assets/images/kafka-jmx-broker.png new file mode 100644 index 00000000..71c821a1 Binary files /dev/null and b/samples/azure-monitor/source-material/assets/images/kafka-jmx-broker.png differ diff --git a/monitor/assets/prometheus-grafana/docker-compose.yml b/samples/azure-monitor/source-material/assets/prometheus-grafana/docker-compose.yml similarity index 100% rename from monitor/assets/prometheus-grafana/docker-compose.yml rename to samples/azure-monitor/source-material/assets/prometheus-grafana/docker-compose.yml diff --git a/monitor/assets/prometheus-grafana/grafana/dashboards/kafka-exporter-adv.json b/samples/azure-monitor/source-material/assets/prometheus-grafana/grafana/dashboards/kafka-exporter-adv.json similarity index 100% rename from monitor/assets/prometheus-grafana/grafana/dashboards/kafka-exporter-adv.json rename to samples/azure-monitor/source-material/assets/prometheus-grafana/grafana/dashboards/kafka-exporter-adv.json diff --git a/monitor/assets/prometheus-grafana/grafana/dashboards/kafka-jmx-broker.json b/samples/azure-monitor/source-material/assets/prometheus-grafana/grafana/dashboards/kafka-jmx-broker.json similarity index 100% rename from monitor/assets/prometheus-grafana/grafana/dashboards/kafka-jmx-broker.json rename to samples/azure-monitor/source-material/assets/prometheus-grafana/grafana/dashboards/kafka-jmx-broker.json diff --git a/monitor/assets/prometheus-grafana/grafana/provisioning/dashboards/provider.yml b/samples/azure-monitor/source-material/assets/prometheus-grafana/grafana/provisioning/dashboards/provider.yml similarity index 100% rename from monitor/assets/prometheus-grafana/grafana/provisioning/dashboards/provider.yml rename to samples/azure-monitor/source-material/assets/prometheus-grafana/grafana/provisioning/dashboards/provider.yml diff --git a/monitor/assets/prometheus-grafana/grafana/provisioning/datasources/prometheus.yml b/samples/azure-monitor/source-material/assets/prometheus-grafana/grafana/provisioning/datasources/prometheus.yml similarity index 100% rename from monitor/assets/prometheus-grafana/grafana/provisioning/datasources/prometheus.yml rename to samples/azure-monitor/source-material/assets/prometheus-grafana/grafana/provisioning/datasources/prometheus.yml diff --git a/monitor/assets/prometheus-grafana/jmx/b0.yml.example b/samples/azure-monitor/source-material/assets/prometheus-grafana/jmx/b0.yml.example similarity index 100% rename from monitor/assets/prometheus-grafana/jmx/b0.yml.example rename to samples/azure-monitor/source-material/assets/prometheus-grafana/jmx/b0.yml.example diff --git a/monitor/assets/prometheus-grafana/jmx/kafka-rules.yml b/samples/azure-monitor/source-material/assets/prometheus-grafana/jmx/kafka-rules.yml similarity index 100% rename from monitor/assets/prometheus-grafana/jmx/kafka-rules.yml rename to samples/azure-monitor/source-material/assets/prometheus-grafana/jmx/kafka-rules.yml diff --git a/monitor/assets/prometheus-grafana/prometheus.yml b/samples/azure-monitor/source-material/assets/prometheus-grafana/prometheus.yml similarity index 100% rename from monitor/assets/prometheus-grafana/prometheus.yml rename to samples/azure-monitor/source-material/assets/prometheus-grafana/prometheus.yml diff --git a/monitor/sre-agent-event-lab/.env.example b/samples/azure-monitor/source-material/sre-agent-event-lab/.env.example similarity index 100% rename from monitor/sre-agent-event-lab/.env.example rename to samples/azure-monitor/source-material/sre-agent-event-lab/.env.example diff --git a/monitor/sre-agent-event-lab/.gitignore b/samples/azure-monitor/source-material/sre-agent-event-lab/.gitignore similarity index 100% rename from monitor/sre-agent-event-lab/.gitignore rename to samples/azure-monitor/source-material/sre-agent-event-lab/.gitignore diff --git a/monitor/sre-agent-event-lab/app/.dockerignore b/samples/azure-monitor/source-material/sre-agent-event-lab/app/.dockerignore similarity index 100% rename from monitor/sre-agent-event-lab/app/.dockerignore rename to samples/azure-monitor/source-material/sre-agent-event-lab/app/.dockerignore diff --git a/monitor/sre-agent-event-lab/app/Dockerfile b/samples/azure-monitor/source-material/sre-agent-event-lab/app/Dockerfile similarity index 100% rename from monitor/sre-agent-event-lab/app/Dockerfile rename to samples/azure-monitor/source-material/sre-agent-event-lab/app/Dockerfile diff --git a/monitor/sre-agent-event-lab/app/main.py b/samples/azure-monitor/source-material/sre-agent-event-lab/app/main.py similarity index 100% rename from monitor/sre-agent-event-lab/app/main.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/app/main.py diff --git a/monitor/sre-agent-event-lab/app/requirements-dev.txt b/samples/azure-monitor/source-material/sre-agent-event-lab/app/requirements-dev.txt similarity index 100% rename from monitor/sre-agent-event-lab/app/requirements-dev.txt rename to samples/azure-monitor/source-material/sre-agent-event-lab/app/requirements-dev.txt diff --git a/monitor/sre-agent-event-lab/app/requirements.txt b/samples/azure-monitor/source-material/sre-agent-event-lab/app/requirements.txt similarity index 100% rename from monitor/sre-agent-event-lab/app/requirements.txt rename to samples/azure-monitor/source-material/sre-agent-event-lab/app/requirements.txt diff --git a/monitor/sre-agent-event-lab/app/telemetry.py b/samples/azure-monitor/source-material/sre-agent-event-lab/app/telemetry.py similarity index 100% rename from monitor/sre-agent-event-lab/app/telemetry.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/app/telemetry.py diff --git a/monitor/sre-agent-event-lab/app/tests/conftest.py b/samples/azure-monitor/source-material/sre-agent-event-lab/app/tests/conftest.py similarity index 100% rename from monitor/sre-agent-event-lab/app/tests/conftest.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/app/tests/conftest.py diff --git a/monitor/sre-agent-event-lab/app/tests/test_main.py b/samples/azure-monitor/source-material/sre-agent-event-lab/app/tests/test_main.py similarity index 100% rename from monitor/sre-agent-event-lab/app/tests/test_main.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/app/tests/test_main.py diff --git a/monitor/sre-agent-event-lab/app/tests/test_telemetry.py b/samples/azure-monitor/source-material/sre-agent-event-lab/app/tests/test_telemetry.py similarity index 100% rename from monitor/sre-agent-event-lab/app/tests/test_telemetry.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/app/tests/test_telemetry.py diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/briefing/s1-agent-conclusion.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/briefing/s1-agent-conclusion.png new file mode 100644 index 00000000..3ebb00f2 Binary files /dev/null and b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/briefing/s1-agent-conclusion.png differ diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/briefing/s1-three-panel.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/briefing/s1-three-panel.png new file mode 100644 index 00000000..f963e1e7 Binary files /dev/null and b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/briefing/s1-three-panel.png differ diff --git a/monitor/sre-agent-event-lab/assets/briefing/s1-three-panel.svg b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/briefing/s1-three-panel.svg similarity index 100% rename from monitor/sre-agent-event-lab/assets/briefing/s1-three-panel.svg rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/briefing/s1-three-panel.svg diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/briefing/sre-agent-process.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/briefing/sre-agent-process.png new file mode 100644 index 00000000..2d84f645 Binary files /dev/null and b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/briefing/sre-agent-process.png differ diff --git a/monitor/sre-agent-event-lab/assets/briefing/sre-agent-process.svg b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/briefing/sre-agent-process.svg similarity index 100% rename from monitor/sre-agent-event-lab/assets/briefing/sre-agent-process.svg rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/briefing/sre-agent-process.svg diff --git a/monitor/sre-agent-event-lab/assets/captures/s1-before-bridge/01-alert-fired.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1-before-bridge/01-alert-fired.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s1-before-bridge/01-alert-fired.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1-before-bridge/01-alert-fired.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s1-before-bridge/02-thread-not-created.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1-before-bridge/02-thread-not-created.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s1-before-bridge/02-thread-not-created.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1-before-bridge/02-thread-not-created.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s1-before-bridge/03-investigation-missing.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1-before-bridge/03-investigation-missing.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s1-before-bridge/03-investigation-missing.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1-before-bridge/03-investigation-missing.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s1-before-bridge/04-conclusion-missing.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1-before-bridge/04-conclusion-missing.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s1-before-bridge/04-conclusion-missing.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1-before-bridge/04-conclusion-missing.png diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1-before-bridge/investigation.gif b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1-before-bridge/investigation.gif new file mode 100644 index 00000000..524391c2 Binary files /dev/null and b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1-before-bridge/investigation.gif differ diff --git a/monitor/sre-agent-event-lab/assets/captures/s1-before-bridge/timeline.md b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1-before-bridge/timeline.md similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s1-before-bridge/timeline.md rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1-before-bridge/timeline.md diff --git a/monitor/sre-agent-event-lab/assets/captures/s1-before-bridge/timeline.mmd b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1-before-bridge/timeline.mmd similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s1-before-bridge/timeline.mmd rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1-before-bridge/timeline.mmd diff --git a/monitor/sre-agent-event-lab/assets/captures/s1/01-alert-fired.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/01-alert-fired.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s1/01-alert-fired.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/01-alert-fired.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s1/02-thread-created.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/02-thread-created.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s1/02-thread-created.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/02-thread-created.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s1/03-investigating.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/03-investigating.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s1/03-investigating.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/03-investigating.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s1/04-investigating.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/04-investigating.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s1/04-investigating.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/04-investigating.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s1/05-investigating.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/05-investigating.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s1/05-investigating.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/05-investigating.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s1/06-investigating.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/06-investigating.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s1/06-investigating.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/06-investigating.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s1/07-conclusion.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/07-conclusion.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s1/07-conclusion.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/07-conclusion.png diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/investigation.gif b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/investigation.gif new file mode 100644 index 00000000..4316475d Binary files /dev/null and b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/investigation.gif differ diff --git a/monitor/sre-agent-event-lab/assets/captures/s1/timeline.md b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/timeline.md similarity index 88% rename from monitor/sre-agent-event-lab/assets/captures/s1/timeline.md rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/timeline.md index e6b7c19a..95f20f12 100644 --- a/monitor/sre-agent-event-lab/assets/captures/s1/timeline.md +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/timeline.md @@ -14,4 +14,4 @@ | 2026-08-12T08:09:55.103364Z | 118s | investigating | sre-agent | **Analyzing activity logs** I’m noticing that we found 0011 modifying the config, but the activity logs only show the initial 0010. At 8:08, there’s no activity—it might be due to a system update. They mentioned a “smallest safe mitigation” for config changes, but we need to validate no new failures occurred after 0011. I’ll check the health and orders from 8:08:40 since I may not be able to query future dates. It’s odd that the current date is at 8:09. | | 2026-08-12T08:09:57.976331Z | 121s | investigating | sre-agent | **Validating deployment activity** I’m considering whether we need to read the git status, but it might not be necessary. I should validate the activity: the initial deployment was at 08:06:09 before the actual start at 08:06:52, with a revision occurring after a failure at around 08:08:49. The activity log likely has a delay in ingestion. I need to query everything post-mitigation from 08:08:49 onward, focusing on health metrics and ensuring there were no significant errors. I'll also look into the revision details for 0011. | | 2026-08-12T08:09:58.183582Z | 121s | investigating | sre-agent | The fault window was only 1.24 seconds: 120 `GET /api/orders` failures began at 08:06:52.147 UTC, after revision `0000010` deployed with `FAILURE_MODE=http500`. A succeeding revision now has `FAILURE_MODE=none`; I’m confirming post-change telemetry before closing the incident. | -| 2026-08-12T08:10:21.971535Z | 145s | conclusion | sre-agent | **Root cause confirmed:** Container App `ca-sre-event-lab-vnet` deployed revision `0000010` at 08:06:16 UTC with `FAILURE_MODE=http500`. - **Affected resource:** Application Insights `appi-sre-event-lab-95933ae5` - **Exact onset:** 2026-08-12 08:06:52.147 UTC - **Impact:** 120 HTTP 500s on `GET /api/orders` through 08:06:53.382 UTC. The alert fired at 08:07:56 UTC. - **Evidence:** Activity Log shows the Container App update began at 08:06:08.954 UTC; the active failing revision had `FAILURE_MODE=http500`. Trace evidence shows a 500 response without an external dependency timeout pattern. - **Mitigation:** Already applied externally: revision `0000011` is provisioned, receives 100% traffic, and has `FAILURE_MODE=none`. A post-change telemetry sample contained no 5xx responses. No resource changes were made by this investigation. | +| 2026-08-12T08:10:21.971535Z | 145s | conclusion | sre-agent | **Root cause confirmed:** Container App `ca-sre-event-lab-vnet` deployed revision `0000010` at 08:06:16 UTC with `FAILURE_MODE=http500`. - **Affected resource:** Application Insights `appi-sre-event-lab-example` - **Exact onset:** 2026-08-12 08:06:52.147 UTC - **Impact:** 120 HTTP 500s on `GET /api/orders` through 08:06:53.382 UTC. The alert fired at 08:07:56 UTC. - **Evidence:** Activity Log shows the Container App update began at 08:06:08.954 UTC; the active failing revision had `FAILURE_MODE=http500`. Trace evidence shows a 500 response without an external dependency timeout pattern. - **Mitigation:** Already applied externally: revision `0000011` is provisioned, receives 100% traffic, and has `FAILURE_MODE=none`. A post-change telemetry sample contained no 5xx responses. No resource changes were made by this investigation. | diff --git a/monitor/sre-agent-event-lab/assets/captures/s1/timeline.mmd b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/timeline.mmd similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s1/timeline.mmd rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s1/timeline.mmd diff --git a/monitor/sre-agent-event-lab/assets/captures/s2/01-alert-fired.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/01-alert-fired.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s2/01-alert-fired.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/01-alert-fired.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s2/02-thread-created.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/02-thread-created.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s2/02-thread-created.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/02-thread-created.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s2/03-investigating.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/03-investigating.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s2/03-investigating.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/03-investigating.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s2/04-investigating.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/04-investigating.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s2/04-investigating.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/04-investigating.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s2/05-investigating.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/05-investigating.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s2/05-investigating.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/05-investigating.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s2/06-investigating.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/06-investigating.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s2/06-investigating.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/06-investigating.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s2/07-conclusion.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/07-conclusion.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s2/07-conclusion.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/07-conclusion.png diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/investigation.gif b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/investigation.gif new file mode 100644 index 00000000..d9df97af Binary files /dev/null and b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/investigation.gif differ diff --git a/monitor/sre-agent-event-lab/assets/captures/s2/timeline.md b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/timeline.md similarity index 96% rename from monitor/sre-agent-event-lab/assets/captures/s2/timeline.md rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/timeline.md index 221baa1d..6c06f2b8 100644 --- a/monitor/sre-agent-event-lab/assets/captures/s2/timeline.md +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/timeline.md @@ -6,7 +6,7 @@ | 2026-08-12T08:18:11.169512Z | 21s | investigating | sre-agent | **Investigating resource context** I need to inspect the resource context, perhaps using Azure's resource graph and the main Bicep repo. It sounds like I need to notify the user before proceeding with Azure commands. Maybe Task Explore could help? I'm thinking about using Grep alongside Azure read to get activity logs. I could also find the current component resource ID with the Azure resource show command, along with scheduled alerts. A pre-execution notification seems necessary as well. | | 2026-08-12T08:18:12.519827Z | 23s | investigating | sre-agent | Checking the alert target, its hosting workload, and nearby Azure changes to establish the incident boundary. | | 2026-08-12T08:18:12.607717Z | 23s | investigating | sre-agent | Grep Search: Found 100 matches in 32 files for query 'api/orders\|orders\|latency\|sleep\|delay' | -| 2026-08-12T08:18:57.794361Z | 68s | investigating | sre-agent | The alert target is the `appi-sre-event-lab-95933ae5` Application Insights instance, and the activity timeline shows Container Apps updates through 08:15 UTC. I’m now separating dependency latency from application failure and inspecting the resulting revisions. | +| 2026-08-12T08:18:57.794361Z | 68s | investigating | sre-agent | The alert target is the `appi-sre-event-lab-example` Application Insights instance, and the activity timeline shows Container Apps updates through 08:15 UTC. I’m now separating dependency latency from application failure and inspecting the resulting revisions. | | 2026-08-12T08:18:57.850080Z | 68s | investigating | sre-agent | Read file: code: devguidesample/infra/modules/monitoring.bicep | | 2026-08-12T08:18:57.850357Z | 68s | investigating | sre-agent | Read file: code: devguidesample/infra/main.bicep | | 2026-08-12T08:18:57.865102Z | 68s | investigating | sre-agent | Grep Search: Found 2 matches in 1 files for query 'sre-event-lab\|/api/orders\|api/orders\|order' | diff --git a/monitor/sre-agent-event-lab/assets/captures/s2/timeline.mmd b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/timeline.mmd similarity index 97% rename from monitor/sre-agent-event-lab/assets/captures/s2/timeline.mmd rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/timeline.mmd index 80241704..8639c71b 100644 --- a/monitor/sre-agent-event-lab/assets/captures/s2/timeline.mmd +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s2/timeline.mmd @@ -8,7 +8,7 @@ sequenceDiagram SREAgent->>Evidence: investigating - **Investigating resource context** I need to inspect the resource context, perhaps using Azure's re SREAgent->>Evidence: investigating - Checking the alert target, its hosting workload, and nearby Azure changes to establish the incident SREAgent->>Evidence: investigating - Grep Search- Found 100 matches in 32 files for query 'api/orders|orders|latency|sleep|delay' - SREAgent->>Evidence: investigating - The alert target is the `appi-sre-event-lab-95933ae5` Application Insights instance, and the activit + SREAgent->>Evidence: investigating - The alert target is the `appi-sre-event-lab-example` Application Insights instance, and the activit SREAgent->>Evidence: investigating - Read file- code- devguidesample/infra/modules/monitoring.bicep SREAgent->>Evidence: investigating - Read file- code- devguidesample/infra/main.bicep SREAgent->>Evidence: investigating - Grep Search- Found 2 matches in 1 files for query 'sre-event-lab|/api/orders|api/orders|order' diff --git a/monitor/sre-agent-event-lab/assets/captures/s3/01-alert-fired.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/01-alert-fired.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s3/01-alert-fired.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/01-alert-fired.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s3/02-thread-created.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/02-thread-created.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s3/02-thread-created.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/02-thread-created.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s3/03-investigating.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/03-investigating.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s3/03-investigating.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/03-investigating.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s3/04-investigating.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/04-investigating.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s3/04-investigating.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/04-investigating.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s3/05-investigating.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/05-investigating.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s3/05-investigating.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/05-investigating.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s3/06-investigating.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/06-investigating.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s3/06-investigating.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/06-investigating.png diff --git a/monitor/sre-agent-event-lab/assets/captures/s3/07-conclusion.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/07-conclusion.png similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s3/07-conclusion.png rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/07-conclusion.png diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/investigation.gif b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/investigation.gif new file mode 100644 index 00000000..ba331b7e Binary files /dev/null and b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/investigation.gif differ diff --git a/monitor/sre-agent-event-lab/assets/captures/s3/timeline.md b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/timeline.md similarity index 87% rename from monitor/sre-agent-event-lab/assets/captures/s3/timeline.md rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/timeline.md index dafd51c7..9fbc61e5 100644 --- a/monitor/sre-agent-event-lab/assets/captures/s3/timeline.md +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/timeline.md @@ -28,4 +28,4 @@ | 2026-08-12T08:46:03.604102Z | 232s | investigating | sre-agent | **Evaluating KQL issues** I'm working through a problem with KQL where I'm trying to use `union kind=outer dependencies, requests`, but I suspect there are issues with different columns. The app-insights CLI might be referencing outdated table names, leading to invalid property errors. I think I can query requests between 8:42:45 and 8:45:30 and aggregate them separately from dependencies. Currently, there’s a 503 error and the container health is false, even though one replica is ready. I’ll consider mitigation options without a restart and check logs for errors. | | 2026-08-12T08:46:04.820660Z | 233s | investigating | sre-agent | **Analyzing query for failures** I need to run two queries to check for failures after adding a role. The current time is 08:45, and I want to specifically query the timestamp from the `dependencies` table. The KQL I'm thinking of is `dependencies \| where timestamp... \| summarize total=count(), failures=countif(resultCode == '403'), lastFailure=maxif(timestamp, resultCode == "403")`. I'm wondering if `maxif` is supported, and I might also simplify it to just check for `resultCode=="403"` since 8:42:45. I need to run a separate query for the requests with a 503 error and then check the role assignment state to ensure it’s processed correctly. | | 2026-08-12T08:46:06.560018Z | 235s | investigating | sre-agent | The role assignment is present, but recovery has not completed: at `08:45 UTC`, the public `/api/documents` endpoint still returns `503` while the replica remains ready. I’m separating post-restore dependency and request telemetry to distinguish RBAC propagation from a second fault. | -| 2026-08-12T08:46:37.172586Z | 266s | conclusion | sre-agent | ## Incident summary - **Alert:** `[SRE-LAB-S3] Blob dependency failures exceeded` (Sev2), fired **2026-08-12 08:42:11 UTC**. - **Affected path:** `GET /api/documents` on `ca-sre-event-lab`; Blob target `stsrelab95933ae5/documents`. - **Onset:** first `503` at **08:40:08.363 UTC**; first Blob `403` at **08:40:08.375 UTC**. ## Root cause The workload managed identity `b2be110a-9aae-463f-aac6-9238cf3a5362` lost its container-scoped **Storage Blob Data Reader** assignment at **08:40:07.988 UTC**. The storage call then failed with `AuthorizationPermissionMismatch`, which the app surfaced as `503`. ## Evidence - Activity Log shows `roleAssignments/delete` for the `documents` container at **08:40:07.988 UTC**. - A representative trace shows `list_blobs` failing against Blob Storage with `403` and `AuthorizationPermissionMismatch`. - The alert window contained **60** Blob `403` failures. - The role was restored at **08:42:48.660 UTC**; current assignment is `Storage Blob Data Reader` at the original container scope. ## Current status Recovery is **still propagating**: a Blob `403` occurred at **08:45:25 UTC**, and the external endpoint returned `503` at approximately **08:45 UTC**. The Container App revision is otherwise running with one ready replica. ## Smallest safe mitigation No additional change is recommended: the least-privilege role has already been restored at the correct scope. Allow RBAC propagation, then recheck `/api/documents` and Blob `403` telemetry. Do not restart or redeploy solely for this issue. The connected repository contains managed-identity Blob-client guidance but not this deployed lab workload’s source: [bench_runner.py](https://github.com/hellices/devguidesample/blob/main/aisearch/custom_vectorization/bench/bench_runner.py#L49). | +| 2026-08-12T08:46:37.172586Z | 266s | conclusion | sre-agent | ## Incident summary - **Alert:** `[SRE-LAB-S3] Blob dependency failures exceeded` (Sev2), fired **2026-08-12 08:42:11 UTC**. - **Affected path:** `GET /api/documents` on `ca-sre-event-lab`; Blob target `stexample00000000/documents`. - **Onset:** first `503` at **08:40:08.363 UTC**; first Blob `403` at **08:40:08.375 UTC**. ## Root cause The workload managed identity `` lost its container-scoped **Storage Blob Data Reader** assignment at **08:40:07.988 UTC**. The storage call then failed with `AuthorizationPermissionMismatch`, which the app surfaced as `503`. ## Evidence - Activity Log shows `roleAssignments/delete` for the `documents` container at **08:40:07.988 UTC**. - A representative trace shows `list_blobs` failing against Blob Storage with `403` and `AuthorizationPermissionMismatch`. - The alert window contained **60** Blob `403` failures. - The role was restored at **08:42:48.660 UTC**; current assignment is `Storage Blob Data Reader` at the original container scope. ## Current status Recovery is **still propagating**: a Blob `403` occurred at **08:45:25 UTC**, and the external endpoint returned `503` at approximately **08:45 UTC**. The Container App revision is otherwise running with one ready replica. ## Smallest safe mitigation No additional change is recommended: the least-privilege role has already been restored at the correct scope. Allow RBAC propagation, then recheck `/api/documents` and Blob `403` telemetry. Do not restart or redeploy solely for this issue. The connected repository contains managed-identity Blob-client guidance but not this deployed lab workload’s source: [bench_runner.py](https://github.com/hellices/devguidesample/blob/main/samples/azure-ai-search/source-material/custom_vectorization/bench/bench_runner.py#L49). | diff --git a/monitor/sre-agent-event-lab/assets/captures/s3/timeline.mmd b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/timeline.mmd similarity index 100% rename from monitor/sre-agent-event-lab/assets/captures/s3/timeline.mmd rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/captures/s3/timeline.mmd diff --git a/monitor/sre-agent-event-lab/assets/notifications/github-issue.json b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/github-issue.json similarity index 100% rename from monitor/sre-agent-event-lab/assets/notifications/github-issue.json rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/github-issue.json diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/github-issue.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/github-issue.png new file mode 100644 index 00000000..dd105359 Binary files /dev/null and b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/github-issue.png differ diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/s1-email-preview.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/s1-email-preview.png new file mode 100644 index 00000000..e58d1d1b Binary files /dev/null and b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/s1-email-preview.png differ diff --git a/monitor/sre-agent-event-lab/assets/notifications/s1-github-issue.md b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/s1-github-issue.md similarity index 84% rename from monitor/sre-agent-event-lab/assets/notifications/s1-github-issue.md rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/s1-github-issue.md index c6a653ab..cd68f0fc 100644 --- a/monitor/sre-agent-event-lab/assets/notifications/s1-github-issue.md +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/s1-github-issue.md @@ -25,7 +25,7 @@ Container App revision `0000010` was deployed with `FAILURE_MODE=http500`. - Agent conclusion: > Affected workload: `ca-sre-event-lab-vnet` -Telemetry source: `appi-sre-event-lab-95933ae5` +Telemetry source: `appi-sre-event-lab-example` Root cause: revision `0000010` had `FAILURE_MODE=http500` Impact: 120 `GET /api/orders` requests returned HTTP 500 Mitigation: a healthy revision with FAILURE_MODE=none restored service @@ -42,6 +42,6 @@ Resolved. A succeeding revision uses `FAILURE_MODE=none`, receives traffic, and ## Tracking -- Agent thread ID: `6dd0e640-d969-46cb-a976-7c81b66fcadc` -- Detailed validation appendix: monitor/sre-agent-event-lab/validation-results.md +- Agent thread ID: `` +- Detailed validation appendix: [Azure SRE Agent validation results](https://github.com/hellices/devguidesample/blob/main/docs/research/azure-monitor/sre-agent-validation-results/index.md) - Generated from actual Azure SRE Agent evidence; no resource change was executed by the Agent. diff --git a/monitor/sre-agent-event-lab/assets/notifications/s1-incident-summary.eml b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/s1-incident-summary.eml similarity index 93% rename from monitor/sre-agent-event-lab/assets/notifications/s1-incident-summary.eml rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/s1-incident-summary.eml index c21166c8..ab3fdd08 100644 --- a/monitor/sre-agent-event-lab/assets/notifications/s1-incident-summary.eml +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/s1-incident-summary.eml @@ -76,8 +76,8 @@ ize: 13px; }

Customer impact

120 GET /api/orders requests returned HTTP 500.

Root cause and evidence

-

Affected workload: ca-sre-event-lab-vnet
Telemetry source: appi-s= -re-event-lab-95933ae5
Root cause: revision 0000010 had FAILURE_MODE=3Dhttp= +

Affected workload: ca-sre-event-lab-vnet
Telemetry source: appi-s= +re-event-lab-example
Root cause: revision 0000010 had FAILURE_MODE=3Dhttp= 500
Impact: 120 GET /api/orders requests returned HTTP 500
Mitigation: = a healthy revision with FAILURE_MODE=3Dnone restored service

Mitigation and current status

@@ -97,8 +97,7 @@ ps://github.com/hellices/devguidesample/issues/43

monitor/sre-agent-event-lab/validation-results.md

- Draft artifact =E2=80=94 not sent. Agent thread 6dd0e640-d969-46cb-a976= --7c81b66fcadc + Draft artifact =E2=80=94 not sent. Agent thread <agent-thread-id-s1>
diff --git a/monitor/sre-agent-event-lab/assets/notifications/s1-incident-summary.html b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/s1-incident-summary.html similarity index 89% rename from monitor/sre-agent-event-lab/assets/notifications/s1-incident-summary.html rename to samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/s1-incident-summary.html index 0c47d084..f0b600f6 100644 --- a/monitor/sre-agent-event-lab/assets/notifications/s1-incident-summary.html +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/notifications/s1-incident-summary.html @@ -34,7 +34,7 @@

Order API HTTP 500 incident

Customer impact

120 GET /api/orders requests returned HTTP 500.

Root cause and evidence

-

Affected workload: ca-sre-event-lab-vnet
Telemetry source: appi-sre-event-lab-95933ae5
Root cause: revision 0000010 had FAILURE_MODE=http500
Impact: 120 GET /api/orders requests returned HTTP 500
Mitigation: a healthy revision with FAILURE_MODE=none restored service

+

Affected workload: ca-sre-event-lab-vnet
Telemetry source: appi-sre-event-lab-example
Root cause: revision 0000010 had FAILURE_MODE=http500
Impact: 120 GET /api/orders requests returned HTTP 500
Mitigation: a healthy revision with FAILURE_MODE=none restored service

Mitigation and current status

A healthy revision with FAILURE_MODE=none restored service. The Agent remained in Review mode and did not modify Azure resources.

Ticket

@@ -48,7 +48,7 @@

Follow-up

monitor/sre-agent-event-lab/validation-results.md

- Draft artifact — not sent. Agent thread 6dd0e640-d969-46cb-a976-7c81b66fcadc + Draft artifact — not sent. Agent thread <agent-thread-id-s1>
diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/agent-reasoning-flow.svg b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/agent-reasoning-flow.svg new file mode 100644 index 00000000..2b0b0b00 --- /dev/null +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/agent-reasoning-flow.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + Your question + or incident alert + + + + + + + Understand + parse request + + + + + + + Gather context + parallel queries + + + + + + + Reason + analyze patterns + + + + + + + + + Execute + safe actions run immediately + + + + Ask for approval + destructive actions wait + + + + Respond + present findings + + + + iterate (up to 10×) + + + Your agent adjusts reasoning depth automatically — simple questions get quick answers, complex incidents get multistep analysis. + diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/azure-sre-agent-networking-vnet.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/azure-sre-agent-networking-vnet.png new file mode 100644 index 00000000..172c118c Binary files /dev/null and b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/azure-sre-agent-networking-vnet.png differ diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/custom-skill-flow.svg b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/custom-skill-flow.svg new file mode 100644 index 00000000..136ef56d --- /dev/null +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/custom-skill-flow.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + Agent + + + + + + + Custom Skill + + + + + + + Your API + + + + + + + Returns data + + + + + + + → + diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/diagnose-azure-services.svg b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/diagnose-azure-services.svg new file mode 100644 index 00000000..31cad4ac --- /dev/null +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/diagnose-azure-services.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + How your agent diagnoses Azure services + + + + Service + issue + + + + + + + SRE Agent + queries all sources + correlates evidence + explains root cause + + + + + + Application Insights + KQL queries, traces, dependencies + + + + + Log Analytics + Workspace logs, custom events + + + + + Azure Monitor Metrics + CPU, memory, requests, analysis + + + + + Resource Graph & Activity + Topology, changes, deployment logs + + + + + Built-in Skills & Az CLI + Deep diagnostics, connectivity, + resource-specific checks, remediation + + + + + + + + + + + + Correlated + diagnosis + Evidence from all sources + + + No connectors needed — works through managed identity and Azure RBAC + diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/incident-platform-flow.svg b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/incident-platform-flow.svg new file mode 100644 index 00000000..d2960e1f --- /dev/null +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/incident-platform-flow.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + Incident Platform + PagerDuty + Azure Monitor + + + + Alert + + + + Response + Plan + Filter + Mode + + + + Match + + + + Agent + Investigates + Automatically + + + + Findings + + + + Take Action + Mitigate issue + Update incident + + + + + + + Track + Metrics + + + Connect your incident platform → Agent responds automatically → Track effectiveness + diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/incident-response-flow.svg b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/incident-response-flow.svg new file mode 100644 index 00000000..9d75e04f --- /dev/null +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/incident-response-flow.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + From alert to resolution in minutes, not hours + + + + Alert Fires + PagerDuty + Azure Monitor + ServiceNow + + + + 30s + + + + Agent + Acknowledges + Notifies team + Starts investigation + + + + 1 min + + + + Investigates + Queries logs + Checks metrics + Correlates deployments + + + + 2 min + + + + Root Cause + Found + Proposes fix + or acts autonomously + + + + + + + Resolved + Ticket updated + Team notified + + + + + + + + + + 0:00 + 0:30 + 1:30 + 3:00 + 5:00 + + Minutes elapsed + diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/knowledge-sources.svg b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/knowledge-sources.svg new file mode 100644 index 00000000..ef2bb279 --- /dev/null +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/knowledge-sources.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + Knowledge Base Sources + + + + Upload Documents + Builder → Knowledge base + + 📄 Runbooks (.md, .txt) + 📄 Architecture guides + 📄 On-call playbooks + 📄 API documentation + 📄 Team procedures + + + + MCP Connectors + Settings → Connectors + + 🔗 Azure DevOps wikis + 🔗 GitHub repos and wikis + 🔗 Microsoft Learn docs + 🔗 Confluence pages + 🔗 Custom MCP servers + + + + + + + Agent's + Knowledge Base + Searched via + SearchMemory + + citations + diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/managed-connectors-icon-grid.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/managed-connectors-icon-grid.png new file mode 100644 index 00000000..b8750f79 Binary files /dev/null and b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/managed-connectors-icon-grid.png differ diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/memory-auto-learning.svg b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/memory-auto-learning.svg new file mode 100644 index 00000000..c8e1821f --- /dev/null +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/memory-auto-learning.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + Your agent learns from every session + + + + Thread + Completes + Sync or Async + + + + 30 min + + + + Session + Evaluator + Auto-extracts learnings + + + + + + + Learns: + • Symptoms observed + • Steps that worked + • Root cause found + • Pitfalls to avoid + + + + + + + Indexed + Memory + For future use + + + Next time a similar incident occurs, your agent recalls what worked + diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/memory-unified-search.svg b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/memory-unified-search.svg new file mode 100644 index 00000000..c370461b --- /dev/null +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/memory-unified-search.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + Sync Chat + Your questions + + + + Async + Auto-triggered + + + + + + + + SearchMemory + Searches all sources + simultaneously + + + + + + Past Sessions + Symptoms, Root Cause + Steps, Pitfalls + Auto-learned + + + + + User Memories + #remember facts + Explicit saves + + + + + Knowledge Base + Runbooks, Docs + Architecture guides + Uploaded + Connectors + + + + + + + + + Grounded + Response + With clickable citations + + + + Learnings saved back to memory (30 min after thread ends) + diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/notification-paths.svg b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/notification-paths.svg new file mode 100644 index 00000000..45a7ed07 --- /dev/null +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/notification-paths.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + Agent + investigates + and recommends + + + + + + Response paths + + + + Execute fix + Scale, restart, rollback + + + + Create work item + ADO, Jira, GitHub + + + + Send notification + Teams, Outlook, Slack + + + + + + + + + Azure resources + Container Apps, VMs, AKS + + + Tracking systems + With context + evidence + + + Team channels + Formatted summary + + + + + + + diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/operations-hub-overview-tab.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/operations-hub-overview-tab.png new file mode 100644 index 00000000..f91fb636 Binary files /dev/null and b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/operations-hub-overview-tab.png differ diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/permission-flow.svg b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/permission-flow.svg new file mode 100644 index 00000000..f3ef5db3 --- /dev/null +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/permission-flow.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + Agent needs to + perform action + + + + + + + Managed identity + has RBAC role? + + + + YES + + + Use managed + identity + + + + + + + NO + + + Request user + permissions (OBO) + + + + + + + Action + performed + + Restart service + Scale resources + Query logs + Modify config + Read metrics + diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/portal-complete-setup-page.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/portal-complete-setup-page.png new file mode 100644 index 00000000..75ceb9af Binary files /dev/null and b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/portal-complete-setup-page.png differ diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/portal-incident-response-plans-list.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/portal-incident-response-plans-list.png new file mode 100644 index 00000000..398a7ee0 Binary files /dev/null and b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/portal-incident-response-plans-list.png differ diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/portal-response-plan-autonomy-step.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/portal-response-plan-autonomy-step.png new file mode 100644 index 00000000..b3ecd6c2 Binary files /dev/null and b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/portal-response-plan-autonomy-step.png differ diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/portal-setup-status-bar.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/portal-setup-status-bar.png new file mode 100644 index 00000000..26304e90 Binary files /dev/null and b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/portal-setup-status-bar.png differ diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/portal-sub-agent-canvas-full.png b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/portal-sub-agent-canvas-full.png new file mode 100644 index 00000000..3f6ac2fd Binary files /dev/null and b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/portal-sub-agent-canvas-full.png differ diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/root-cause-analysis.svg b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/root-cause-analysis.svg new file mode 100644 index 00000000..fe24fa57 --- /dev/null +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/root-cause-analysis.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + Symptoms + errors, alerts + + + + + + Gather context + logs, metrics, deploys + + + + + + Form hypotheses + test & validate + + + + + + Root cause + with evidence + + + + + + Fix + resolve + + + + App Insights + + + Azure Monitor + + + Deployments + + + Source code + + + Memory + + + + + + + + + + Agent queries multiple data sources and correlates evidence to identify root cause + diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/run-modes-comparison.svg b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/run-modes-comparison.svg new file mode 100644 index 00000000..680042a8 --- /dev/null +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/assets/official/run-modes-comparison.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + Review Mode + + + + Agent + + + + + Proposes + action + + + + + User + + + Autonomous Mode + + + + Agent + + + + + Executes + immediately + + + + + Done + + + + User approves or denies + before agent acts + + + Agent completes task + independently + diff --git a/monitor/sre-agent-event-lab/azure.yaml b/samples/azure-monitor/source-material/sre-agent-event-lab/azure.yaml similarity index 100% rename from monitor/sre-agent-event-lab/azure.yaml rename to samples/azure-monitor/source-material/sre-agent-event-lab/azure.yaml diff --git a/monitor/sre-agent-event-lab/infra/alerts.bicep b/samples/azure-monitor/source-material/sre-agent-event-lab/infra/alerts.bicep similarity index 100% rename from monitor/sre-agent-event-lab/infra/alerts.bicep rename to samples/azure-monitor/source-material/sre-agent-event-lab/infra/alerts.bicep diff --git a/monitor/sre-agent-event-lab/infra/lab.bicep b/samples/azure-monitor/source-material/sre-agent-event-lab/infra/lab.bicep similarity index 100% rename from monitor/sre-agent-event-lab/infra/lab.bicep rename to samples/azure-monitor/source-material/sre-agent-event-lab/infra/lab.bicep diff --git a/monitor/sre-agent-event-lab/infra/main.bicep b/samples/azure-monitor/source-material/sre-agent-event-lab/infra/main.bicep similarity index 100% rename from monitor/sre-agent-event-lab/infra/main.bicep rename to samples/azure-monitor/source-material/sre-agent-event-lab/infra/main.bicep diff --git a/monitor/sre-agent-event-lab/infra/main.parameters.json b/samples/azure-monitor/source-material/sre-agent-event-lab/infra/main.parameters.json similarity index 100% rename from monitor/sre-agent-event-lab/infra/main.parameters.json rename to samples/azure-monitor/source-material/sre-agent-event-lab/infra/main.parameters.json diff --git a/monitor/sre-agent-event-lab/infra/observability.bicep b/samples/azure-monitor/source-material/sre-agent-event-lab/infra/observability.bicep similarity index 100% rename from monitor/sre-agent-event-lab/infra/observability.bicep rename to samples/azure-monitor/source-material/sre-agent-event-lab/infra/observability.bicep diff --git a/monitor/sre-agent-event-lab/infra/tests/test_alerts_bicep.py b/samples/azure-monitor/source-material/sre-agent-event-lab/infra/tests/test_alerts_bicep.py similarity index 100% rename from monitor/sre-agent-event-lab/infra/tests/test_alerts_bicep.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/infra/tests/test_alerts_bicep.py diff --git a/monitor/sre-agent-event-lab/infra/tests/test_azd_project.py b/samples/azure-monitor/source-material/sre-agent-event-lab/infra/tests/test_azd_project.py similarity index 96% rename from monitor/sre-agent-event-lab/infra/tests/test_azd_project.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/infra/tests/test_azd_project.py index 6236bc05..f6d87559 100644 --- a/monitor/sre-agent-event-lab/infra/tests/test_azd_project.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/infra/tests/test_azd_project.py @@ -6,6 +6,15 @@ LAB_ROOT = Path(__file__).parents[2] +REPO_ROOT = Path(__file__).parents[6] +LAB_PAGE = ( + REPO_ROOT + / "docs" + / "labs" + / "azure-monitor" + / "sre-agent-event-lab" + / "index.md" +) PLACEHOLDER_IMAGE = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest" # Everything that changes the deployed application. `azd provision` must do @@ -296,7 +305,7 @@ def test_main_parameters_map_action_group_and_deployed_image(): def test_hardcoded_lab_bicepparam_is_deleted(): - """lab.bicepparam pinned a dead suffix (95933ae5) and an expiry date; + """lab.bicepparam pinned a deployment-specific suffix and an expiry date; azd owns those values now, so the file must not linger. """ assert not (LAB_ROOT / "infra" / "lab.bicepparam").exists() @@ -360,8 +369,9 @@ def test_azd_onboarding_docs_and_config_do_not_hardcode_a_subscription_id(): subscription too, and so this test does not have to restate a real subscription ID in order to prove it is absent. """ + assert LAB_PAGE.is_file(), f"lab page missing: {LAB_PAGE}" onboarding_paths = [ - LAB_ROOT / "README.md", + LAB_PAGE, LAB_ROOT / "azure.yaml", LAB_ROOT / "infra" / "main.bicep", LAB_ROOT / "infra" / "lab.bicep", @@ -375,9 +385,11 @@ def test_azd_onboarding_docs_and_config_do_not_hardcode_a_subscription_id(): ] offenders = { - str(path.relative_to(LAB_ROOT)): leaked_guids(path.read_text()) + str(path.relative_to(REPO_ROOT)): leaked_guids( + path.read_text(encoding="utf-8") + ) for path in onboarding_paths - if path.is_file() and leaked_guids(path.read_text()) + if path.is_file() and leaked_guids(path.read_text(encoding="utf-8")) } assert offenders == {}, ( diff --git a/monitor/sre-agent-event-lab/infra/tests/test_workload_networking.py b/samples/azure-monitor/source-material/sre-agent-event-lab/infra/tests/test_workload_networking.py similarity index 100% rename from monitor/sre-agent-event-lab/infra/tests/test_workload_networking.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/infra/tests/test_workload_networking.py diff --git a/monitor/sre-agent-event-lab/infra/workload.bicep b/samples/azure-monitor/source-material/sre-agent-event-lab/infra/workload.bicep similarity index 100% rename from monitor/sre-agent-event-lab/infra/workload.bicep rename to samples/azure-monitor/source-material/sre-agent-event-lab/infra/workload.bicep diff --git a/monitor/sre-agent-event-lab/scripts/azd-configure.sh b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/azd-configure.sh similarity index 100% rename from monitor/sre-agent-event-lab/scripts/azd-configure.sh rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/azd-configure.sh diff --git a/monitor/sre-agent-event-lab/scripts/azd-deploy-app.sh b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/azd-deploy-app.sh similarity index 100% rename from monitor/sre-agent-event-lab/scripts/azd-deploy-app.sh rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/azd-deploy-app.sh diff --git a/monitor/sre-agent-event-lab/scripts/azd-postprovision-local.sh b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/azd-postprovision-local.sh similarity index 100% rename from monitor/sre-agent-event-lab/scripts/azd-postprovision-local.sh rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/azd-postprovision-local.sh diff --git a/monitor/sre-agent-event-lab/scripts/capture_agent.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/capture_agent.py old mode 100755 new mode 100644 similarity index 100% rename from monitor/sre-agent-event-lab/scripts/capture_agent.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/capture_agent.py diff --git a/monitor/sre-agent-event-lab/scripts/capture_model.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/capture_model.py old mode 100755 new mode 100644 similarity index 100% rename from monitor/sre-agent-event-lab/scripts/capture_model.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/capture_model.py diff --git a/monitor/sre-agent-event-lab/scripts/cleanup-external.sh b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/cleanup-external.sh similarity index 96% rename from monitor/sre-agent-event-lab/scripts/cleanup-external.sh rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/cleanup-external.sh index 389898a1..2610c4d1 100755 --- a/monitor/sre-agent-event-lab/scripts/cleanup-external.sh +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/cleanup-external.sh @@ -160,7 +160,7 @@ if ! RECORDED_ASSIGNMENTS="$(jq -r ' | join("|") ' "${CLEANUP_SETUP_FILE}" 2>/dev/null)"; then echo "Agent setup evidence is not valid JSON: ${CLEANUP_SETUP_FILE}" >&2 - echo "Rewrite evidence/agent-setup.json as shown in guides/01-agent-setup.md, then run: python3 scripts/lab_state.py acknowledge-agent" >&2 + echo "Rewrite evidence/agent-setup.json as shown in docs/labs/azure-monitor/sre-agent-event-lab-setup/index.md, then run: python3 scripts/lab_state.py acknowledge-agent" >&2 exit 1 fi readonly RECORDED_ASSIGNMENTS @@ -260,12 +260,12 @@ while IFS='|' read -r assignment_key assignment_id principal_key expected_princi echo "${assignment_key} is empty for principal ${expected_principal_id:-unknown}." >&2 echo "Find the live ID: az role assignment list --assignee-object-id ${expected_principal_id:-} --role \"Monitoring Contributor\" --scope ${SUBSCRIPTION_SCOPE} --query \"[0].id\" -o tsv" >&2 echo "If no assignment is returned and teardown should continue, clear both ${assignment_key} and ${principal_key} in the evidence file." >&2 - echo "Update evidence/agent-setup.json as shown in guides/01-agent-setup.md, then run: python3 scripts/lab_state.py acknowledge-agent" >&2 + echo "Update evidence/agent-setup.json as shown in docs/labs/azure-monitor/sre-agent-event-lab-setup/index.md, then run: python3 scripts/lab_state.py acknowledge-agent" >&2 exit 1 fi if [[ -z "${expected_principal_id}" ]]; then echo "Incomplete Agent setup evidence: ${assignment_id} was recorded without ${principal_key}." >&2 - echo "Update evidence/agent-setup.json as shown in guides/01-agent-setup.md, then run: python3 scripts/lab_state.py acknowledge-agent" >&2 + echo "Update evidence/agent-setup.json as shown in docs/labs/azure-monitor/sre-agent-event-lab-setup/index.md, then run: python3 scripts/lab_state.py acknowledge-agent" >&2 exit 1 fi case "${VERIFIED_ASSIGNMENT_IDS}" in diff --git a/monitor/sre-agent-event-lab/scripts/common.sh b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/common.sh similarity index 99% rename from monitor/sre-agent-event-lab/scripts/common.sh rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/common.sh index 46c8b210..c1d189fe 100755 --- a/monitor/sre-agent-event-lab/scripts/common.sh +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/common.sh @@ -273,7 +273,8 @@ evidence_dir_path() { # create_evidence_dir SCENARIO -- name it and create it in one step, for # callers that write into it immediately and have nothing left to refuse -# them (the baseline step in `guides/01-agent-setup.md`). +# them (the baseline step in +# `docs/labs/azure-monitor/sre-agent-event-lab-setup/index.md`). create_evidence_dir() { local directory directory="$(evidence_dir_path "$1")" diff --git a/monitor/sre-agent-event-lab/scripts/generate_notifications.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/generate_notifications.py old mode 100755 new mode 100644 similarity index 99% rename from monitor/sre-agent-event-lab/scripts/generate_notifications.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/generate_notifications.py index 91ee0fc6..f6156873 --- a/monitor/sre-agent-event-lab/scripts/generate_notifications.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/generate_notifications.py @@ -43,7 +43,7 @@ def verified_summary(markdown: bool = False) -> str: return "\n".join( ( f"Affected workload: {code('ca-sre-event-lab-vnet')}", - f"Telemetry source: {code('appi-sre-event-lab-95933ae5')}", + f"Telemetry source: {code('appi-sre-event-lab-example')}", f"Root cause: revision {code('0000010')} had {code('FAILURE_MODE=http500')}", f"Impact: 120 {code('GET /api/orders')} requests returned HTTP 500", "Mitigation: a healthy revision with FAILURE_MODE=none restored service", diff --git a/monitor/sre-agent-event-lab/scripts/lab-env.sh b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/lab-env.sh similarity index 99% rename from monitor/sre-agent-event-lab/scripts/lab-env.sh rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/lab-env.sh index d23d0548..958322f5 100755 --- a/monitor/sre-agent-event-lab/scripts/lab-env.sh +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/lab-env.sh @@ -2,7 +2,7 @@ # Resolves the lab's configuration once and exports it into the current # shell. Source it; do not execute it: # -# cd monitor/sre-agent-event-lab +# cd samples/azure-monitor/source-material/sre-agent-event-lab # source ./scripts/lab-env.sh # # Every value here is a name, an ID or a URL that `azd provision` already diff --git a/monitor/sre-agent-event-lab/scripts/lab_state.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/lab_state.py old mode 100755 new mode 100644 similarity index 98% rename from monitor/sre-agent-event-lab/scripts/lab_state.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/lab_state.py index 6b3b63bd..6afeb9e2 --- a/monitor/sre-agent-event-lab/scripts/lab_state.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/lab_state.py @@ -178,16 +178,18 @@ def terminal_state(events: Iterable[Dict[str, Any]]) -> str: # messages name it instead of a command, because the lab is run by hand: # there is no script that performs a scenario. SCENARIO_GUIDES = { - "s1": "guides/02-scenario-s1.md", - "s2": "guides/03-scenario-s2.md", - "s3": "guides/04-scenario-s3.md", + "s1": "docs/labs/azure-monitor/sre-agent-scenario-http-500/index.md", + "s2": "docs/labs/azure-monitor/sre-agent-scenario-latency/index.md", + "s3": "docs/labs/azure-monitor/sre-agent-scenario-blob-permission/index.md", } def scenario_guide(scenario: str) -> str: # A remedy that cannot be acted on is not a remedy: an unmapped # scenario still has to say where to look. - return SCENARIO_GUIDES.get(scenario, "the matching guide under guides/") + return SCENARIO_GUIDES.get( + scenario, "the matching page under docs/labs/azure-monitor/" + ) def _scenario_stage(stage: str) -> Optional[Sequence[str]]: @@ -488,7 +490,8 @@ def _remedy(self, missing: Sequence[str]) -> str: """ remedies = { "baseline_passed": ( - "Run the baseline steps in guides/01-agent-setup.md, then: " + "Run the baseline steps in " + "docs/labs/azure-monitor/sre-agent-event-lab-setup/index.md, then: " "lab_state.py mark baseline_passed" ), "agent_setup_acknowledged": "Run: lab_state.py acknowledge-agent", diff --git a/monitor/sre-agent-event-lab/scripts/loadgen.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/loadgen.py old mode 100755 new mode 100644 similarity index 100% rename from monitor/sre-agent-event-lab/scripts/loadgen.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/loadgen.py diff --git a/monitor/sre-agent-event-lab/scripts/query-evidence.sh b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/query-evidence.sh similarity index 100% rename from monitor/sre-agent-event-lab/scripts/query-evidence.sh rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/query-evidence.sh diff --git a/monitor/sre-agent-event-lab/scripts/render_briefing_assets.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/render_briefing_assets.py old mode 100755 new mode 100644 similarity index 99% rename from monitor/sre-agent-event-lab/scripts/render_briefing_assets.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/render_briefing_assets.py index e11f53ce..1c57b385 --- a/monitor/sre-agent-event-lab/scripts/render_briefing_assets.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/render_briefing_assets.py @@ -274,7 +274,7 @@ def save_conclusion_png(path: Path, timeline: list[dict[str, Any]]) -> None: ) fields = [ ("영향을 받은 서비스", "ca-sre-event-lab-vnet"), - ("원격 분석 원본", "appi-sre-event-lab-95933ae5"), + ("원격 분석 원본", "appi-sre-event-lab-example"), ("근본 원인", "revision 0000010에 FAILURE_MODE=http500 설정"), ("고객 영향", "GET /api/orders 요청 120건 실패"), ("완화 조치", "정상 설정을 사용한 revision으로 복귀"), diff --git a/monitor/sre-agent-event-lab/scripts/render_capture.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/render_capture.py old mode 100755 new mode 100644 similarity index 100% rename from monitor/sre-agent-event-lab/scripts/render_capture.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/render_capture.py diff --git a/monitor/sre-agent-event-lab/scripts/score.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/score.py old mode 100755 new mode 100644 similarity index 98% rename from monitor/sre-agent-event-lab/scripts/score.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/score.py index edf0b5cd..c7d3161b --- a/monitor/sre-agent-event-lab/scripts/score.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/score.py @@ -333,7 +333,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if not any(state.capture_status(scenario) for scenario in SCENARIOS): print( "No captured scenario evidence in {0}. Run the s1 steps in " - "guides/02-scenario-s1.md, capture included.".format(evidence_root), + "docs/labs/azure-monitor/sre-agent-scenario-http-500/index.md, " + "capture included.".format(evidence_root), file=sys.stderr, ) return 1 diff --git a/monitor/sre-agent-event-lab/scripts/setup-venv.sh b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/setup-venv.sh similarity index 98% rename from monitor/sre-agent-event-lab/scripts/setup-venv.sh rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/setup-venv.sh index b0489ba8..4536ed8d 100755 --- a/monitor/sre-agent-event-lab/scripts/setup-venv.sh +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/setup-venv.sh @@ -5,7 +5,8 @@ set -euo pipefail # needs: the app's own runtime dependencies (`requirements.txt`, pulled in # by `-r requirements.txt` at the top of `requirements-dev.txt`), Pillow for # `render_capture.py`'s PNG/GIF rendering, and pytest/httpx for `app/tests`. -# The capture step in each scenario guide and `guides/05-results.md`'s +# The capture step in each scenario guide and +# `docs/labs/azure-monitor/sre-agent-results/index.md`'s # notification step both # run under this interpreter. # diff --git a/monitor/sre-agent-event-lab/scripts/tests/azd_common_harness.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/azd_common_harness.py similarity index 100% rename from monitor/sre-agent-event-lab/scripts/tests/azd_common_harness.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/azd_common_harness.py diff --git a/monitor/sre-agent-event-lab/scripts/tests/azd_fake.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/azd_fake.py similarity index 100% rename from monitor/sre-agent-event-lab/scripts/tests/azd_fake.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/azd_fake.py diff --git a/monitor/sre-agent-event-lab/scripts/tests/cleanup_harness.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/cleanup_harness.py similarity index 100% rename from monitor/sre-agent-event-lab/scripts/tests/cleanup_harness.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/cleanup_harness.py diff --git a/monitor/sre-agent-event-lab/scripts/tests/deploy_app_harness.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/deploy_app_harness.py similarity index 99% rename from monitor/sre-agent-event-lab/scripts/tests/deploy_app_harness.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/deploy_app_harness.py index 2f5a1dc7..bcc2fd25 100644 --- a/monitor/sre-agent-event-lab/scripts/tests/deploy_app_harness.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/deploy_app_harness.py @@ -63,7 +63,7 @@ f"/subscriptions/{SUBSCRIPTION_ID}/resourceGroups/{RESOURCE_GROUP}" ) CONTAINER_APP_NAME = "ca-sre-event-lab-vnet" -CONTAINER_APP_FQDN = "ca-sre-event-lab-vnet.koreacentral.azurecontainerapps.io" +CONTAINER_APP_FQDN = "ca-sre-event-lab-vnet.example.koreacentral.azurecontainerapps.io" WORKLOAD_PRINCIPAL_ID = "aaaaaaaa-0000-4000-8000-aaaaaaaaaaaa" WORKLOAD_IDENTITY_RESOURCE_ID = ( f"/subscriptions/{SUBSCRIPTION_ID}/resourceGroups/{RESOURCE_GROUP}" diff --git a/monitor/sre-agent-event-lab/scripts/tests/lab_script_harness.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/lab_script_harness.py similarity index 100% rename from monitor/sre-agent-event-lab/scripts/tests/lab_script_harness.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/lab_script_harness.py diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/published_layout.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/published_layout.py new file mode 100644 index 00000000..f4ae07ac --- /dev/null +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/published_layout.py @@ -0,0 +1,61 @@ +"""Canonical repository paths for the SRE lab's published documentation.""" + +from __future__ import annotations + +from pathlib import Path + + +REPO_ROOT = Path(__file__).parents[6] +LAB_ROOT = Path(__file__).parents[2] +DOCS_ROOT = REPO_ROOT / "docs" + +README = DOCS_ROOT / "labs" / "azure-monitor" / "sre-agent-event-lab" / "index.md" +GUIDE_PATHS = { + "01-agent-setup.md": ( + DOCS_ROOT / "labs" / "azure-monitor" / "sre-agent-event-lab-setup" / "index.md" + ), + "02-scenario-s1.md": ( + DOCS_ROOT / "labs" / "azure-monitor" / "sre-agent-scenario-http-500" / "index.md" + ), + "03-scenario-s2.md": ( + DOCS_ROOT / "labs" / "azure-monitor" / "sre-agent-scenario-latency" / "index.md" + ), + "04-scenario-s3.md": ( + DOCS_ROOT + / "labs" + / "azure-monitor" + / "sre-agent-scenario-blob-permission" + / "index.md" + ), + "05-results.md": ( + DOCS_ROOT / "labs" / "azure-monitor" / "sre-agent-results" / "index.md" + ), +} + + +class PublishedGuideDirectory: + """Provide the former directory interface over published page bundles.""" + + def __truediv__(self, name: str | Path) -> Path: + return GUIDE_PATHS[str(name)] + + def glob(self, pattern: str): + if pattern != "*.md": + return iter(()) + return iter(GUIDE_PATHS.values()) + + +GUIDES = PublishedGuideDirectory() +RESULTS_GUIDE = GUIDE_PATHS["05-results.md"] +RUNBOOK = DOCS_ROOT / "guides" / "azure-monitor" / "sre-agent-incident-runbook" / "index.md" +DYNAMIC_THRESHOLDS = ( + DOCS_ROOT / "guides" / "azure-monitor" / "sre-agent-dynamic-thresholds" / "index.md" +) +VALIDATION_RESULTS = ( + DOCS_ROOT / "research" / "azure-monitor" / "sre-agent-validation-results" / "index.md" +) +BRIEFING = ( + DOCS_ROOT / "guides" / "azure-monitor" / "azure-sre-agent-overview" / "index.md" +) +OFFICIAL_ASSETS = LAB_ROOT / "assets" / "official" +DEPLOYMENT_PLAN = REPO_ROOT / ".azure" / "deployment-plan.md" diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_azd_deploy_app.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_azd_deploy_app.py similarity index 100% rename from monitor/sre-agent-event-lab/scripts/tests/test_azd_deploy_app.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_azd_deploy_app.py diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_azd_env.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_azd_env.py similarity index 100% rename from monitor/sre-agent-event-lab/scripts/tests/test_azd_env.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_azd_env.py diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_azd_hooks.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_azd_hooks.py similarity index 99% rename from monitor/sre-agent-event-lab/scripts/tests/test_azd_hooks.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_azd_hooks.py index 5f865aff..a236f01a 100644 --- a/monitor/sre-agent-event-lab/scripts/tests/test_azd_hooks.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_azd_hooks.py @@ -296,6 +296,8 @@ def _run_hook_script(script_path, tmp_path, az_stub_factory, environment=None): bin_dir.mkdir(exist_ok=True) log_path = tmp_path / "az-calls.log" az_stub_factory(bin_dir, log_path) + # The login-path test must not depend on the runner having azd installed. + write_azd_stub(bin_dir, {}, "azd_1_29", tmp_path / "azd-calls.log") env = dict(os.environ) env["PATH"] = f"{bin_dir}{os.pathsep}{env['PATH']}" diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_briefing_assets.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_briefing_assets.py similarity index 100% rename from monitor/sre-agent-event-lab/scripts/tests/test_briefing_assets.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_briefing_assets.py diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_briefing_docs.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_briefing_docs.py similarity index 96% rename from monitor/sre-agent-event-lab/scripts/tests/test_briefing_docs.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_briefing_docs.py index d507a299..472774fd 100644 --- a/monitor/sre-agent-event-lab/scripts/tests/test_briefing_docs.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_briefing_docs.py @@ -1,11 +1,9 @@ import re import unicodedata from collections import Counter -from pathlib import Path +from published_layout import BRIEFING, OFFICIAL_ASSETS as SOURCE_OFFICIAL_ASSETS -REPO_ROOT = Path(__file__).parents[4] -BRIEFING = REPO_ROOT / "monitor" / "azure-sre-agent.md" OFFICIAL_SVGS = { "incident-response-flow.svg", "root-cause-analysis.svg", @@ -27,12 +25,11 @@ "operations-hub-overview-tab.png", } OFFICIAL_ASSETS = OFFICIAL_SVGS | OFFICIAL_PNGS -OFFICIAL_ASSET_PREFIX = "sre-agent-event-lab/assets/official/" +OFFICIAL_ASSET_PREFIX = "images/" # Screenshots the ordered walkthrough under `guides/` renders. They share the # `assets/official/` directory because they come from the same Learn articles, -# but they are not part of the briefing's selected set: `guides/` owns them and -# `scripts/tests/test_lab_guides.py` checks their captions and alt text. +# but they are not part of the briefing's selected set. GUIDE_SCREENSHOT_PNGS = { "portal-setup-status-bar.png", "portal-complete-setup-page.png", @@ -403,7 +400,7 @@ def test_every_official_asset_is_rendered_as_a_markdown_image(): rendered_official = { target.rsplit("/", 1)[-1] for target in markdown_targets - if "assets/official/" in target + if target.rsplit("/", 1)[-1] in OFFICIAL_ASSETS } assert rendered_official == OFFICIAL_ASSETS, sorted( @@ -545,10 +542,11 @@ def test_official_images_are_placed_with_sections_and_sources(): def official_image_alt_texts() -> dict: - pattern = re.compile(r"!\[([^\]]*)\]\(([^)]*assets/official/([^)]+))\)") + pattern = re.compile(r"!\[([^\]]*)\]\(images/([^)]+)\)") return { - match.group(3): match.group(1) + match.group(2): match.group(1) for match in pattern.finditer(BRIEFING.read_text()) + if match.group(2) in OFFICIAL_ASSETS } @@ -683,13 +681,7 @@ def test_briefing_marks_preview_capabilities(): def test_official_asset_set_has_16_selected_files(): - asset_dir = ( - REPO_ROOT - / "monitor" - / "sre-agent-event-lab" - / "assets" - / "official" - ) + asset_dir = SOURCE_OFFICIAL_ASSETS assert len(OFFICIAL_ASSETS) == 16 # The directory also stores the guide screenshots; nothing else may @@ -701,13 +693,7 @@ def test_official_asset_set_has_16_selected_files(): def test_official_sre_agent_svgs_are_stored_locally(): - asset_dir = ( - REPO_ROOT - / "monitor" - / "sre-agent-event-lab" - / "assets" - / "official" - ) + asset_dir = SOURCE_OFFICIAL_ASSETS assert {path.name for path in asset_dir.glob("*.svg")} == OFFICIAL_SVGS for name in OFFICIAL_SVGS: @@ -717,13 +703,7 @@ def test_official_sre_agent_svgs_are_stored_locally(): def test_official_sre_agent_pngs_are_stored_locally(): - asset_dir = ( - REPO_ROOT - / "monitor" - / "sre-agent-event-lab" - / "assets" - / "official" - ) + asset_dir = SOURCE_OFFICIAL_ASSETS assert {path.name for path in asset_dir.glob("*.png")} == ( OFFICIAL_PNGS | GUIDE_SCREENSHOT_PNGS @@ -737,14 +717,14 @@ def test_all_conceptual_svgs_are_referenced_in_briefing(): text = BRIEFING.read_text() for name in OFFICIAL_SVGS: - assert f"assets/official/{name}" in text, name + assert f"images/{name}" in text, name def test_all_selected_official_pngs_are_referenced_in_briefing(): text = BRIEFING.read_text() for name in OFFICIAL_PNGS: - assert f"assets/official/{name}" in text, name + assert f"images/{name}" in text, name def test_briefing_covers_private_network_connectivity(): diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_capture_agent.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_capture_agent.py similarity index 100% rename from monitor/sre-agent-event-lab/scripts/tests/test_capture_agent.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_capture_agent.py diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_capture_model.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_capture_model.py similarity index 95% rename from monitor/sre-agent-event-lab/scripts/tests/test_capture_model.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_capture_model.py index f18263bd..dba7058d 100644 --- a/monitor/sre-agent-event-lab/scripts/tests/test_capture_model.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_capture_model.py @@ -152,7 +152,7 @@ def test_redact_replaces_container_app_fqdn_preserving_surrounding_text(): redacted = model.redact( "Terminal: curl --silent --show-error --output /dev/null " "--write-out '%{http_code}\\n' --max-time 15 " - "https://ca-sre-event-lab.blueocean-da60b151.koreacentral" + "https://ca-sre-event-lab.example-env.koreacentral" ".azurecontainerapps.io/api/documents (exit 0)" ) @@ -170,9 +170,9 @@ def test_redact_replaces_multiple_container_app_fqdns_in_one_string(): value = ( "for host in \\\n" - " ca-sre-event-lab.blueocean-da60b151.koreacentral" + " ca-sre-event-lab.example-env.koreacentral" ".azurecontainerapps.io \\\n" - " ca-sre-event-lab-vnet.ambitiousmoss-e0d0dcb7.koreacentral" + " ca-sre-event-lab-vnet.example-vnet-env.koreacentral" ".azurecontainerapps.io; do\n" " curl \"https://$host/api/orders\"\n" "done" @@ -190,11 +190,11 @@ def test_redact_replaces_container_app_fqdn_recursively_in_nested_structures(): redacted = model.redact( { "summary": ( - "https://ca-sre-event-lab.blueocean-da60b151.koreacentral" + "https://ca-sre-event-lab.example-env.koreacentral" ".azurecontainerapps.io/api/orders" ), "nested": [ - "see ca-sre-event-lab-vnet.ambitiousmoss-e0d0dcb7.koreacentral" + "see ca-sre-event-lab-vnet.example-vnet-env.koreacentral" ".azurecontainerapps.io for details", ], } @@ -235,7 +235,7 @@ def test_normalize_capture_redacts_container_app_fqdn_in_message_summary(): "createdAt": "2026-08-12T08:21:00Z", "content": ( "Terminal: curl https://ca-sre-event-lab" - ".blueocean-da60b151.koreacentral.azurecontainerapps" + ".example-env.koreacentral.azurecontainerapps" ".io/api/orders (exit 0)" ), } diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_cleanup_external.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_cleanup_external.py similarity index 99% rename from monitor/sre-agent-event-lab/scripts/tests/test_cleanup_external.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_cleanup_external.py index 2993ba0d..f2c161e9 100644 --- a/monitor/sre-agent-event-lab/scripts/tests/test_cleanup_external.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_cleanup_external.py @@ -38,6 +38,7 @@ run_cleanup, staged_assignments, ) +from published_layout import README RECORDED_ASSIGNMENT_ID = assignment_id(AGENT_ASSIGNMENT_NAME) @@ -418,7 +419,7 @@ def test_cleanup_refuses_malformed_evidence(tmp_path): run = run_cleanup(tmp_path, ["--yes"], raw_evidence="{not json at all") assert run.returncode != 0 - assert "guides/01-agent-setup.md" in run.stderr + assert "docs/labs/azure-monitor/sre-agent-event-lab-setup/index.md" in run.stderr assert "agent-setup.json" in run.stderr assert "role assignment delete" not in run.az_calls @@ -578,7 +579,7 @@ def test_azure_yaml_removes_roles_predown_and_resets_image_values_postdown(): def test_readme_documents_the_teardown_hooks_and_manual_recovery(): - readme = (LAB_ROOT / "README.md").read_text() + readme = README.read_text() section = readme.split("## 정리", 1)[1].split("\n## ", 1)[0] assert "predown" in section diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_common.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_common.py similarity index 98% rename from monitor/sre-agent-event-lab/scripts/tests/test_common.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_common.py index 9b144ab8..e8a42b70 100644 --- a/monitor/sre-agent-event-lab/scripts/tests/test_common.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_common.py @@ -4,6 +4,7 @@ from pathlib import Path from azd_common_harness import run_common +from published_layout import GUIDES, README COMMON_SH = Path(__file__).parents[1] / "common.sh" @@ -143,7 +144,7 @@ def test_no_tracked_lab_file_references_the_deleted_subscription_templates(): def test_readme_documents_a_working_deployment_command(): - readme = (Path(__file__).parents[2] / "README.md").read_text() + readme = README.read_text() assert "azd up" in readme assert "az deployment sub show" not in readme @@ -158,7 +159,7 @@ def test_readme_documents_that_evidence_collection_reads_the_current_azd_environ scenario-execution section must describe that mechanism instead of the old "legacy, not yet rewritten" caveat. """ - readme = (Path(__file__).parents[2] / "README.md").read_text() + readme = README.read_text() scenario_heading = "## 시나리오 실행" assert scenario_heading in readme @@ -278,8 +279,8 @@ def test_nothing_points_an_operator_at_a_script_that_is_gone(): sources = ( list(scripts_dir.glob("*.sh")) + list(scripts_dir.glob("*.py")) - + [lab_root / "README.md"] - + sorted((lab_root / "guides").glob("*.md")) + + [README] + + sorted(GUIDES.glob("*.md")) ) for path in sources: diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_dynamic_threshold_brief.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_dynamic_threshold_brief.py similarity index 77% rename from monitor/sre-agent-event-lab/scripts/tests/test_dynamic_threshold_brief.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_dynamic_threshold_brief.py index 99dae047..05d1825b 100644 --- a/monitor/sre-agent-event-lab/scripts/tests/test_dynamic_threshold_brief.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_dynamic_threshold_brief.py @@ -5,9 +5,9 @@ from PIL import Image -REPO_ROOT = Path(__file__).parents[4] -BRIEF = REPO_ROOT / "monitor" / "azure-monitor-dynamic-thresholds-brief.md" -ASSET = REPO_ROOT / "monitor" / "assets" / "official" / "dynamic-threshold-preview-chart.png" +REPO_ROOT = Path(__file__).parents[6] +BRIEF = REPO_ROOT / "docs" / "research" / "azure-monitor" / "dynamic-thresholds-brief" / "index.md" +ASSET = BRIEF.parent / "images" / "dynamic-threshold-preview-chart.png" ARTICLE = "https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-dynamic-thresholds" RAW_MEDIA = ( "https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/media/" @@ -15,12 +15,12 @@ ) OFFICIAL_ASSETS = {"dynamic-threshold-preview-chart.png"} # To update this digest, download RAW_MEDIA and run: -# shasum -a 256 monitor/assets/official/dynamic-threshold-preview-chart.png +# shasum -a 256 docs/research/azure-monitor/dynamic-thresholds-brief/images/dynamic-threshold-preview-chart.png EXPECTED_ASSET_SHA256 = "4688901b73dff95c47d6d87c6d73f774dcb613fec38b757d1a76953df098636c" def test_brief_uses_the_local_official_chart(): - text = BRIEF.read_text() + text = BRIEF.read_text(encoding="utf-8") assert ASSET.is_file() assert {path.name for path in ASSET.parent.iterdir()} == OFFICIAL_ASSETS @@ -31,11 +31,11 @@ def test_brief_uses_the_local_official_chart(): f"got {actual_sha256}; download {RAW_MEDIA} and verify the replacement " "before updating EXPECTED_ASSET_SHA256" ) - assert "assets/official/dynamic-threshold-preview-chart.png" in text + assert "images/dynamic-threshold-preview-chart.png" in text assert RAW_MEDIA not in text linked_chart = re.search( rf"\[!\[(?P[^\]]+)\]" - rf"\(assets/official/dynamic-threshold-preview-chart\.png\)\]" + rf"\(images/dynamic-threshold-preview-chart\.png\)\]" rf"\({re.escape(ARTICLE)}\)", text, ) @@ -54,7 +54,7 @@ def test_official_chart_is_a_valid_1000_by_598_png(): def test_brief_describes_ongoing_learning_without_an_unsupported_time_bound(): - text = BRIEF.read_text() + text = BRIEF.read_text(encoding="utf-8") assert "**Ongoing learning** uses all available historical data" in text assert "**Up to 10 days**" not in text diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_lab_env.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_lab_env.py similarity index 98% rename from monitor/sre-agent-event-lab/scripts/tests/test_lab_env.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_lab_env.py index c73297f8..901c9edf 100644 --- a/monitor/sre-agent-event-lab/scripts/tests/test_lab_env.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_lab_env.py @@ -11,14 +11,10 @@ import re import subprocess import textwrap -from pathlib import Path +from published_layout import GUIDES, LAB_ROOT, README -REPO_ROOT = Path(__file__).parents[4] -LAB_ROOT = REPO_ROOT / "monitor" / "sre-agent-event-lab" LAB_ENV_SH = LAB_ROOT / "scripts" / "lab-env.sh" -GUIDES = LAB_ROOT / "guides" -README = LAB_ROOT / "README.md" SCENARIO_GUIDES = { "02-scenario-s1.md": "s1", diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_lab_state.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_lab_state.py similarity index 97% rename from monitor/sre-agent-event-lab/scripts/tests/test_lab_state.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_lab_state.py index 97c3257c..3b2eb1d7 100644 --- a/monitor/sre-agent-event-lab/scripts/tests/test_lab_state.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_lab_state.py @@ -11,13 +11,13 @@ import importlib.util import json import os -import pathlib import subprocess import sys from pathlib import Path import pytest +from published_layout import REPO_ROOT MODULE_PATH = Path(__file__).parents[1] / "lab_state.py" @@ -50,10 +50,15 @@ def test_every_scenario_has_a_guide_to_send_an_operator_to(): """ import lab_state + expected = { + "s1": "docs/labs/azure-monitor/sre-agent-scenario-http-500/index.md", + "s2": "docs/labs/azure-monitor/sre-agent-scenario-latency/index.md", + "s3": "docs/labs/azure-monitor/sre-agent-scenario-blob-permission/index.md", + } + assert lab_state.SCENARIO_GUIDES == expected assert set(lab_state.SCENARIO_GUIDES) == set(lab_state.SCENARIOS) - lab_root = pathlib.Path(__file__).parents[2] - for scenario, guide in lab_state.SCENARIO_GUIDES.items(): - assert (lab_root / guide).is_file(), (scenario, guide) + for scenario, guide in expected.items(): + assert (REPO_ROOT / guide).is_file(), (scenario, guide) def run_cli(state_path, args, stdin="", env=None): @@ -577,7 +582,7 @@ def test_the_refusal_lists_every_blocker_earliest_first(tmp_path): assert message.index("s2") < message.index("s3"), message assert "running" in message and "failed" in message assert "mark-failed s2" in message - assert "guides/04-scenario-s3.md" in message + assert "docs/labs/azure-monitor/sre-agent-scenario-blob-permission/index.md" in message def test_a_repair_is_refused_while_an_earlier_run_is_still_running(tmp_path): @@ -609,7 +614,7 @@ def test_the_refusal_names_the_blocking_scenario_its_status_and_a_remedy(tmp_pat message = str(refusal.value) assert "s2" in message assert "failed" in message - assert "guides/03-scenario-s2.md" in message, message + assert "docs/labs/azure-monitor/sre-agent-scenario-latency/index.md" in message, message def test_the_refusal_for_a_running_scenario_names_how_to_end_it(tmp_path): @@ -641,7 +646,7 @@ def test_the_ordered_remedy_never_tells_an_operator_to_restart_a_running_run(tmp message = str(refusal.value) assert "s1_recovered" in message - assert "guides/02-scenario-s1.md" not in message, message + assert "docs/labs/azure-monitor/sre-agent-scenario-http-500/index.md" not in message, message assert "mark-failed s1" in message, message @@ -782,9 +787,21 @@ def test_a_conclusion_cannot_be_recorded_against_a_run_that_never_recovered( @pytest.mark.parametrize( "run_status, expected, forbidden", ( - ("running", "mark-failed s1", "guides/02-scenario-s1.md"), - ("failed", "guides/02-scenario-s1.md", "mark-failed s1"), - (None, "guides/02-scenario-s1.md", "mark-failed s1"), + ( + "running", + "mark-failed s1", + "docs/labs/azure-monitor/sre-agent-scenario-http-500/index.md", + ), + ( + "failed", + "docs/labs/azure-monitor/sre-agent-scenario-http-500/index.md", + "mark-failed s1", + ), + ( + None, + "docs/labs/azure-monitor/sre-agent-scenario-http-500/index.md", + "mark-failed s1", + ), ), ids=("running", "failed", "none"), ) @@ -1216,7 +1233,7 @@ def test_cli_evidence_dir_without_a_run_names_the_command_to_run(tmp_path): result = run_cli(tmp_path / "state.json", ["evidence-dir", "s1"]) assert result.returncode == 1 - assert "guides/02-scenario-s1.md" in result.stderr + assert "docs/labs/azure-monitor/sre-agent-scenario-http-500/index.md" in result.stderr assert "Traceback" not in result.stderr @@ -1290,7 +1307,7 @@ def test_cli_require_run_refuses_every_scenario_while_one_run_is_unfinished(tmp_ assert refused.returncode == 1, refused.stdout assert "s3" in refused.stderr assert "failed" in refused.stderr - assert "guides/04-scenario-s3.md" in refused.stderr + assert "docs/labs/azure-monitor/sre-agent-scenario-blob-permission/index.md" in refused.stderr assert "Traceback" not in refused.stderr assert run_cli(path, ["require-run", "s3"]).returncode == 0 diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_loadgen.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_loadgen.py similarity index 100% rename from monitor/sre-agent-event-lab/scripts/tests/test_loadgen.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_loadgen.py diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_notifications.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_notifications.py similarity index 90% rename from monitor/sre-agent-event-lab/scripts/tests/test_notifications.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_notifications.py index 89df3e4a..38872639 100644 --- a/monitor/sre-agent-event-lab/scripts/tests/test_notifications.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_notifications.py @@ -3,10 +3,23 @@ import sys from email import policy from email.parser import BytesParser +from html.parser import HTMLParser from pathlib import Path MODULE_PATH = Path(__file__).parents[1] / "generate_notifications.py" +CHECKED_IN_SUMMARY = ( + Path(__file__).parents[2] / "assets" / "notifications" / "s1-incident-summary.html" +) + + +class TextCollector(HTMLParser): + def __init__(self): + super().__init__() + self.parts = [] + + def handle_data(self, data): + self.parts.append(data) def load_module(): @@ -101,6 +114,13 @@ def test_generate_notifications_creates_ticket_and_email(tmp_path): assert outputs["issue"].endswith("s1-github-issue.md") +def test_checked_in_summary_renders_the_redacted_thread_placeholder(): + parser = TextCollector() + parser.feed(CHECKED_IN_SUMMARY.read_text(encoding="utf-8")) + + assert "Agent thread " in "".join(parser.parts) + + def test_generate_notifications_redacts_sensitive_values(tmp_path): notifications = load_module() redacted = notifications.sanitize( diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_privacy.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_privacy.py similarity index 77% rename from monitor/sre-agent-event-lab/scripts/tests/test_privacy.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_privacy.py index aea4a011..16b2c3b0 100644 --- a/monitor/sre-agent-event-lab/scripts/tests/test_privacy.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_privacy.py @@ -6,17 +6,24 @@ docs) is left untouched. """ import re -from pathlib import Path +from published_layout import ( + BRIEFING, + DEPLOYMENT_PLAN, + DYNAMIC_THRESHOLDS, + LAB_ROOT, + README, + REPO_ROOT, + VALIDATION_RESULTS, +) -REPO_ROOT = Path(__file__).parents[4] SRE_LAB_TRACKED_FILES = ( - REPO_ROOT / ".azure" / "deployment-plan.md", - REPO_ROOT / "monitor" / "sre-agent-event-lab" / "README.md", - REPO_ROOT / "monitor" / "sre-agent-event-lab" / "scripts" / "common.sh", - REPO_ROOT / "monitor" / "sre-agent-event-lab" / "validation-results.md", - REPO_ROOT / "monitor" / "sre-agent-event-lab" / "dynamic-thresholds.md", + DEPLOYMENT_PLAN, + README, + LAB_ROOT / "scripts" / "common.sh", + VALIDATION_RESULTS, + DYNAMIC_THRESHOLDS, ) FORBIDDEN_STRINGS = ( @@ -35,7 +42,7 @@ def test_sre_lab_docs_do_not_contain_personal_identifier(): def test_sre_lab_captures_do_not_expose_container_app_fqdn(): captures_dir = ( - REPO_ROOT / "monitor" / "sre-agent-event-lab" / "assets" / "captures" + LAB_ROOT / "assets" / "captures" ) text_files = [ *captures_dir.glob("**/*.md"), @@ -53,11 +60,11 @@ def test_repository_does_not_ship_agent_workflow_docs(): def test_sre_lab_docs_do_not_link_removed_workflow_paths(): checked = [ - REPO_ROOT / "monitor" / "azure-sre-agent.md", - REPO_ROOT / "monitor" / "sre-agent-event-lab" / "README.md", - REPO_ROOT / ".azure" / "deployment-plan.md", - *(REPO_ROOT / "monitor" / "sre-agent-event-lab" / "assets" / "notifications").glob("*.md"), - *(REPO_ROOT / "monitor" / "sre-agent-event-lab" / "assets" / "notifications").glob("*.html"), + BRIEFING, + README, + DEPLOYMENT_PLAN, + *(LAB_ROOT / "assets" / "notifications").glob("*.md"), + *(LAB_ROOT / "assets" / "notifications").glob("*.html"), ] for path in checked: @@ -66,25 +73,23 @@ def test_sre_lab_docs_do_not_link_removed_workflow_paths(): def test_briefing_relative_links_resolve(): - briefing = REPO_ROOT / "monitor" / "azure-sre-agent.md" - targets = re.findall(r"\]\((?!https?://)([^)#]+)", briefing.read_text()) + targets = re.findall(r"\]\((?!https?://)([^)#]+)", BRIEFING.read_text()) assert targets for target in targets: - assert (briefing.parent / target).resolve().exists(), target + assert (BRIEFING.parent / target).resolve().exists(), target def test_relocated_lab_docs_drop_internal_workflow_directives(): - for name in ("validation-results.md", "dynamic-thresholds.md"): - content = ( - REPO_ROOT / "monitor" / "sre-agent-event-lab" / name - ).read_text() + for name, path in ( + ("validation-results.md", VALIDATION_RESULTS), + ("dynamic-thresholds.md", DYNAMIC_THRESHOLDS), + ): + content = path.read_text() for marker in ("승인된 설계", "보고서 반영", "검증 부록"): assert marker not in content, (name, marker) -LAB_ROOT = REPO_ROOT / "monitor" / "sre-agent-event-lab" -VALIDATION_RESULTS = LAB_ROOT / "validation-results.md" UUID_PATTERN = re.compile( r"\b[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\b" ) diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_query_evidence.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_query_evidence.py similarity index 100% rename from monitor/sre-agent-event-lab/scripts/tests/test_query_evidence.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_query_evidence.py diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_render_capture.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_render_capture.py similarity index 96% rename from monitor/sre-agent-event-lab/scripts/tests/test_render_capture.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_render_capture.py index 771d2887..6d036761 100644 --- a/monitor/sre-agent-event-lab/scripts/tests/test_render_capture.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_render_capture.py @@ -85,8 +85,8 @@ def test_render_capture_rejects_unredacted_container_app_fqdn(tmp_path): renderer = load_module() timeline = sample_timeline() timeline[2]["summary"] = ( - "Terminal: curl https://ca-sre-event-lab.blueocean-da60b151" - ".koreacentral.azurecontainerapps.io/api/orders (exit 0)" + "Terminal: curl https://ca-sre-event-lab.example-env" + ".example.koreacentral.azurecontainerapps.io/api/orders (exit 0)" ) with pytest.raises(ValueError, match="sensitive"): diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_repo_devcontainer.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_repo_devcontainer.py similarity index 96% rename from monitor/sre-agent-event-lab/scripts/tests/test_repo_devcontainer.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_repo_devcontainer.py index 27ac6097..b7f0a1b3 100644 --- a/monitor/sre-agent-event-lab/scripts/tests/test_repo_devcontainer.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_repo_devcontainer.py @@ -15,8 +15,8 @@ import re from pathlib import Path +from published_layout import README as LAB_README, REPO_ROOT -REPO_ROOT = Path(__file__).parents[4] DEVCONTAINER_DIR = REPO_ROOT / ".devcontainer" DEVCONTAINER = DEVCONTAINER_DIR / "devcontainer.json" TOOLCHAIN_DOC = DEVCONTAINER_DIR / "README.md" @@ -111,7 +111,6 @@ def test_one_document_owns_the_install_instructions(): def test_the_lab_points_at_that_document_instead_of_repeating_it(): - lab_readme = REPO_ROOT / "monitor" / "sre-agent-event-lab" / "README.md" - assert ".devcontainer/README.md" in lab_readme.read_text(), ( + assert ".devcontainer/README.md" in LAB_README.read_text(), ( "the lab must link the shared toolchain document, not restate it" ) diff --git a/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_repo_readme.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_repo_readme.py new file mode 100644 index 00000000..19950687 --- /dev/null +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_repo_readme.py @@ -0,0 +1,21 @@ +"""Contracts for discovering the public site and its runnable SRE sample.""" + +from published_layout import README as LAB_PAGE, REPO_ROOT + + +ROOT_README = REPO_ROOT / "README.md" +SAMPLE_ROOT = "samples/azure-monitor/source-material/sre-agent-event-lab" + + +def test_root_readme_routes_readers_to_the_searchable_pages_site(): + text = ROOT_README.read_text(encoding="utf-8") + + assert "https://hellices.github.io/devguidesample/" in text + assert "전체 문서 목록은 파일로 중복 관리하지 않습니다" in text + + +def test_public_lab_page_routes_runnable_commands_to_the_sample_tree(): + text = LAB_PAGE.read_text(encoding="utf-8") + + assert (REPO_ROOT / SAMPLE_ROOT / "azure.yaml").is_file() + assert SAMPLE_ROOT in text diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_score.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_score.py similarity index 99% rename from monitor/sre-agent-event-lab/scripts/tests/test_score.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_score.py index 1117315f..98c99cfd 100644 --- a/monitor/sre-agent-event-lab/scripts/tests/test_score.py +++ b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_score.py @@ -412,5 +412,5 @@ def test_cli_without_any_state_explains_what_to_run_first(tmp_path): result = run_cli(tmp_path) assert result.returncode == 1 - assert "guides/02-scenario-s1.md" in result.stderr + assert "docs/labs/azure-monitor/sre-agent-scenario-http-500/index.md" in result.stderr assert "Traceback" not in result.stderr diff --git a/monitor/sre-agent-event-lab/scripts/tests/test_setup_venv.py b/samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_setup_venv.py similarity index 100% rename from monitor/sre-agent-event-lab/scripts/tests/test_setup_venv.py rename to samples/azure-monitor/source-material/sre-agent-event-lab/scripts/tests/test_setup_venv.py diff --git a/samples/azure-openai/ptu-load-balancing/README.md b/samples/azure-openai/ptu-load-balancing/README.md new file mode 100644 index 00000000..e7bd85e3 --- /dev/null +++ b/samples/azure-openai/ptu-load-balancing/README.md @@ -0,0 +1,3 @@ +# PTU LB Test Docs + +메인 문서는 [Adaptive PTU 부하 분산 가이드](../../../docs/guides/azure-openai/adaptive-ptu-load-balancing/index.md)입니다. diff --git a/ptu_lb/ptu_architecture.excalidraw b/samples/azure-openai/ptu-load-balancing/ptu_architecture.excalidraw similarity index 100% rename from ptu_lb/ptu_architecture.excalidraw rename to samples/azure-openai/ptu-load-balancing/ptu_architecture.excalidraw diff --git a/samples/azure-openai/ptu-load-balancing/ptu_architecture.png b/samples/azure-openai/ptu-load-balancing/ptu_architecture.png new file mode 100644 index 00000000..c79ced68 Binary files /dev/null and b/samples/azure-openai/ptu-load-balancing/ptu_architecture.png differ diff --git a/ptu_lb/render_excalidraw.py b/samples/azure-openai/ptu-load-balancing/render_excalidraw.py similarity index 100% rename from ptu_lb/render_excalidraw.py rename to samples/azure-openai/ptu-load-balancing/render_excalidraw.py diff --git a/ptu_lb/runbooks/adaptive_tpm_quota_controller.py b/samples/azure-openai/ptu-load-balancing/runbooks/adaptive_tpm_quota_controller.py similarity index 99% rename from ptu_lb/runbooks/adaptive_tpm_quota_controller.py rename to samples/azure-openai/ptu-load-balancing/runbooks/adaptive_tpm_quota_controller.py index e3a1573b..2f369e03 100644 --- a/ptu_lb/runbooks/adaptive_tpm_quota_controller.py +++ b/samples/azure-openai/ptu-load-balancing/runbooks/adaptive_tpm_quota_controller.py @@ -66,7 +66,7 @@ # 설정 (운영 시 환경변수/파라미터로 외부화 권장) # --------------------------------------------------------------------------- -SUBSCRIPTION_ID = "2cf925b6-80cb-4567-abda-5ccd3010aab5" +SUBSCRIPTION_ID = "" RESOURCE_GROUP = "ptu-lb" STORAGE_ACCOUNT = "ptulb59018sa" diff --git a/ptu_lb/runbooks/optional_adaptive_tpm_quota_controller.ps1 b/samples/azure-openai/ptu-load-balancing/runbooks/optional_adaptive_tpm_quota_controller.ps1 similarity index 99% rename from ptu_lb/runbooks/optional_adaptive_tpm_quota_controller.ps1 rename to samples/azure-openai/ptu-load-balancing/runbooks/optional_adaptive_tpm_quota_controller.ps1 index 5758cdfe..928ea875 100644 --- a/ptu_lb/runbooks/optional_adaptive_tpm_quota_controller.ps1 +++ b/samples/azure-openai/ptu-load-balancing/runbooks/optional_adaptive_tpm_quota_controller.ps1 @@ -42,7 +42,7 @@ param( # --- 대상 리소스 ------------------------------------------------------------- - [string]$SubscriptionId = "2cf925b6-80cb-4567-abda-5ccd3010aab5", + [string]$SubscriptionId = "", [string]$ResourceGroup = "ptu-lb", [string]$StorageAccountName = "ptulb59018sa", diff --git a/ptu_lb/runbooks/requirements.txt b/samples/azure-openai/ptu-load-balancing/runbooks/requirements.txt similarity index 100% rename from ptu_lb/runbooks/requirements.txt rename to samples/azure-openai/ptu-load-balancing/runbooks/requirements.txt diff --git a/azureblob/spring-resumable-upload/.gitignore b/samples/azure-storage/source-material/spring-resumable-upload/.gitignore similarity index 100% rename from azureblob/spring-resumable-upload/.gitignore rename to samples/azure-storage/source-material/spring-resumable-upload/.gitignore diff --git a/azureblob/spring-resumable-upload/architecture.excalidraw b/samples/azure-storage/source-material/spring-resumable-upload/architecture.excalidraw similarity index 100% rename from azureblob/spring-resumable-upload/architecture.excalidraw rename to samples/azure-storage/source-material/spring-resumable-upload/architecture.excalidraw diff --git a/samples/azure-storage/source-material/spring-resumable-upload/architecture.png b/samples/azure-storage/source-material/spring-resumable-upload/architecture.png new file mode 100644 index 00000000..d5625069 Binary files /dev/null and b/samples/azure-storage/source-material/spring-resumable-upload/architecture.png differ diff --git a/azureblob/spring-resumable-upload/pom.xml b/samples/azure-storage/source-material/spring-resumable-upload/pom.xml similarity index 100% rename from azureblob/spring-resumable-upload/pom.xml rename to samples/azure-storage/source-material/spring-resumable-upload/pom.xml diff --git a/azureblob/spring-resumable-upload/scripts/make-test-file.sh b/samples/azure-storage/source-material/spring-resumable-upload/scripts/make-test-file.sh old mode 100755 new mode 100644 similarity index 100% rename from azureblob/spring-resumable-upload/scripts/make-test-file.sh rename to samples/azure-storage/source-material/spring-resumable-upload/scripts/make-test-file.sh diff --git a/azureblob/spring-resumable-upload/scripts/tus_client.py b/samples/azure-storage/source-material/spring-resumable-upload/scripts/tus_client.py old mode 100755 new mode 100644 similarity index 100% rename from azureblob/spring-resumable-upload/scripts/tus_client.py rename to samples/azure-storage/source-material/spring-resumable-upload/scripts/tus_client.py diff --git a/azureblob/spring-resumable-upload/scripts/verify-blob.sh b/samples/azure-storage/source-material/spring-resumable-upload/scripts/verify-blob.sh old mode 100755 new mode 100644 similarity index 100% rename from azureblob/spring-resumable-upload/scripts/verify-blob.sh rename to samples/azure-storage/source-material/spring-resumable-upload/scripts/verify-blob.sh diff --git a/azureblob/spring-resumable-upload/src/main/java/com/example/upload/UploadApplication.java b/samples/azure-storage/source-material/spring-resumable-upload/src/main/java/com/example/upload/UploadApplication.java similarity index 100% rename from azureblob/spring-resumable-upload/src/main/java/com/example/upload/UploadApplication.java rename to samples/azure-storage/source-material/spring-resumable-upload/src/main/java/com/example/upload/UploadApplication.java diff --git a/azureblob/spring-resumable-upload/src/main/java/com/example/upload/config/BlobConfig.java b/samples/azure-storage/source-material/spring-resumable-upload/src/main/java/com/example/upload/config/BlobConfig.java similarity index 100% rename from azureblob/spring-resumable-upload/src/main/java/com/example/upload/config/BlobConfig.java rename to samples/azure-storage/source-material/spring-resumable-upload/src/main/java/com/example/upload/config/BlobConfig.java diff --git a/azureblob/spring-resumable-upload/src/main/java/com/example/upload/tus/TusController.java b/samples/azure-storage/source-material/spring-resumable-upload/src/main/java/com/example/upload/tus/TusController.java similarity index 100% rename from azureblob/spring-resumable-upload/src/main/java/com/example/upload/tus/TusController.java rename to samples/azure-storage/source-material/spring-resumable-upload/src/main/java/com/example/upload/tus/TusController.java diff --git a/azureblob/spring-resumable-upload/src/main/java/com/example/upload/tus/TusFilter.java b/samples/azure-storage/source-material/spring-resumable-upload/src/main/java/com/example/upload/tus/TusFilter.java similarity index 100% rename from azureblob/spring-resumable-upload/src/main/java/com/example/upload/tus/TusFilter.java rename to samples/azure-storage/source-material/spring-resumable-upload/src/main/java/com/example/upload/tus/TusFilter.java diff --git a/azureblob/spring-resumable-upload/src/main/java/com/example/upload/tus/TusUploadService.java b/samples/azure-storage/source-material/spring-resumable-upload/src/main/java/com/example/upload/tus/TusUploadService.java similarity index 100% rename from azureblob/spring-resumable-upload/src/main/java/com/example/upload/tus/TusUploadService.java rename to samples/azure-storage/source-material/spring-resumable-upload/src/main/java/com/example/upload/tus/TusUploadService.java diff --git a/azureblob/spring-resumable-upload/src/main/resources/application.yml b/samples/azure-storage/source-material/spring-resumable-upload/src/main/resources/application.yml similarity index 100% rename from azureblob/spring-resumable-upload/src/main/resources/application.yml rename to samples/azure-storage/source-material/spring-resumable-upload/src/main/resources/application.yml diff --git a/infra/main.bicep b/samples/infrastructure/bicep/main.bicep similarity index 100% rename from infra/main.bicep rename to samples/infrastructure/bicep/main.bicep diff --git a/infra/main.bicepparam b/samples/infrastructure/bicep/main.bicepparam similarity index 100% rename from infra/main.bicepparam rename to samples/infrastructure/bicep/main.bicepparam diff --git a/infra/main.json b/samples/infrastructure/bicep/main.json similarity index 100% rename from infra/main.json rename to samples/infrastructure/bicep/main.json diff --git a/infra/modules/monitoring.bicep b/samples/infrastructure/bicep/modules/monitoring.bicep similarity index 100% rename from infra/modules/monitoring.bicep rename to samples/infrastructure/bicep/modules/monitoring.bicep diff --git a/memory/README.md b/samples/microsoft-foundry/memory-artifacts/README.md similarity index 100% rename from memory/README.md rename to samples/microsoft-foundry/memory-artifacts/README.md diff --git a/samples/microsoft-foundry/memory-artifacts/agent-memory/images/01-memory-taxonomy.svg b/samples/microsoft-foundry/memory-artifacts/agent-memory/images/01-memory-taxonomy.svg new file mode 100644 index 00000000..e14c46bc --- /dev/null +++ b/samples/microsoft-foundry/memory-artifacts/agent-memory/images/01-memory-taxonomy.svg @@ -0,0 +1,94 @@ + + + + Agent Memory 분류 체계 — 6개 패밀리 / 30개 기법 + 각 패밀리는 서로 다른 문제를 해결한다. 하나의 기법이 다른 기법을 대체하지 않는다. + + + + + + + ① 단기 기억 (Short-term) + 01–05 + 한 대화(세션) 안에서 컨텍스트 윈도우를 넘기지 + 않으면서 최근 맥락을 유지한다. + 01 Conversation Buffer — 전체 원문 보관 + 02 Sliding Window — 최근 k턴만 유지 + 03 Summary — 과거를 요약으로 치환 + 04 Summary Buffer — 요약 + 최근 원문 + 05 Token Buffer — 토큰 예산 강제 트리밍 + + + + + + ② 장기 기억 (Long-term) + 06–11 + 세션·사용자·시간을 넘어 지식을 보존한다. + 개인화의 실질적 토대가 되는 계층. + 06 Vector Store — 의미 기반 유사도 검색 + 07 Entity — 인물·상품·프로젝트 단위 추적 + 08 Knowledge Graph — 관계 그래프 순회 + 09 Episodic — "언제 무슨 일이" 사건 기록 + 10 Semantic · 11 Procedural — 사실 / 절차 + + + + + + ③ 인지 아키텍처 (Cognitive) + 12–19 + 인간 기억을 모사한 운영 계층. 저장이 아니라 + "기억을 관리하는 방법"을 다룬다. + 12 Working · 13 Hierarchical — 주의/티어 + 14 Consolidation — 병합·중복제거·충돌해소 + 15 Compaction — 압축으로 토큰 절감 + 16 Self-Reflection · 17 Routing + 18 Temporal · 19 Forgetting & Decay + + + + + + + ④ 검색 · 멀티에이전트 + 20–23 + "무엇을 언제 꺼낼 것인가". 저장보다 검색이 + 체감 품질을 좌우한다. + 20 Retrieval Patterns — 하이브리드+리랭크 + 21 Cross-Session — 유저별 상태 복원 + 22 Multi-Agent Shared — 공유 저장소·권한 + 23 Memory as Tools — 저장/검색/삭제를 + 에이전트 툴로 노출 + + + + + + ⑤ 프레임워크 · 플랫폼 + 24–27 + 직접 구현 대신 도입 가능한 완제품 계층. + 속도를 얻고 통제권을 내준다. + 24 Graphiti — 시간 인식 지식 그래프 + 25 Mem0 — 관리형 추출·저장·검색 레이어 + 26 Letta(MemGPT) — 자기수정 3계층 메모리 + 27 Zep — 대화 분류 + 시간 그래프 + (MS 자료 추가: Cognee — 듀얼 스토어 시맨틱 메모리) + + + + + + ⑥ 평가 · 프로덕션 + 28–30 + "동작한다"를 숫자로 바꾸고 규모에서 버티게 + 만드는 운영 계층. B2C에서는 필수. + 28 Evaluation — Precision/Recall/MRR, + staleness · 모순 탐지 · LLM-as-Judge + 29 Benchmarks — LoCoMo / LongMemEval + 30 Production — 캐싱·TTL·샤딩·GDPR· + 관측성·비용 관리 + + 단기 → 장기 → 인지 → 검색 → (프레임워크) → 프로덕션 순으로 쌓아 올린다. 하위 패밀리 없이 상위만 도입하면 대부분 실패한다. + diff --git a/samples/microsoft-foundry/memory-artifacts/agent-memory/images/02-architecture-patterns.svg b/samples/microsoft-foundry/memory-artifacts/agent-memory/images/02-architecture-patterns.svg new file mode 100644 index 00000000..6a941af4 --- /dev/null +++ b/samples/microsoft-foundry/memory-artifacts/agent-memory/images/02-architecture-patterns.svg @@ -0,0 +1,159 @@ + + + + + + + + + + + + 메모리 시스템 아키텍처 5가지 패턴 + 왼쪽에서 오른쪽으로 갈수록 복잡도가 커진다. 패턴은 중첩된다 — Full Cognitive 안에 Graph-Augmented가, 그 안에 Dual-Store가 들어 있다. + 권장: Dual-Store에서 시작해 필요할 때 확장한다. + + + + + + 1. Single-Store + 복잡도 낮음 · 저장소 1개 + + + Vector Store + 단일 컬렉션 + 모든 기억을 한 곳에 임베딩 + + 검색 + 시맨틱 top-k 만 + 강점 + 50줄이면 구현. 라우팅 불필요 + 약점 + 커질수록 검색 품질 급락 + 시간 개념·압축 없음 + 적합 + 프로토타입 · 데모 + + + + + + 2. Dual-Store ★ + 복잡도 중간 · 버퍼 + 영구저장 + + + Short-Term Buffer + 최근 k턴 / 러닝 요약 + + flush + + Long-Term Store + 추출 사실 / 엔티티 / 요약 + 주기적으로 버퍼를 증류해 이관 + + 검색 + 버퍼 스캔 + 시맨틱 검색 + 강점 + 최근맥락·누적지식 명확히 분리 + 약점 + flush 설계(시점·추출·중복)가 + 전적으로 개발자 책임 + 적합 + 개인비서 · 고객지원 · B2C 시작점 + + + + + + 3. Tiered + 복잡도 높음 · 3계층 캐시 모델 + + + HOT · Azure Managed Redis + <1ms · 현재 세션 · 고정 사실 + + WARM · pgvector / AI Search + 5~50ms · 최근 세션 · 활성 엔티티 + + COLD · Azure Blob (Cool/Archive) + 100ms+ · 전체 이력 + 접근 빈도로 승격(promote) / 강등(demote) + + 검색 + HOT → WARM → COLD 폴스루 + 강점 + 토큰·지연·비용을 동시에 통제 + 약점 + 승격/강등 정책 튜닝 난이도 최상 + 중요 기억이 COLD에 갇힐 위험 + 적합 + 대규모 · 엄격한 지연 요구 + + + + + + 4. Graph-Augmented + 복잡도 중상 · 벡터 + 지식그래프 + + + Vector + 의미 유사도 + + Graph + 관계 순회 + + + + Result Merger + 중복제거 · 재랭킹 + 턴마다 엔티티 추출기가 양쪽을 갱신 + + 검색 + 시맨틱 + 그래프 순회 병렬 + 강점 + 멀티홉 추론. 시간 정보를 엣지에 + 약점 + 엔티티 추출 오류·환각. 스키마 + 설계 필요. 병합 지연 추가 + 적합 + 엔티티 풍부한 도메인 (커머스 카탈로그) + + + + + + 5. Full Cognitive + 복잡도 최상 · 전문화된 다중 저장소 + + + Working Memory (토큰 예산) + + Memory Router + + Episodic + 무슨 일이 + + Semantic + 무엇이 참 + + Procedural + 어떻게 + + Background Jobs + Consolidation · Compaction · Decay · Reflection + 라우터 정확도가 시스템 전체 품질을 결정 + + 검색 + 라우터가 지목한 저장소만 조회 + 강점 + 저장소별 독립 확장. 최고 표현력 + 약점 + 구현·운영 부담 최대. 오버엔지니어링 + 위험이 가장 큰 패턴 + 적합 + 수개월 축적되는 장수 에이전트 + + 실사례 매핑 — Single: LangChain VectorStoreRetrieverMemory · Dual: Mem0 · Tiered: MemGPT 계층, Anthropic Claude Code 계층 · Graph: Zep, Graphiti, GraphRAG · Cognitive: Letta/MemGPT, SOAR + diff --git a/samples/microsoft-foundry/memory-artifacts/agent-memory/images/03-memory-pipeline.svg b/samples/microsoft-foundry/memory-artifacts/agent-memory/images/03-memory-pipeline.svg new file mode 100644 index 00000000..68a51a5d --- /dev/null +++ b/samples/microsoft-foundry/memory-artifacts/agent-memory/images/03-memory-pipeline.svg @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + 메모리 파이프라인 — 쓰기 경로와 읽기 경로는 분리한다 + 실시간 응답을 지키는 핵심 원칙: LLM을 쓰는 무거운 작업(추출·통합)은 전부 비동기 쓰기 경로로 밀어낸다. + + + + ① 쓰기 경로 (비동기 · 응답 경로를 막지 않는다) + 턴 종료 후 / 배치 / 유휴 시간에 실행 — 사용자 대기시간에 포함되지 않음 + + + + 대화 턴 + user + assistant + 메시지 원문 + + + PII 스캔 · 마스킹 + 저장 전에 반드시 + 정규식만으론 부족 + + + 사실 추출 (LLM) + 선호 · 제약 · 의도 + 저비용 모델로 1차 게이트 + + + 중복 · 모순 판정 + 코사인 > 0.85 → 중복 + 0.50~0.85 → 모순 검사 + + + Memory Router + 유형 분류 후 분배 + 오분류 시 조용히 유실 + + + 저장소 기록 + Episodic / Semantic + Procedural / Entity Graph + + + 유지보수 잡 + Consolidation · Compaction · Decay + 주기/임계값 트리거 + + + + + + + + + + 유지보수 잡의 3종 세트 + • Consolidation(14) — 유사 기억 클러스터링 → 병합 → 모순 해소(최신 우선) → 중요도 재계산. 클러스터당 LLM 호출이 들어가므로 비용이 저장소 크기에 비례한다. + • Compaction(15) — 다단계 요약으로 토큰/용량 절감. 정확도가 아니라 "크기"가 목적일 때 쓴다. + • Forgetting & Decay(19) — S(t) = S₀·e^(−λt). 접근 시 강화(reinforcement), 임계값 미만은 soft/hard delete. 반드시 pin 예외 목록을 함께 설계한다. + + + + ② 읽기 경로 (동기 · 사용자 지연 예산 안에서 끝나야 한다) + 단계를 더할수록 정확도는 오르고 지연도 오른다 — 실시간 채팅에서는 뒷단계를 과감히 생략한다 + + + + 사용자 쿼리 + 현재 발화 + + 세션 컨텍스트 + + + HyDE 확장 + 가상 답변 생성 후 + 그걸로 검색 (선택) + + LLM 호출 1회 + + + 벡터 검색 + + BM25 키워드 + 병렬 실행 · 20~50ms + + + RRF 융합 + 두 랭킹을 하나로 + Reciprocal Rank Fusion + 거의 무료 + + + Cross-Encoder + (쿼리,문서) 쌍 재점수 + 정밀도 최대 향상 + +50~200ms + + + MMR 다양성 + 유사 결과 5개 반복 + 방지 (lambda 튜닝) + 거의 무료 + + + 시간 가중 + recency decay 곱 + stable은 decay 면제 + 거의 무료 + + + 컨텍스트 조립 + 섹션별 토큰 예산 + 초과분 evict + 거의 무료 + + + + + + + + + + + + 지연 예산 판단 기준 + • 벡터 단독 20~50ms → 전체 파이프라인 200~500ms. 하이브리드 검색만으로도 리콜이 통상 15~30% 개선된다. + • 항목 50개 미만 저장소에서는 풀 파이프라인 이득이 거의 없다. 파라미터(RRF k, MMR lambda, 후보 수)가 늘수록 오설정 위험이 커진다. + + 핵심 설계 결론 + 쓰기 경로에서 품질을 만들고(추출 정확도·중복 제거·모순 해소), 읽기 경로에서는 저장된 품질을 싸게 꺼내 쓴다. 읽기 경로에 LLM 호출을 넣는 순간 실시간성이 무너진다. + Mem0가 채택한 2단계(extraction → update) 구조가 정확히 이 원칙이며, "knowledge agent"(대화를 관찰하는 별도 에이전트) 패턴도 같은 분리를 말한다. + diff --git a/samples/microsoft-foundry/memory-artifacts/agent-memory/images/04-production-tiers.svg b/samples/microsoft-foundry/memory-artifacts/agent-memory/images/04-production-tiers.svg new file mode 100644 index 00000000..6313c796 --- /dev/null +++ b/samples/microsoft-foundry/memory-artifacts/agent-memory/images/04-production-tiers.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + 프로덕션 메모리 아키텍처 — 계층 저장 + 가드레일 + 메모리 기법이 아니라 "그 위에 얹는 운영 계층"이다. B2C 규모에서는 선택이 아니라 전제 조건. + + + + Ingestion — 저장 전 처리 + + + 신규 메시지 + 대화 턴 / 행동 이벤트 + + + PII 스캐너 · 레닥터 + 이메일·전화·주민번호 등 + 정규식은 이름·주소를 놓친다 → 전용 서비스 필요 + + + Fact Extractor (LLM) + 구조화 사실 + 엔티티 관계 + 저비용 모델로 가치 판단 후 승격 + + + Embedding Generator + 배치 처리 · 임베딩 캐시로 + 중복 생성 비용 제거 + + + Cost Tracker (게이트) + 일/월 상한 대비 실지출 추적 + 초과 시 추출 강도 하향 + + + + + + + + + Tiered Storage — 접근 빈도로 승격/강등 + + + HOT + Azure Managed Redis + TTL: 수분~수시간 + 지연 < 1ms + + + WARM + Azure DB for PostgreSQL (pgvector) / AI Search + TTL: 수일~수개월 + 지연 5~50ms + + + COLD + Azure Blob Storage (Cool/Archive) + TTL: 무기한 + 지연 100ms+ + + + 강등 + + 승격 + + 강등 + + 승격 + + 승격(promote): 접근 횟수 임계 초과 · 에이전트가 명시적으로 pin · 최근 N턴 내 참조 + 강등(demote): M턴 동안 미접근 · relevance score 하락 · 세션 종료 · decay score < 컷오프 + + + + Relationship Layer + + Entity Graph + 노드 = 유저 · 상품 · 카테고리 · 브랜드 + 엣지 = 조회 · 구매 · 반품 · 선호 · 대체 + 멀티홉 질의: "이 사람이 좋아한 브랜드의 신상" + 엣지에 타임스탬프를 실으면 시간 추론이 자연스러워진다 + (Graphiti / Zep이 채택한 방식) + + + + Guardrails — 모든 티어를 가로지른다 + + + Budget Enforcer + 유저당 최대 토큰 / 최대 기억 개수 상한 + 초과 시 중요도 가중 eviction + 중요도 점수가 틀리면 가치 있는 기억이 날아간다 + + + Privacy Manager + GDPR 삭제권 — 원문·추출사실·임베딩· + 그래프 노드/엣지·캐시까지 전부 삭제 + 저장소가 여러 개일수록 누락 위험 ↑ · 감사 로그 필수 + + + Observability + 검색 지연 · 캐시 적중률 · 저장소 크기 · + 에러율 · 추출 품질을 상시 계측 + OpenTelemetry / 구조화 로깅 / 분산 추적 + + 이 계층을 언제 넣나 + 활성 유저 100명 초과 · 민감정보 취급 · 가용성 보장 필요 — 이 중 하나라도 해당하면 필요하다. 반대로 그 전에 넣으면 오버엔지니어링이다. + 주의: 기법 30은 단일 노드 패턴까지만 다룬다. 수평 샤딩·분산 캐시·리전 간 복제는 별도 인프라 설계가 필요하다. + diff --git a/samples/microsoft-foundry/memory-artifacts/agent-memory/images/05-commerce-blueprint.svg b/samples/microsoft-foundry/memory-artifacts/agent-memory/images/05-commerce-blueprint.svg new file mode 100644 index 00000000..d970895a --- /dev/null +++ b/samples/microsoft-foundry/memory-artifacts/agent-memory/images/05-commerce-blueprint.svg @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + B2C 커머스 에이전트 — 실시간 추천 · 개인 맞춤 구매 유도를 위한 메모리 설계 + 자체 에이전트 프레임워크 위에 얹는 구조. 핵심은 읽기(실시간)와 쓰기(비동기)의 완전한 분리, 그리고 decay 속도가 다른 기억을 섞지 않는 것. + + + 신호 소스 — 대화만이 메모리 입력이 아니다 + + 대화 턴 + 발화 · 질문 · 불만 + + + 탐색 행동 + 조회 · 검색어 · 체류시간 + + + 장바구니 · 위시리스트 + 담기 · 빼기 · 이탈 + + + 거래 이력 + 구매 · 반품 · 교환 사유 + + + CS · 리뷰 + 불만 · 만족 · 사이즈 후기 + + + + ① 실시간 경로 — 사용자를 기다리게 하는 유일한 구간 + LLM 호출은 마지막 1회만. 앞단은 전부 조회/스코어링으로 끝낸다. + + + 1. 컨텍스트 조립 + HOT 캐시에서 유저 + 고정 프로필 pin 로드 + + 현재 세션 의도 + + 목표 < 5ms + + + 2. 후보 생성 + 벡터 + BM25 병렬 + → RRF 융합 + 관련 기억 + 상품 top-K + + 20~50ms + + + 3. 랭킹 · 하드 필터 + temporal decay 가중 곱 + 제약 위반 상품 제거 + + MMR 다양성 + + < 10ms + + + 4. 메시지 생성 + LLM 호출 1회 + 근거 기억 ID를 함께 + 반환 (설명가능성) + + 지연 대부분 차지 + + + + + + + 조회 + + + 실시간 경로에서 금지할 것 + Cross-Encoder 리랭크(+50~200ms) · HyDE(LLM 1회 추가) · Memory Router LLM 분류(200~500ms) · 사실 추출 · Consolidation + → 정확도가 정말 필요하면 "추천 카드 프리페치"처럼 사용자 대기 밖에서 미리 계산해 둔다. + + + + 메모리 저장소 — decay 속도로 분리한다 + + + A. 고정 프로필 (Semantic) — No decay · Pinned + 사이즈 · 알레르기/성분 회피 · 브랜드 선호 · 예산대 · 배송지 유형 + HOT + + + B. 세션 의도 (Working) — half-life 수십 분 + 지금 찾는 카테고리 · 비교 중인 상품 · 방금 거절한 추천 · 예산 발언 + HOT + + + C. 행동 에피소드 (Episodic) — half-life 수주 + 조회/담기/구매/반품 + 타임스탬프 · "지난달에 뭘 샀지" 질의 대응 + WARM + + + D. 상품 · 관계 그래프 (Entity Graph) + 유저 ↔ 상품 ↔ 브랜드 ↔ 카테고리 ↔ 대체재 · 멀티홉 추천 근거 + + + + ② 비동기 학습 경로 — 여기서 메모리 품질이 결정된다 + 응답 반환 직후 큐에 적재. 사용자 지연에 전혀 포함되지 않으므로 비싼 LLM 작업을 여기 몰아넣는다. + + + 1. 이벤트 큐 + 대화 턴 + 행동 로그를 + 동일 스키마로 정규화 + 유실 허용 X · 재처리 가능하게 + + + 2. 동의 · PII 게이트 + 마케팅 활용 동의 여부 확인 + 민감정보 마스킹 + 동의 없으면 여기서 폐기 + + + 3. 사실 추출 + 저비용 모델로 "저장 가치" + 1차 판정 → 통과분만 정밀 추출 + 지연·비용 최적화의 정석 + + + 4. 중복 · 모순 해소 + 라우팅 + "235 신는다" vs "240으로 바꿨다" + 최신 우선 + 구버전 아카이브 + A/B/C/D 저장소로 분배 + + + 5. 유지보수 배치 + Consolidation · Decay · 승격/강등 + 유저당 예산 초과분 eviction + 야간/유휴 시간대 실행 + + + + + + + + 저장 (비동기 쓰기) + + + B2C 전용 가드레일 — 기술 문제가 아니라 신뢰 문제다 + + + 과잉 개인화 방지 + 기억을 드러내는 강도를 단계화한다. + "지난번 그 브랜드" O / "화요일 밤 11시에 + 보시던 상품" X + + + 오기억 방지 · 확인 루프 + 추론된 사실은 confidence를 낮게 두고, + 구매 유도에 쓰기 전 가볍게 확인한다. + 틀린 기억 1회가 신뢰를 크게 깎는다. + + + 사용자 통제권 + "이거 기억해 / 잊어" 명령 지원, + 기억 목록 조회·삭제 UI, 삭제 시 + 임베딩·그래프·캐시까지 전파. + + + 메시지 빈도 · 피로도 + "거절한 추천"도 기억해 재노출을 막는다. + 푸시/유도 메시지는 유저별 쿨다운을 + 메모리에 기록해 강제한다. + diff --git a/memory/hybrid-architecture.excalidraw b/samples/microsoft-foundry/memory-artifacts/hybrid-architecture.excalidraw similarity index 100% rename from memory/hybrid-architecture.excalidraw rename to samples/microsoft-foundry/memory-artifacts/hybrid-architecture.excalidraw diff --git a/samples/microsoft-foundry/memory-artifacts/hybrid-architecture.png b/samples/microsoft-foundry/memory-artifacts/hybrid-architecture.png new file mode 100644 index 00000000..6f1a577e Binary files /dev/null and b/samples/microsoft-foundry/memory-artifacts/hybrid-architecture.png differ diff --git a/scripts/docs/content.py b/scripts/docs/content.py new file mode 100644 index 00000000..fb31644d --- /dev/null +++ b/scripts/docs/content.py @@ -0,0 +1,322 @@ +"""Shared content model for public documentation.""" + +from __future__ import annotations + +from dataclasses import dataclass, replace +from datetime import date, datetime +from pathlib import Path, PurePosixPath +import re +from typing import Any, Iterable, Mapping +from urllib.parse import urlparse + +import yaml + + +KEBAB_CASE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") +COMMON_REQUIRED_FIELDS = ( + "title", + "description", + "document_type", + "services", + "technologies", + "tags", + "status", + "verification_status", + "sources_checked_at", + "official_sources", +) + + +class DocumentFormatError(ValueError): + """Raised when a Markdown file cannot be parsed as a public document.""" + + +@dataclass(frozen=True) +class ValidationResult: + document_count: int + errors: list[str] + + +@dataclass(frozen=True) +class Document: + path: Path + relative_path: PurePosixPath + metadata: dict[str, Any] + body: str + + def with_metadata(self, metadata: Mapping[str, Any]) -> "Document": + return replace(self, metadata=dict(metadata)) + + +def load_taxonomy(path: Path | str) -> dict[str, Any]: + """Load the declarative documentation taxonomy.""" + source = Path(path) + data = yaml.safe_load(source.read_text(encoding="utf-8")) + if not isinstance(data, dict): + raise DocumentFormatError(f"{source}: taxonomy must be a YAML mapping") + return data + + +def _infer_docs_dir(path: Path) -> Path: + for parent in (path.parent, *path.parents): + if parent.name == "docs": + return parent + raise DocumentFormatError(f"{path}: cannot infer docs directory") + + +def load_document(path: Path | str, docs_dir: Path | str | None = None) -> Document: + """Parse YAML front matter and Markdown body from a public page.""" + source = Path(path) + text = source.read_text(encoding="utf-8-sig") + lines = text.splitlines() + if not lines or lines[0].strip() != "---": + raise DocumentFormatError(f"{source}: missing YAML front matter") + + try: + end = next(index for index, line in enumerate(lines[1:], start=1) if line.strip() == "---") + except StopIteration as error: + raise DocumentFormatError(f"{source}: unterminated YAML front matter") from error + + raw_metadata = "\n".join(lines[1:end]) + try: + metadata = yaml.safe_load(raw_metadata) + except yaml.YAMLError as error: + raise DocumentFormatError(f"{source}: invalid YAML front matter: {error}") from error + if not isinstance(metadata, dict): + raise DocumentFormatError(f"{source}: YAML front matter must be a mapping") + + base = Path(docs_dir) if docs_dir is not None else _infer_docs_dir(source) + try: + relative = source.resolve().relative_to(base.resolve()) + except ValueError as error: + raise DocumentFormatError(f"{source}: document is outside {base}") from error + + body = "\n".join(lines[end + 1 :]).lstrip("\n") + return Document(source, PurePosixPath(relative.as_posix()), metadata, body) + + +def iter_public_documents( + docs_dir: Path | str, taxonomy: Mapping[str, Any] +) -> Iterable[Document]: + """Yield page-bundle documents from configured public collections.""" + root = Path(docs_dir) + collection_paths = sorted( + { + config["path"] + for config in taxonomy.get("collections", {}).values() + if isinstance(config, Mapping) and isinstance(config.get("path"), str) + } + ) + for collection_path in collection_paths: + collection_root = root / collection_path + if not collection_root.is_dir(): + continue + for path in sorted(collection_root.rglob("index.md")): + relative = path.relative_to(root) + if len(relative.parts) == 4: + yield load_document(path, docs_dir=root) + + +def _is_date(value: Any) -> bool: + return isinstance(value, date) and not isinstance(value, datetime) + + +def _validate_string_list( + metadata: Mapping[str, Any], + field: str, + vocabulary: Mapping[str, Any], + noun: str, + errors: list[str], +) -> None: + values = metadata.get(field) + if not isinstance(values, list) or not values or not all(isinstance(item, str) for item in values): + errors.append(f"{field} must be a non-empty list of strings") + return + for value in values: + if not KEBAB_CASE.fullmatch(value): + errors.append(f"{noun} must be kebab-case: {value}") + if value not in vocabulary: + errors.append(f"unknown {noun}: {value}") + + +def _append_source_errors( + metadata: Mapping[str, Any], taxonomy: Mapping[str, Any], errors: list[str] +) -> None: + sources = metadata.get("official_sources") + if not isinstance(sources, list) or not sources: + errors.append("official_sources must be a non-empty list") + return + + allowed_hosts = set(taxonomy.get("official_source_hosts", [])) + required_host = taxonomy.get("required_source_host") + seen_hosts: set[str] = set() + for index, source in enumerate(sources): + if not isinstance(source, Mapping): + errors.append(f"official_sources[{index}] must be a mapping") + continue + title = source.get("title") + url = source.get("url") + if not isinstance(title, str) or not title.strip(): + errors.append(f"official_sources[{index}].title must be a non-empty string") + if not isinstance(url, str) or not url.strip(): + errors.append(f"official_sources[{index}].url must be a non-empty string") + continue + try: + parsed = urlparse(url) + except ValueError as error: + errors.append(f"official_sources[{index}].url is invalid: {error}") + continue + host = (parsed.hostname or "").lower() + if parsed.scheme != "https": + errors.append(f"official source URL must use HTTPS: {url}") + if not host: + errors.append(f"official source URL must have a host: {url}") + continue + seen_hosts.add(host) + if allowed_hosts and host not in allowed_hosts: + errors.append(f"official source host is not allowed: {host}") + + if required_host and required_host not in seen_hosts: + errors.append(f"at least one official source must use {required_host}") + + +def validate_source_metadata( + metadata: Mapping[str, Any], + taxonomy: Mapping[str, Any], + today: date | None = None, +) -> list[str]: + """Validate official-source evidence without validating other page fields.""" + current_date = today or date.today() + errors: list[str] = [] + if "sources_checked_at" not in metadata: + errors.append("missing required field: sources_checked_at") + if "official_sources" not in metadata: + errors.append("missing required field: official_sources") + + checked_at = metadata.get("sources_checked_at") + if "sources_checked_at" in metadata: + if not _is_date(checked_at): + errors.append("sources_checked_at must be a date") + elif checked_at > current_date: + errors.append("sources_checked_at cannot be in the future") + + _append_source_errors(metadata, taxonomy, errors) + return errors + + +def validate_document( + document: Document, + taxonomy: Mapping[str, Any], + today: date | None = None, +) -> list[str]: + """Return every content-contract violation for one public document.""" + current_date = today or date.today() + metadata = document.metadata + errors: list[str] = [] + + for field in COMMON_REQUIRED_FIELDS: + if field not in metadata: + errors.append(f"missing required field: {field}") + + for field in ("title", "description", "document_type", "status", "verification_status"): + if field in metadata and ( + not isinstance(metadata[field], str) or not metadata[field].strip() + ): + errors.append(f"{field} must be a non-empty string") + + collections = taxonomy.get("collections", {}) + document_type = metadata.get("document_type") + collection = ( + collections.get(document_type) + if isinstance(collections, Mapping) and isinstance(document_type, str) + else None + ) + parts = document.relative_path.parts + if len(parts) != 4 or parts[-1] != "index.md" or not KEBAB_CASE.fullmatch(parts[2]): + errors.append("public documents must use ///index.md") + + if not isinstance(collection, Mapping): + if isinstance(document_type, str): + errors.append(f"unknown document_type: {document_type}") + else: + expected_folder = collection.get("path") + if parts and parts[0] != expected_folder: + errors.append( + f"folder '{parts[0]}' does not match {document_type} collection '{expected_folder}'" + ) + status = metadata.get("status") + if isinstance(status, str) and status not in collection.get("statuses", []): + errors.append(f"invalid {document_type} status: {status}") + for field in collection.get("required_fields", []): + if field not in metadata: + errors.append(f"missing required field for {document_type}: {field}") + elif field != "last_verified": + value = metadata[field] + if ( + value is None + or value == [] + or (isinstance(value, str) and not value.strip()) + ): + errors.append( + f"required field for {document_type} must not be empty: {field}" + ) + + services = taxonomy.get("services", {}) + technologies = taxonomy.get("technologies", {}) + tags = taxonomy.get("tags", {}) + _validate_string_list(metadata, "services", services, "service", errors) + _validate_string_list(metadata, "technologies", technologies, "technology", errors) + _validate_string_list(metadata, "tags", tags, "tag", errors) + + if len(parts) >= 2 and isinstance(metadata.get("services"), list): + service_folder = parts[1] + if not KEBAB_CASE.fullmatch(service_folder): + errors.append(f"service folder must be kebab-case: {service_folder}") + if service_folder not in metadata["services"]: + errors.append(f"service folder must appear in services: {service_folder}") + + verification_status = metadata.get("verification_status") + if isinstance(verification_status, str) and verification_status not in taxonomy.get( + "verification_statuses", [] + ): + errors.append(f"invalid verification_status: {verification_status}") + + errors.extend(validate_source_metadata(metadata, taxonomy, today=current_date)) + + last_verified = metadata.get("last_verified") + if last_verified is not None and not _is_date(last_verified): + errors.append("last_verified must be a date or null") + if verification_status == "verified" and document_type in ("guide", "lab"): + if not _is_date(last_verified): + errors.append("last_verified must be a date when verification_status is verified") + if _is_date(last_verified) and last_verified > current_date: + errors.append("last_verified cannot be in the future") + + review_cycle = metadata.get("review_cycle_days") + if review_cycle is not None and ( + isinstance(review_cycle, bool) + or not isinstance(review_cycle, int) + or review_cycle <= 0 + ): + errors.append("review_cycle_days must be a positive integer") + + for date_field in ("occurred_at", "resolved_at", "published_at"): + value = metadata.get(date_field) + if date_field in metadata and not _is_date(value): + errors.append(f"{date_field} must be a date") + + for list_field in ("applies_to", "related_cases", "related_guides"): + value = metadata.get(list_field) + if value is not None and ( + not isinstance(value, list) or not all(isinstance(item, str) for item in value) + ): + errors.append(f"{list_field} must be a list of strings") + + cleanup_required = metadata.get("cleanup_required") + if cleanup_required is not None and not isinstance(cleanup_required, bool): + errors.append("cleanup_required must be a boolean") + + if "featured" in metadata and not isinstance(metadata["featured"], bool): + errors.append("featured must be a boolean") + + return errors diff --git a/scripts/docs/generate_indexes.py b/scripts/docs/generate_indexes.py new file mode 100644 index 00000000..b90741c9 --- /dev/null +++ b/scripts/docs/generate_indexes.py @@ -0,0 +1,562 @@ +"""Generate collection, service, and home landing pages from public document metadata.""" + +from __future__ import annotations + +from collections import defaultdict +import html +from pathlib import Path, PurePosixPath +import posixpath +import sys +from typing import Any, Iterable, Mapping, Sequence + +import yaml + +REPO_ROOT = Path(__file__).resolve().parents[2] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from scripts.docs.content import Document, iter_public_documents, load_taxonomy + + +# Static English eyebrow labels for each document type / overview page, matching +# the visual style already used on the hand-authored home template. +_EYEBROW_LABELS: dict[str, str] = { + "case": "CASE FILES", + "guide": "GUIDES", + "lab": "LABS", + "research": "RESEARCH", +} +_SERVICES_EYEBROW = "SERVICES" + +# Raw metadata values are never surfaced verbatim when a friendlier label exists. +_STATUS_LABELS: dict[str, str] = { + "resolved": "해결 기록", + "unresolved": "조사 중", + "historical": "과거 사례", +} + +# document_type -> (metadata field holding the date, label for that date). +_DATE_FIELD_BY_TYPE: dict[str, tuple[str, str]] = { + "case": ("occurred_at", "관측일"), + "guide": ("last_verified", "최종 확인일"), + "lab": ("last_verified", "최종 확인일"), + "research": ("published_at", "게시일"), +} + +_HOME_SLOTS = ("", "", "") + + +def _escape(value: Any) -> str: + """Escape text for use inside raw (non-markdown) HTML.""" + return html.escape(str(value), quote=True) + + +def _md_label(value: Any) -> str: + """Preserve plain-text metadata inside Markdown labels and headings.""" + text = html.escape(str(value), quote=False) + for marker in ("\\", "[", "]", "`", "*", "_"): + text = text.replace(marker, "\\" + marker) + return text + + +def _relative_link(index_path: PurePosixPath, document: Document) -> str: + return posixpath.relpath( + document.relative_path.as_posix(), start=index_path.parent.as_posix() + ) + + +def _front_matter(title: str, description: str) -> str: + rendered = yaml.safe_dump( + { + "title": title, + "description": description, + "hide": ["toc"], + }, + allow_unicode=True, + default_flow_style=False, + sort_keys=False, + ).rstrip() + return f"---\n{rendered}\n---\n\n" + + +def _label_for(vocabulary: Mapping[str, Any], slug: str) -> str: + """Look up a taxonomy label, gracefully falling back to the raw slug.""" + label = vocabulary.get(slug) if isinstance(vocabulary, Mapping) else None + return label if isinstance(label, str) and label else slug + + +def _collection_title(taxonomy: Mapping[str, Any], document_type: Any) -> str: + collections = taxonomy.get("collections", {}) + config = collections.get(document_type) if isinstance(collections, Mapping) else None + if isinstance(config, Mapping): + title = config.get("title") + if isinstance(title, str) and title: + return title + return str(document_type) + + +def _ordered_keys(present: Iterable[str], preferred_order: Iterable[str]) -> list[str]: + """Order group keys by a preferred sequence, appending unknown keys alphabetically.""" + present_set = set(present) + ordered = [key for key in preferred_order if key in present_set] + known = set(ordered) + ordered.extend(sorted(key for key in present_set if key not in known)) + return ordered + + +def _doc_meta_date(document: Document) -> str | None: + document_type = document.metadata.get("document_type") + field_and_label = _DATE_FIELD_BY_TYPE.get(document_type) + if not field_and_label: + return None + field, label = field_and_label + value = document.metadata.get(field) + if value is None: + return None + formatted = value.isoformat() if hasattr(value, "isoformat") else str(value) + if not formatted: + return None + return f"{label}: {formatted}" + + +def _doc_detail_spans(document: Document, taxonomy: Mapping[str, Any]) -> list[str]: + """Build type-specific detail spans, never fabricating missing metadata.""" + metadata = document.metadata + document_type = metadata.get("document_type") + technologies = taxonomy.get("technologies", {}) + spans: list[str] = [] + + if document_type == "case": + status = metadata.get("status") + if isinstance(status, str) and status: + label = _STATUS_LABELS.get(status, status) + spans.append(f"{_escape(label)}") + elif document_type == "guide": + applies_to = metadata.get("applies_to") + if isinstance(applies_to, list) and applies_to: + joined = ", ".join(str(item) for item in applies_to) + spans.append(f"적용 대상: {_escape(joined)}") + elif document_type == "lab": + estimated_time = metadata.get("estimated_time") + if isinstance(estimated_time, str) and estimated_time.strip(): + spans.append(f"소요 시간: {_escape(estimated_time)}") + cost = metadata.get("cost") + if isinstance(cost, str) and cost.strip(): + spans.append(f"예상 비용: {_escape(cost)}") + cleanup_required = metadata.get("cleanup_required") + if isinstance(cleanup_required, bool): + state = "필요" if cleanup_required else "불필요" + spans.append(f"정리 절차: {state}") + elif document_type == "research": + techs = metadata.get("technologies") + if isinstance(techs, list) and techs: + joined = ", ".join(_label_for(technologies, tech) for tech in techs if isinstance(tech, str)) + if joined: + spans.append(f"다룬 기술: {_escape(joined)}") + + return spans + + +def _doc_card( + index_path: PurePosixPath, + document: Document, + taxonomy: Mapping[str, Any], + extra_classes: str = "", +) -> str: + metadata = document.metadata + document_type = metadata.get("document_type", "") + collection_label = _collection_title(taxonomy, document_type) + link = _relative_link(index_path, document) + title = metadata.get("title", "") + description = metadata.get("description", "") + tags = taxonomy.get("tags", {}) + + meta_spans = f'{_escape(collection_label)}' + date_text = _doc_meta_date(document) + if date_text: + meta_spans += f'{_escape(date_text)}' + + tag_values = metadata.get("tags") + tag_html = "" + if isinstance(tag_values, list) and tag_values: + tag_spans = "".join( + f'{_escape(_label_for(tags, tag))}' + for tag in tag_values[:3] + if isinstance(tag, str) + ) + if tag_spans: + tag_html = f'\n
\n{tag_spans}\n
\n' + + detail_spans = _doc_detail_spans(document, taxonomy) + detail_html = "" + if detail_spans: + detail_html = f'\n
\n{"".join(detail_spans)}\n
\n' + + classes = f"dg-doc-card dg-doc-card--{document_type}" + if extra_classes: + classes = f"{classes} {extra_classes}" + + return ( + f'
\n\n' + f'
\n{meta_spans}\n
\n\n' + f'### [{_md_label(title)}]({link})\n\n' + f'

{_escape(description)}

\n' + f'{tag_html}' + f'{detail_html}' + f'\n
\n' + ) + + +def _page_heading(eyebrow_prefix: str, count: int, title: str, description: str) -> str: + eyebrow = f"{eyebrow_prefix} / {count} DOCUMENTS" + return ( + '
\n\n' + f'

{_escape(eyebrow)}

\n\n' + f'# {_md_label(title)}\n\n' + f'

{_escape(description)}

\n\n' + '
\n' + ) + + +def _jump_links(entries: Sequence[tuple[str, str]]) -> str: + """entries: sequence of (anchor, label) pairs, in display order.""" + if not entries: + return "" + links = " ".join(f"[{_md_label(label)}](#{anchor})" for anchor, label in entries) + # A plain Markdown paragraph (not a raw

) so md_in_html re-processes the + # links; python-markdown auto-wraps it in a

for the .dg-jump-links CSS. + return f'

\n' + + +def _empty_state(message: str) -> str: + return f'

{_escape(message)}

\n' + + +def _build_collection_page( + document_type: str, + config: Mapping[str, Any], + matching: list[Document], + taxonomy: Mapping[str, Any], +) -> tuple[PurePosixPath, str]: + index_path = PurePosixPath(config["path"]) / "index.md" + title = config.get("title", document_type) + description = config.get("description", "") + eyebrow_prefix = _EYEBROW_LABELS.get(document_type, str(document_type).upper()) + services = taxonomy.get("services", {}) + + by_service: dict[str, list[Document]] = defaultdict(list) + for document in matching: + parts = document.relative_path.parts + if len(parts) >= 2: + by_service[parts[1]].append(document) + + ordered_service_keys = _ordered_keys(by_service.keys(), services.keys()) + + body = _front_matter(title, description) + body += f'
\n\n' + body += _page_heading(eyebrow_prefix, len(matching), title, description) + body += "\n" + + if not matching: + body += _empty_state("아직 등록된 문서가 없습니다.") + else: + jump_entries = [ + (f"service-{slug}", _label_for(services, slug)) for slug in ordered_service_keys + ] + body += _jump_links(jump_entries) + body += "\n" + for slug in ordered_service_keys: + label = _label_for(services, slug) + body += f"## {_md_label(label)} {{ #service-{slug} }}\n\n" + body += '
\n\n' + for document in by_service[slug]: + body += _doc_card(index_path, document, taxonomy) + body += "\n" + body += "
\n\n" + + body += "
\n" + return index_path, body + + +def _build_service_page( + slug: str, + matching: list[Document], + taxonomy: Mapping[str, Any], +) -> tuple[PurePosixPath, str]: + page_path = PurePosixPath(f"services/{slug}.md") + services = taxonomy.get("services", {}) + collections = taxonomy.get("collections", {}) + label = _label_for(services, slug) + description = f"{label}을 다룬 기록입니다. 지금 겪는 문제나 구현하려는 작업에 맞는 글부터 살펴보세요." + + body = _front_matter(label, description) + body += '
\n\n' + body += _page_heading(_SERVICES_EYEBROW, len(matching), label, description) + body += "\n" + + if not matching: + body += _empty_state("아직 등록된 문서가 없습니다.") + else: + by_type: dict[str, list[Document]] = defaultdict(list) + for document in matching: + by_type[document.metadata.get("document_type")].append(document) + for document_type in collections: + typed = by_type.get(document_type, []) + if not typed: + continue + group_title = _collection_title(taxonomy, document_type) + body += f"## {_md_label(group_title)}\n\n" + body += '
\n\n' + for document in typed: + body += _doc_card(page_path, document, taxonomy) + body += "\n" + body += "
\n\n" + + body += "
\n" + return page_path, body + + +def _build_services_overview_page( + by_service: Mapping[str, list[Document]], + taxonomy: Mapping[str, Any], + total_documents: int, +) -> tuple[PurePosixPath, str]: + services_path = PurePosixPath("services/index.md") + services = taxonomy.get("services", {}) + collections = taxonomy.get("collections", {}) + title = "서비스별 찾기" + description = "사용 중인 서비스에서 출발하세요. 기록이 많은 순서로 살펴보고, 필요한 문제 해결·구현·비교 자료로 이어갈 수 있습니다." + + body = _front_matter(title, description) + body += '
\n\n' + body += _page_heading(_SERVICES_EYEBROW, total_documents, title, description) + body += "\n" + + if not services: + body += _empty_state("아직 등록된 서비스가 없습니다.") + else: + # Nonempty services surface first (highest document count first, then + # label) so zero-document services don't crowd out discovery; empty + # services are still rendered afterward with an explicit 0 marker to + # preserve their paths. + ordered_slugs = sorted( + services, + key=lambda slug: ( + len(by_service.get(slug, [])) == 0, + -len(by_service.get(slug, [])), + _label_for(services, slug).casefold(), + ), + ) + body += '
\n\n' + for slug in ordered_slugs: + label = services[slug] + matching = by_service.get(slug, []) + body += '
\n\n' + body += f"## [{_md_label(label)}]({slug}.md)\n\n" + body += f'

{_escape(f"{len(matching)}개 문서")}

\n' + per_type_counts = [ + (document_type, sum(1 for doc in matching if doc.metadata.get("document_type") == document_type)) + for document_type in collections + ] + present_counts = [(document_type, count) for document_type, count in per_type_counts if count] + if present_counts: + items = "".join( + f"
  • {_escape(_collection_title(taxonomy, document_type))} {count}
  • " + for document_type, count in present_counts + ) + body += f"
      \n{items}\n
    \n" + body += "\n
    \n\n" + body += "
    \n\n" + + body += "
    \n" + return services_path, body + + +def build_index_pages( + documents: Iterable[Document], taxonomy: Mapping[str, Any] +) -> dict[PurePosixPath, str]: + """Return virtual Markdown pages keyed by their docs-relative path.""" + docs = sorted(documents, key=lambda item: str(item.metadata.get("title", "")).casefold()) + pages: dict[PurePosixPath, str] = {} + collections = taxonomy.get("collections", {}) + + for document_type, config in collections.items(): + matching = [doc for doc in docs if doc.metadata.get("document_type") == document_type] + index_path, body = _build_collection_page(document_type, config, matching, taxonomy) + pages[index_path] = body + + by_service: dict[str, list[Document]] = defaultdict(list) + for document in docs: + services_meta = document.metadata.get("services") + if isinstance(services_meta, list): + for service in services_meta: + if isinstance(service, str): + by_service[service].append(document) + + services = taxonomy.get("services", {}) + for slug in services: + page_path, page_body = _build_service_page(slug, by_service.get(slug, []), taxonomy) + pages[page_path] = page_body + + services_path, services_body = _build_services_overview_page(by_service, taxonomy, len(docs)) + pages[services_path] = services_body + return pages + + +def _collection_order(taxonomy: Mapping[str, Any]) -> list[str]: + collections = taxonomy.get("collections", {}) + return list(collections) if isinstance(collections, Mapping) else [] + + +def _select_featured(documents: list[Document], taxonomy: Mapping[str, Any]) -> list[Document]: + order = _collection_order(taxonomy) + + def sort_key(document: Document) -> tuple[int, str]: + document_type = document.metadata.get("document_type") + try: + rank = order.index(document_type) + except ValueError: + rank = len(order) + return rank, str(document.metadata.get("title", "")).casefold() + + ordered = sorted(documents, key=sort_key) + featured = [doc for doc in ordered if doc.metadata.get("featured") is True][:3] + if featured: + return featured + + selected: list[Document] = [] + seen_types: set[Any] = set() + for document_type in order: + if len(selected) >= 3: + break + for document in ordered: + if document.metadata.get("document_type") != document_type: + continue + if document in selected: + continue + selected.append(document) + seen_types.add(document_type) + break + + if len(selected) < 3: + for document in ordered: + if len(selected) >= 3: + break + if document in selected: + continue + selected.append(document) + + return selected[:3] + + +def _feature_card(document: Document, taxonomy: Mapping[str, Any]) -> str: + index_path = PurePosixPath("index.md") + return _doc_card(index_path, document, taxonomy, extra_classes="dg-feature-card") + + +def _build_home_stats(documents: list[Document]) -> str: + total = len(documents) + used_services: set[str] = set() + for document in documents: + services_meta = document.metadata.get("services") + if isinstance(services_meta, list): + used_services.update(service for service in services_meta if isinstance(service, str)) + used_types = { + document.metadata.get("document_type") + for document in documents + if document.metadata.get("document_type") + } + + stats = [ + (total, "전체 문서"), + (len(used_services), "다루는 서비스"), + (len(used_types), "문서 유형"), + ] + body = '
    \n\n' + for number, label in stats: + body += ( + '
    ' + f"{_escape(number)}" + f"{_escape(label)}" + "
    \n" + ) + body += "\n
    \n" + return body + + +def _build_home_featured(documents: list[Document], taxonomy: Mapping[str, Any]) -> str: + selected = _select_featured(documents, taxonomy) + if not selected: + return _empty_state("아직 소개할 문서가 없습니다.") + + body = '
    \n\n' + for document in selected: + body += _feature_card(document, taxonomy) + body += "\n" + body += "
    \n" + return body + + +def _build_home_collections(documents: list[Document], taxonomy: Mapping[str, Any]) -> str: + collections = taxonomy.get("collections", {}) + if not collections: + return _empty_state("아직 등록된 컬렉션이 없습니다.") + + body = '
    \n\n' + for document_type, config in collections.items(): + count = sum(1 for doc in documents if doc.metadata.get("document_type") == document_type) + path = config.get("path", document_type) + title = config.get("title", document_type) + description = config.get("description", "") + body += f'
    \n\n' + body += f'

    {_escape(_EYEBROW_LABELS.get(document_type, str(document_type).upper()))}

    \n\n' + body += f"### [{_md_label(title)}]({path}/index.md)\n\n" + body += f'

    {_escape(description)}

    \n' + body += f'

    {_escape(f"{count}개 문서")}

    \n\n' + body += "
    \n\n" + body += "
    \n" + return body + + +def build_home_page( + template: str, documents: Iterable[Document], taxonomy: Mapping[str, Any] +) -> str: + """Render the home landing page by filling metadata-driven slots in ``template``.""" + for slot in _HOME_SLOTS: + occurrences = template.count(slot) + if occurrences == 0: + raise ValueError(f"home template is missing required slot: {slot}") + if occurrences > 1: + raise ValueError(f"home template has duplicate slot: {slot}") + + docs = sorted(documents, key=lambda item: str(item.metadata.get("title", "")).casefold()) + + rendered = template + rendered = rendered.replace("", _build_home_stats(docs), 1) + rendered = rendered.replace("", _build_home_featured(docs, taxonomy), 1) + rendered = rendered.replace( + "", _build_home_collections(docs, taxonomy), 1 + ) + return rendered + + +def write_generated_pages(repo_root: Path | None = None) -> None: + """Write virtual pages through mkdocs-gen-files during a site build.""" + import mkdocs_gen_files + + root = repo_root or REPO_ROOT + taxonomy = load_taxonomy(root / "docs-taxonomy.yml") + documents = list(iter_public_documents(root / "docs", taxonomy)) + + for path, content in build_index_pages(documents, taxonomy).items(): + with mkdocs_gen_files.open(path.as_posix(), "w") as generated: + generated.write(content) + + template = (root / "docs" / "index.md").read_text(encoding="utf-8") + home_page = build_home_page(template, documents, taxonomy) + with mkdocs_gen_files.open("index.md", "w") as generated: + generated.write(home_page) + + +if __name__ == "__main__" or __name__.startswith("<"): + write_generated_pages() diff --git a/scripts/docs/hooks.py b/scripts/docs/hooks.py new file mode 100644 index 00000000..a4260ce4 --- /dev/null +++ b/scripts/docs/hooks.py @@ -0,0 +1,81 @@ +"""MkDocs hooks for article summaries and source references.""" + +from __future__ import annotations + +from html import escape +from pathlib import Path, PurePosixPath +import sys +from typing import Any, Mapping + +from mkdocs.structure.nav import Navigation, Section +from mkdocs.structure.pages import Page + + +REPO_ROOT = Path(__file__).resolve().parents[2] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from scripts.docs.content import load_taxonomy + + +def on_nav(nav: Navigation, config: Mapping[str, Any], files: Any) -> Navigation: + """Expose bundles as document leaves, using metadata rather than folder labels.""" + taxonomy = load_taxonomy(Path(config["docs_dir"]).parent / "docs-taxonomy.yml") + collections = {entry["path"] for entry in taxonomy["collections"].values()} + for collection in nav.items: + if not isinstance(collection, Section): + continue + for service in collection.children: + if not isinstance(service, Section): + continue + for index, bundle in enumerate(service.children): + if not isinstance(bundle, Section) or len(bundle.children) != 1: + continue + page = bundle.children[0] + if not isinstance(page, Page): + continue + parts = PurePosixPath(page.file.src_uri).parts + if len(parts) != 4 or parts[0] not in collections or parts[-1] != "index.md": + continue + service.title = taxonomy["services"][parts[1]] + page.parent = service + service.children[index] = page + return nav + + +def _insert_after_title(markdown: str, block: str) -> str: + lines = markdown.splitlines() + for index, line in enumerate(lines): + if line.startswith("# "): + lines[index + 1 : index + 1] = ["", block.rstrip(), ""] + return "\n".join(lines).rstrip() + "\n" + return block + "\n" + markdown + + +def on_page_markdown(markdown: str, page: Any, config: Mapping[str, Any], files: Any) -> str: + """Render reader-facing summaries and references without workflow notices.""" + metadata = page.meta + if not metadata.get("document_type"): + return markdown + + sources = metadata.get("official_sources") or [] + source_links = "\n".join( + f'
  • ' + f'{escape(str(source["title"]))}
  • ' + for source in sources + if isinstance(source, Mapping) and source.get("title") and source.get("url") + ) + description = metadata.get("description") + if description: + markdown = _insert_after_title( + markdown, f'

    {escape(str(description))}

    ' + ) + if source_links: + markdown = ( + markdown.rstrip() + + '\n\n
    \n' + + "참고 문서\n
      \n" + + source_links + + "\n
    \n
    \n" + ) + return markdown diff --git a/scripts/docs/validate_links.py b/scripts/docs/validate_links.py new file mode 100644 index 00000000..cc5209f5 --- /dev/null +++ b/scripts/docs/validate_links.py @@ -0,0 +1,120 @@ +"""Validate rendered local links, image targets, and image alt text.""" + +from __future__ import annotations + +import argparse +from dataclasses import dataclass +from html.parser import HTMLParser +from pathlib import Path +import sys +from typing import Any, Iterable, Mapping +from urllib.parse import unquote, urlparse + +from markdown import Markdown +from mkdocs.config import load_config + +REPO_ROOT = Path(__file__).resolve().parents[2] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from scripts.docs.content import DocumentFormatError, ValidationResult, load_document, load_taxonomy + + +@dataclass(frozen=True) +class _RenderedLink: + target: str + alt_text: str | None = None + + +class _LinkParser(HTMLParser): + def __init__(self) -> None: + super().__init__() + self.links: list[_RenderedLink] = [] + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + attributes = dict(attrs) + if tag == "img": + target = attributes.get("src") + alt_text = (attributes.get("alt") or "").strip() + elif tag == "a": + target = attributes.get("href") + alt_text = None + else: + return + if target is not None: + self.links.append(_RenderedLink(target, alt_text)) + + +def _candidate_paths(docs_dir: Path, taxonomy: Mapping[str, Any]) -> Iterable[Path]: + for config in taxonomy.get("collections", {}).values(): + collection = docs_dir / config["path"] + if collection.is_dir(): + yield from sorted(collection.rglob("index.md")) + + +def _resolve_target(page: Path, docs_dir: Path, raw_target: str) -> Path | None: + target = raw_target.removeprefix("<").removesuffix(">") + if target.startswith("#"): + return None + parsed = urlparse(target) + if parsed.scheme or parsed.netloc: + return None + path_text = unquote(parsed.path) + if not path_text: + return None + resolved = (docs_dir / path_text.lstrip("/")) if path_text.startswith("/") else (page.parent / path_text) + resolved = resolved.resolve() + if resolved.is_dir() or path_text.endswith("/"): + return resolved / "index.md" + if not resolved.suffix: + directory_index = resolved / "index.md" + if directory_index.exists(): + return directory_index + return resolved + + +def validate_repository(repo_root: Path | str) -> ValidationResult: + root = Path(repo_root) + docs_dir = root / "docs" + taxonomy = load_taxonomy(root / "docs-taxonomy.yml") + config = load_config(config_file=str(root / "mkdocs.yml")) + renderer = Markdown( + extensions=config.markdown_extensions, extension_configs=config.mdx_configs + ) + errors: list[str] = [] + count = 0 + for page in _candidate_paths(docs_dir, taxonomy): + count += 1 + relative = page.relative_to(root).as_posix() + try: + document = load_document(page, docs_dir=docs_dir) + except DocumentFormatError as error: + errors.append(str(error)) + continue + parser = _LinkParser() + parser.feed(renderer.reset().convert(document.body)) + parser.close() + # Rendered HTML positions do not correspond to Markdown source lines. + for link in parser.links: + if link.alt_text is not None and not link.alt_text: + errors.append(f"{relative}: image alt text is required") + target = _resolve_target(page, docs_dir, link.target) + if target is not None and not target.exists(): + errors.append(f"{relative}: target does not exist: {link.target}") + return ValidationResult(count, errors) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repo-root", type=Path, default=REPO_ROOT) + args = parser.parse_args() + result = validate_repository(args.repo_root) + if result.errors: + print("\n".join(result.errors)) + return 1 + print(f"Validated links for {result.document_count} public documents.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/docs/validate_metadata.py b/scripts/docs/validate_metadata.py new file mode 100644 index 00000000..a8fea7ed --- /dev/null +++ b/scripts/docs/validate_metadata.py @@ -0,0 +1,70 @@ +"""Validate public-document paths and front matter.""" + +from __future__ import annotations + +import argparse +from datetime import date +from pathlib import Path +import sys +from typing import Iterable, Mapping, Any + +REPO_ROOT = Path(__file__).resolve().parents[2] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from scripts.docs.content import ( + DocumentFormatError, + ValidationResult, + load_document, + load_taxonomy, + validate_document, +) + + +def _candidate_paths(docs_dir: Path, taxonomy: Mapping[str, Any]) -> Iterable[Path]: + paths = { + config["path"] + for config in taxonomy.get("collections", {}).values() + if isinstance(config, Mapping) and isinstance(config.get("path"), str) + } + for collection in sorted(paths): + root = docs_dir / collection + if root.is_dir(): + yield from sorted(root.rglob("*.md")) + + +def validate_repository(repo_root: Path | str, today: date | None = None) -> ValidationResult: + root = Path(repo_root) + docs_dir = root / "docs" + taxonomy = load_taxonomy(root / "docs-taxonomy.yml") + errors: list[str] = [] + count = 0 + for path in _candidate_paths(docs_dir, taxonomy): + relative = path.relative_to(root).as_posix() + count += 1 + try: + document = load_document(path, docs_dir=docs_dir) + except DocumentFormatError as error: + errors.append(str(error)) + continue + errors.extend( + f"{relative}: {message}" + for message in validate_document(document, taxonomy, today=today) + ) + return ValidationResult(count, errors) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repo-root", type=Path, default=REPO_ROOT) + args = parser.parse_args() + result = validate_repository(args.repo_root) + if result.errors: + print("\n".join(result.errors)) + return 1 + print(f"Validated metadata for {result.document_count} public documents.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/docs/validate_public_safety.py b/scripts/docs/validate_public_safety.py new file mode 100644 index 00000000..541c974c --- /dev/null +++ b/scripts/docs/validate_public_safety.py @@ -0,0 +1,159 @@ +"""Detect environment-specific identifiers before public documentation is published.""" + +from __future__ import annotations + +import argparse +from dataclasses import dataclass +from pathlib import Path +import re + + +REPO_ROOT = Path(__file__).resolve().parents[2] +TEXT_SUFFIXES = { + ".bicep", + ".bicepparam", + ".eml", + ".env", + ".excalidraw", + ".html", + ".java", + ".js", + ".json", + ".md", + ".mmd", + ".ps1", + ".py", + ".sh", + ".svg", + ".txt", + ".xml", + ".yaml", + ".yml", +} +TEXT_FILENAMES = {".env", "dockerfile"} +TEXT_COMPOUND_SUFFIXES = (".env.example",) +SCAN_ROOTS = ("docs", "samples") +UUID = r"[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}" +KNOWN_ENVIRONMENT_MARKERS = ( + "rg-rubicon", + "rg-krafton", + "ais-aiplay", + "saidxtest", + "rg-hellices", + "acrcustomvec", + "cae-customvec", + "icycliff", + "db-neu-prd", + "onnuri-search", + "aisearchtest", + "acrmemray", +) +ENVIRONMENT_SPECIFIC_NAME = re.compile( + r"(?i)\b(?:" + + "|".join(re.escape(marker) for marker in KNOWN_ENVIRONMENT_MARKERS) + + r")(?:[-_a-z0-9]*)\b" +) +AZURE_SERVICE_HOSTNAME = re.compile( + r"(?i)\b[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?\." + r"(?:search\.windows\.net|azurecr\.io|azurecontainerapps\.io)\b" +) +GENERIC_AZURE_SERVICE_HOSTNAMES = {"myregistry.azurecr.io"} +CHECKS = ( + ( + "Azure subscription ID", + re.compile( + rf"(?i)(?:/subscriptions/|\bsubscription_?id\b[^\r\n]{{0,24}}?|구독[^\r\n]{{0,24}}?)({UUID})" + ), + ), + ( + "agent thread ID", + re.compile(rf"(?i)\bagent\s+thread(?:\s+id)?\b[^\r\n]{{0,24}}?({UUID})"), + ), + ("environment-specific name", ENVIRONMENT_SPECIFIC_NAME), + ( + "IP-based nip.io endpoint", + re.compile(r"(?i)(?:[0-9]{1,3}\.){3}[0-9]{1,3}\.nip\.io"), + ), + ( + "deployment-specific lab suffix", + re.compile( + r"(?i)\b(?:[a-z0-9-]*sre(?:-event)?-lab-|stsrelab)[0-9a-f]{8}\b" + ), + ), +) + + +@dataclass(frozen=True) +class PublicSafetyResult: + file_count: int + errors: list[str] + + +def _is_example_azure_service_hostname(hostname: str) -> bool: + lowered = hostname.casefold() + return ( + lowered in GENERIC_AZURE_SERVICE_HOSTNAMES + or any("example" in label for label in lowered.split(".")) + ) + + +def _text_files(root: Path): + for top_level in SCAN_ROOTS: + directory = root / top_level + if not directory.is_dir(): + continue + for path in sorted(directory.rglob("*")): + lowered_name = path.name.casefold() + if path.is_file() and ( + path.suffix.casefold() in TEXT_SUFFIXES + or lowered_name in TEXT_FILENAMES + or lowered_name.endswith(TEXT_COMPOUND_SUFFIXES) + ): + yield path + + +def validate_repository(repo_root: Path | str) -> PublicSafetyResult: + root = Path(repo_root) + errors = [ + f"{name}: expected public content directory is missing under {root}" + for name in SCAN_ROOTS + if not (root / name).is_dir() + ] + count = 0 + for path in _text_files(root): + count += 1 + relative = path.relative_to(root).as_posix() + try: + text = path.read_text(encoding="utf-8-sig") + except UnicodeDecodeError: + errors.append(f"{relative}: file is not valid UTF-8") + continue + for line_number, line in enumerate(text.splitlines(), start=1): + for label, pattern in CHECKS: + if pattern.search(line): + errors.append(f"{relative}:{line_number}: possible {label}") + for match in AZURE_SERVICE_HOSTNAME.finditer(line): + if not _is_example_azure_service_hostname(match.group(0)): + errors.append( + f"{relative}:{line_number}: possible " + "non-example Azure service hostname" + ) + if count == 0: + errors.append(f"{root}: no public text files were scanned") + return PublicSafetyResult(count, errors) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repo-root", type=Path, default=REPO_ROOT) + args = parser.parse_args() + result = validate_repository(args.repo_root) + if result.errors: + print("\n".join(result.errors)) + return 1 + print(f"Checked {result.file_count} public text files for environment-specific identifiers.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/docs/validate_search_index.py b/scripts/docs/validate_search_index.py new file mode 100644 index 00000000..731f3452 --- /dev/null +++ b/scripts/docs/validate_search_index.py @@ -0,0 +1,179 @@ +"""Validate that the built Pages search index covers pages, body text, and tags.""" + +from __future__ import annotations + +import argparse +from dataclasses import dataclass +from html.parser import HTMLParser +import json +from pathlib import Path +import re +import sys + +from markdown import Markdown +from mkdocs.config import load_config + + +REPO_ROOT = Path(__file__).resolve().parents[2] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from scripts.docs.content import iter_public_documents, load_taxonomy + + +KOREAN_WORD = re.compile(r"[가-힣]{2,}") +ENGLISH_PRODUCT_PHRASE = re.compile( + r"\b[A-Z][A-Za-z0-9.+/-]*(?:[ \t]+[A-Z][A-Za-z0-9.+/-]*)+\b" +) + + +class _VisibleTextParser(HTMLParser): + BLOCK_TAGS = { + "blockquote", "br", "div", "h1", "h2", "h3", "h4", "h5", "h6", + "hr", "li", "p", "pre", "td", "th", + } + + def __init__(self) -> None: + super().__init__() + self.parts: list[str] = [] + self.hidden_tag: str | None = None + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + if tag in {"script", "style"}: + self.hidden_tag = tag + if tag in self.BLOCK_TAGS: + self.parts.append("\n") + + def handle_endtag(self, tag: str) -> None: + if tag == self.hidden_tag: + self.hidden_tag = None + if tag in self.BLOCK_TAGS: + self.parts.append("\n") + + def handle_data(self, data: str) -> None: + if self.hidden_tag is None: + self.parts.append(data.replace("\n", " ")) + + +def _visible_text(html: str) -> str: + """Keep block boundaries so product phrases cannot span unrelated paragraphs.""" + parser = _VisibleTextParser() + parser.feed(html) + parser.close() + return "".join(parser.parts) + + +@dataclass(frozen=True) +class SearchIndexResult: + document_count: int + tag_count: int + errors: list[str] + + +def _page_location(relative_path) -> str: + parent = relative_path.parent.as_posix() + return "" if parent in {"", "."} else parent.rstrip("/") + "/" + + +def _normalized(value: str) -> str: + return " ".join(value.split()) + + +def _representative_match(pattern: re.Pattern[str], body: str, title: str) -> str | None: + for match in pattern.finditer(body): + candidate = _normalized(match.group(0)) + if candidate and candidate not in title: + return candidate + return None + + +def validate_repository(repo_root: Path | str) -> SearchIndexResult: + root = Path(repo_root) + taxonomy = load_taxonomy(root / "docs-taxonomy.yml") + documents = list(iter_public_documents(root / "docs", taxonomy)) + index_path = root / "site" / "search" / "search_index.json" + if not index_path.is_file(): + return SearchIndexResult( + len(documents), + 0, + [f"{index_path.relative_to(root).as_posix()}: search index is missing"], + ) + + try: + data = json.loads(index_path.read_text(encoding="utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as error: + return SearchIndexResult(len(documents), 0, [f"{index_path}: invalid JSON: {error}"]) + entries = data.get("docs", []) if isinstance(data, dict) else [] + if not isinstance(entries, list): + return SearchIndexResult(len(documents), 0, [f"{index_path}: docs must be a list"]) + + errors: list[str] = [] + used_tags: set[str] = set() + config = load_config(config_file=str(root / "mkdocs.yml")) + renderer = Markdown( + extensions=config.markdown_extensions, extension_configs=config.mdx_configs + ) + for document in documents: + title = str(document.metadata.get("title", "")) + location = _page_location(document.relative_path) + page_entries = [ + entry + for entry in entries + if isinstance(entry, dict) + and ( + entry.get("location") == location + or str(entry.get("location", "")).startswith(location + "#") + ) + ] + relative = document.path.relative_to(root).as_posix() + if not page_entries: + errors.append(f"{relative}: page is missing from the search index") + page_text = "" + else: + page_text = _normalized( + " ".join( + f"{entry.get('title', '')} {_visible_text(str(entry.get('text', '')))}" + for entry in page_entries + ) + ) + + body_text = _visible_text(renderer.reset().convert(document.body)) + korean = _representative_match(KOREAN_WORD, body_text, title) + if korean and korean not in page_text: + errors.append(f"{relative}: Korean body text is missing from the search index") + english = _representative_match(ENGLISH_PRODUCT_PHRASE, body_text, title) + if english and english not in page_text: + errors.append( + f"{relative}: English product phrase is missing from the search index: {english}" + ) + tags = document.metadata.get("tags", []) + if isinstance(tags, list): + used_tags.update(str(tag) for tag in tags) + + locations = { + str(entry.get("location", "")) for entry in entries if isinstance(entry, dict) + } + for tag in sorted(used_tags): + if f"tags/#tag:{tag}" not in locations: + errors.append(f"search index tag is missing: {tag}") + + return SearchIndexResult(len(documents), len(used_tags), errors) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repo-root", type=Path, default=REPO_ROOT) + args = parser.parse_args() + result = validate_repository(args.repo_root) + if result.errors: + print("\n".join(result.errors)) + return 1 + print( + f"Validated search coverage for {result.document_count} public documents " + f"and {result.tag_count} tags." + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/docs/validate_sources.py b/scripts/docs/validate_sources.py new file mode 100644 index 00000000..a9c13bc1 --- /dev/null +++ b/scripts/docs/validate_sources.py @@ -0,0 +1,56 @@ +"""Validate Microsoft Learn and upstream official-source evidence.""" + +from __future__ import annotations + +import argparse +from datetime import date +from pathlib import Path +import sys + +REPO_ROOT = Path(__file__).resolve().parents[2] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from scripts.docs.content import ( + DocumentFormatError, + ValidationResult, + iter_public_documents, + load_taxonomy, + validate_source_metadata, +) + + +def validate_repository(repo_root: Path | str, today: date | None = None) -> ValidationResult: + root = Path(repo_root) + docs_dir = root / "docs" + taxonomy = load_taxonomy(root / "docs-taxonomy.yml") + errors: list[str] = [] + count = 0 + try: + documents = list(iter_public_documents(docs_dir, taxonomy)) + except DocumentFormatError as error: + return ValidationResult(0, [str(error)]) + for document in documents: + count += 1 + relative = document.path.relative_to(root).as_posix() + errors.extend( + f"{relative}: {message}" + for message in validate_source_metadata(document.metadata, taxonomy, today=today) + ) + return ValidationResult(count, errors) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repo-root", type=Path, default=REPO_ROOT) + args = parser.parse_args() + result = validate_repository(args.repo_root) + if result.errors: + print("\n".join(result.errors)) + return 1 + print(f"Validated official sources for {result.document_count} public documents.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/docs/fixtures/invalid-guide.md b/tests/docs/fixtures/invalid-guide.md new file mode 100644 index 00000000..300fe14f --- /dev/null +++ b/tests/docs/fixtures/invalid-guide.md @@ -0,0 +1 @@ +# Front matter가 없는 문서 diff --git a/tests/docs/fixtures/valid-guide.md b/tests/docs/fixtures/valid-guide.md new file mode 100644 index 00000000..1b39ee68 --- /dev/null +++ b/tests/docs/fixtures/valid-guide.md @@ -0,0 +1,26 @@ +--- +title: AKS 네트워크 진단 +description: AKS 네트워크 문제를 진단하는 절차 +document_type: guide +services: + - aks +technologies: + - kubernetes +tags: + - networking + - troubleshooting +status: current +verification_status: verified +sources_checked_at: 2026-09-12 +official_sources: + - title: Azure Kubernetes Service documentation + url: https://learn.microsoft.com/azure/aks/ +last_verified: 2026-09-12 +review_cycle_days: 180 +applies_to: + - AKS 1.34+ +--- + +# AKS 네트워크 진단 + +본문입니다. diff --git a/tests/docs/test_content.py b/tests/docs/test_content.py new file mode 100644 index 00000000..f18280cf --- /dev/null +++ b/tests/docs/test_content.py @@ -0,0 +1,227 @@ +from __future__ import annotations + +from datetime import date +from pathlib import Path + +import pytest + +from scripts.docs.content import ( + DocumentFormatError, + iter_public_documents, + load_document, + load_taxonomy, + validate_document, +) + + +FIXTURES = Path(__file__).parent / "fixtures" +ROOT = Path(__file__).parents[2] + + +@pytest.fixture +def taxonomy() -> dict: + return { + "collections": { + "guide": { + "path": "guides", + "statuses": ["current", "needs-review", "deprecated"], + "required_fields": [ + "last_verified", + "review_cycle_days", + "applies_to", + ], + } + }, + "services": {"aks": "Azure Kubernetes Service"}, + "technologies": {"kubernetes": "Kubernetes"}, + "tags": { + "networking": "Networking", + "troubleshooting": "Troubleshooting", + }, + "verification_statuses": ["verified", "needs-review"], + "official_source_hosts": ["learn.microsoft.com", "kubernetes.io"], + "required_source_host": "learn.microsoft.com", + } + + +def copy_fixture(tmp_path: Path, name: str, relative: str) -> Path: + target = tmp_path / "docs" / relative + target.parent.mkdir(parents=True) + target.write_text((FIXTURES / name).read_text(encoding="utf-8"), encoding="utf-8") + return target + + +def test_valid_page_bundle_satisfies_the_content_contract( + tmp_path: Path, taxonomy: dict +) -> None: + path = copy_fixture( + tmp_path, + "valid-guide.md", + "guides/aks/network-diagnosis/index.md", + ) + + document = load_document(path, docs_dir=tmp_path / "docs") + + assert document.relative_path.as_posix() == ( + "guides/aks/network-diagnosis/index.md" + ) + assert document.metadata["sources_checked_at"] == date(2026, 9, 12) + assert validate_document(document, taxonomy, today=date(2026, 9, 12)) == [] + + +def test_page_bundle_requires_yaml_front_matter(tmp_path: Path) -> None: + path = copy_fixture( + tmp_path, + "invalid-guide.md", + "guides/aks/no-front-matter/index.md", + ) + + with pytest.raises(DocumentFormatError, match="YAML front matter"): + load_document(path, docs_dir=tmp_path / "docs") + + +def test_content_contract_rejects_invalid_path_taxonomy_and_source( + tmp_path: Path, taxonomy: dict +) -> None: + path = copy_fixture( + tmp_path, + "valid-guide.md", + "guides/aks/Network_Diagnosis.md", + ) + loaded = load_document(path, docs_dir=tmp_path / "docs") + metadata = { + **loaded.metadata, + "services": ["unknown"], + "status": "draft", + "sources_checked_at": date(2026, 9, 13), + "official_sources": [ + {"title": "Kubernetes", "url": "http://kubernetes.io/docs/"} + ], + } + + errors = validate_document( + loaded.with_metadata(metadata), + taxonomy, + today=date(2026, 9, 12), + ) + + expected = ( + "public documents must use ///index.md", + "unknown service: unknown", + "invalid guide status: draft", + "sources_checked_at cannot be in the future", + "official source URL must use HTTPS", + "at least one official source must use learn.microsoft.com", + ) + for message in expected: + assert any(message in error for error in errors), errors + + +def test_unverified_guide_can_leave_last_verified_empty( + tmp_path: Path, taxonomy: dict +) -> None: + path = copy_fixture( + tmp_path, + "valid-guide.md", + "guides/aks/network-diagnosis/index.md", + ) + loaded = load_document(path, docs_dir=tmp_path / "docs") + metadata = { + **loaded.metadata, + "status": "needs-review", + "verification_status": "needs-review", + "last_verified": None, + } + + errors = validate_document( + loaded.with_metadata(metadata), + taxonomy, + today=date(2026, 9, 12), + ) + + assert errors == [] + + +@pytest.mark.parametrize( + ("field", "value"), + [ + ("document_type", ["guide"]), + ("document_type", {"name": "guide"}), + ("review_cycle_days", True), + ("review_cycle_days", None), + ("applies_to", None), + ("applies_to", []), + ("featured", "true"), + ("featured", 1), + ], +) +def test_invalid_metadata_values_return_validation_errors( + tmp_path: Path, taxonomy: dict, field: str, value: object +) -> None: + path = copy_fixture( + tmp_path, "valid-guide.md", "guides/aks/network-diagnosis/index.md" + ) + loaded = load_document(path, docs_dir=tmp_path / "docs") + + errors = validate_document( + loaded.with_metadata({**loaded.metadata, field: value}), + taxonomy, + today=date(2026, 9, 12), + ) + + assert any(field in error for error in errors), errors + + +@pytest.mark.parametrize("featured", [True, False]) +def test_featured_is_optional_boolean( + tmp_path: Path, taxonomy: dict, featured: bool +) -> None: + path = copy_fixture( + tmp_path, "valid-guide.md", "guides/aks/network-diagnosis/index.md" + ) + loaded = load_document(path, docs_dir=tmp_path / "docs") + + assert validate_document( + loaded.with_metadata({**loaded.metadata, "featured": featured}), + taxonomy, + today=date(2026, 9, 12), + ) == [] + + +def test_malformed_source_url_returns_a_validation_error( + tmp_path: Path, taxonomy: dict +) -> None: + path = copy_fixture( + tmp_path, "valid-guide.md", "guides/aks/network-diagnosis/index.md" + ) + loaded = load_document(path, docs_dir=tmp_path / "docs") + + errors = validate_document( + loaded.with_metadata( + { + **loaded.metadata, + "official_sources": [{"title": "Source", "url": "https://[invalid"}], + } + ), + taxonomy, + today=date(2026, 9, 12), + ) + + assert any("official_sources[0].url" in error for error in errors), errors + + +def test_repository_uses_provider_consistent_service_slugs() -> None: + taxonomy = load_taxonomy(ROOT / "docs-taxonomy.yml") + + for slug, label in taxonomy["services"].items(): + if label.startswith("Azure "): + assert slug.startswith("azure-"), f"{label} must use an azure-* slug" + if label.startswith("Microsoft "): + assert slug.startswith( + "microsoft-" + ), f"{label} must use a microsoft-* slug" + + for document in iter_public_documents(ROOT / "docs", taxonomy): + service = document.relative_path.parts[1] + assert service in taxonomy["services"] + assert service in document.metadata["services"] diff --git a/tests/docs/test_indexes.py b/tests/docs/test_indexes.py new file mode 100644 index 00000000..8901aed9 --- /dev/null +++ b/tests/docs/test_indexes.py @@ -0,0 +1,787 @@ +"""Tests for the metadata-driven landing/index generator. + +These tests inspect meaningful rendered links, metadata, and coverage rather +than exact prose or full-page snapshots, per the public documentation +contract for generated collection/service/home pages. +""" + +from __future__ import annotations + +from datetime import date +from html import escape +from pathlib import Path, PurePosixPath + +from markdown import Markdown +from mkdocs.config import load_config +import pytest +import yaml + +from scripts.docs.content import Document +from scripts.docs.generate_indexes import build_home_page, build_index_pages + + +ROOT = Path(__file__).parents[2] + + +def doc(relative_path: str, **metadata: object) -> Document: + return Document( + path=Path(relative_path), + relative_path=PurePosixPath(relative_path), + metadata=metadata, + body="", + ) + + +@pytest.fixture +def taxonomy() -> dict: + return { + "collections": { + "case": { + "path": "cases", + "title": "트러블슈팅", + "description": "사례 모음입니다.", + }, + "guide": { + "path": "guides", + "title": "구현 가이드", + "description": "가이드 모음입니다.", + }, + "lab": { + "path": "labs", + "title": "실습", + "description": "실습 모음입니다.", + }, + "research": { + "path": "research", + "title": "비교·분석", + "description": "리서치 모음입니다.", + }, + }, + "services": { + "azure-kubernetes-service": "Azure Kubernetes Service", + "azure-monitor": "Azure Monitor", + }, + "technologies": {"kubernetes": "Kubernetes", "python": "Python"}, + "tags": { + "latency": "Latency", + "networking": "Networking", + "monitoring": "Monitoring", + }, + } + + +@pytest.fixture +def markdown_renderer() -> Markdown: + config = load_config(config_file=str(ROOT / "mkdocs.yml")) + return Markdown(extensions=config.markdown_extensions, extension_configs=config.mdx_configs) + + +def render_body(markdown_renderer: Markdown, page: str) -> str: + """Render a generated virtual page's Markdown body (after front matter) to HTML.""" + body = page.split("---", 2)[2] + return markdown_renderer.reset().convert(body) + + +# --------------------------------------------------------------------------- +# build_index_pages: collection pages +# --------------------------------------------------------------------------- + + +def test_collection_page_groups_by_primary_service_without_duplicates(taxonomy: dict) -> None: + multi_service_doc = doc( + "guides/azure-monitor/diag/index.md", + title="AKS 진단 가이드", + description="진단 절차", + document_type="guide", + services=["azure-monitor", "azure-kubernetes-service"], + status="current", + last_verified=date(2026, 1, 1), + applies_to=["AKS 1.34+"], + verification_status="verified", + tags=["networking"], + ) + other_doc = doc( + "guides/azure-kubernetes-service/other/index.md", + title="다른 가이드", + description="다른 절차", + document_type="guide", + services=["azure-kubernetes-service"], + status="current", + last_verified=date(2026, 1, 2), + applies_to=["AKS 1.34+"], + verification_status="verified", + tags=[], + ) + + pages = build_index_pages([multi_service_doc, other_doc], taxonomy) + guide_index = pages[PurePosixPath("guides/index.md")] + + # The multi-service document is grouped by its path's primary service + # (azure-monitor) and therefore appears exactly once, even though its + # metadata also lists azure-kubernetes-service. + assert guide_index.count("AKS 진단 가이드") == 1 + assert "azure-monitor/diag/index.md" in guide_index + assert "azure-kubernetes-service/other/index.md" in guide_index + + # Jump links target explicit, existing H2 anchors for each present group. + assert "#service-azure-monitor" in guide_index + assert "#service-azure-kubernetes-service" in guide_index + assert '## Azure Monitor { #service-azure-monitor }' in guide_index + assert '## Azure Kubernetes Service { #service-azure-kubernetes-service }' in guide_index + + +def test_collection_page_reflects_real_counts_and_wrapper_classes(taxonomy: dict) -> None: + documents = [ + doc( + f"cases/azure-kubernetes-service/case-{i}/index.md", + title=f"사례 {i}", + description="설명", + document_type="case", + services=["azure-kubernetes-service"], + status="resolved", + occurred_at=date(2026, 6, i + 1), + verification_status="verified", + tags=[], + ) + for i in range(2) + ] + + pages = build_index_pages(documents, taxonomy) + case_index = pages[PurePosixPath("cases/index.md")] + + assert 'class="dg-landing dg-collection dg-collection--case"' in case_index + assert "CASE FILES / 2 DOCUMENTS" in case_index + assert 'class="dg-doc-card dg-doc-card--case"' in case_index + assert case_index.count('class="dg-doc-card') == 2 + + +def test_collection_page_empty_state_is_explicit_and_has_no_fake_data(taxonomy: dict) -> None: + pages = build_index_pages([], taxonomy) + lab_index = pages[PurePosixPath("labs/index.md")] + + assert "LABS / 0 DOCUMENTS" in lab_index + assert "dg-doc-card" not in lab_index + assert "dg-jump-links" not in lab_index + assert "다" in lab_index or "없" in lab_index # explicit, human-readable empty message + + +def test_status_labels_are_mapped_to_korean_copy_not_raw_strings(taxonomy: dict) -> None: + resolved = doc( + "cases/azure-monitor/resolved/index.md", + title="해결된 사례", + description="설명", + document_type="case", + services=["azure-monitor"], + status="resolved", + occurred_at=date(2026, 1, 1), + verification_status="verified", + tags=[], + ) + unresolved = doc( + "cases/azure-monitor/unresolved/index.md", + title="조사 중 사례", + description="설명", + document_type="case", + services=["azure-monitor"], + status="unresolved", + occurred_at=date(2026, 1, 2), + verification_status="needs-review", + tags=[], + ) + historical = doc( + "cases/azure-monitor/historical/index.md", + title="과거 사례", + description="설명", + document_type="case", + services=["azure-monitor"], + status="historical", + occurred_at=date(2026, 1, 3), + verification_status="verified", + tags=[], + ) + + pages = build_index_pages([resolved, unresolved, historical], taxonomy) + case_index = pages[PurePosixPath("cases/index.md")] + + assert "해결 기록" in case_index + assert "조사 중" in case_index + assert "과거 사례" in case_index + # Raw taxonomy status strings must never leak as user-facing copy. + assert ">resolved<" not in case_index + assert ">unresolved<" not in case_index + assert ">historical<" not in case_index + + +def test_review_metadata_is_preserved_without_reader_badges(taxonomy: dict) -> None: + needs_review = doc( + "guides/azure-monitor/needs-review/index.md", + title="검토 필요 가이드", + description="설명", + document_type="guide", + services=["azure-monitor"], + status="needs-review", + last_verified=None, + applies_to=["AKS 1.34+"], + verification_status="needs-review", + tags=[], + ) + verified = doc( + "guides/azure-monitor/verified/index.md", + title="검증된 가이드", + description="설명", + document_type="guide", + services=["azure-monitor"], + status="current", + last_verified=date(2026, 3, 1), + applies_to=["AKS 1.34+"], + verification_status="verified", + tags=[], + ) + + pages = build_index_pages([needs_review, verified], taxonomy) + guide_index = pages[PurePosixPath("guides/index.md")] + + assert "dg-review-state" not in guide_index + assert "근거 재검토 필요" not in guide_index + assert needs_review.metadata["verification_status"] == "needs-review" + # No fabricated last_verified date for the needs-review document. + needs_review_card = guide_index.split("검토 필요 가이드")[0] + assert "최종 확인일" not in needs_review_card.rsplit(" None: + complete_lab = doc( + "labs/azure-monitor/complete/index.md", + title="완전한 실습", + description="설명", + document_type="lab", + services=["azure-monitor"], + status="verified", + last_verified=date(2026, 2, 1), + estimated_time="30m", + cost="free", + cleanup_required=True, + verification_status="verified", + tags=[], + ) + sparse_lab = doc( + "labs/azure-monitor/sparse/index.md", + title="부족한 실습", + description="설명", + document_type="lab", + services=["azure-monitor"], + status="needs-review", + last_verified=None, + verification_status="needs-review", + tags=[], + ) + + pages = build_index_pages([complete_lab, sparse_lab], taxonomy) + lab_index = pages[PurePosixPath("labs/index.md")] + + assert "소요 시간: 30m" in lab_index + assert "예상 비용: free" in lab_index + assert "정리 절차: 필요" in lab_index + + sparse_card = lab_index.split("부족한 실습", 1)[1] + # No dg-doc-details block is fabricated when a lab is missing optional + # fields, and no fabricated last_verified date is shown either. + next_article_boundary = sparse_card.find("") + sparse_card_body = sparse_card[:next_article_boundary] + assert "dg-doc-details" not in sparse_card_body + assert "소요 시간" not in sparse_card_body + assert "최종 확인일" not in sparse_card_body + + +def test_research_card_uses_published_date_and_technologies(taxonomy: dict) -> None: + research = doc( + "research/azure-monitor/bench/index.md", + title="벤치마크", + description="설명", + document_type="research", + services=["azure-monitor"], + status="current", + published_at=date(2026, 7, 22), + verification_status="needs-review", + technologies=["python", "kubernetes"], + tags=[], + ) + + pages = build_index_pages([research], taxonomy) + research_index = pages[PurePosixPath("research/index.md")] + + assert "게시일: 2026-07-22" in research_index + assert "다룬 기술: Python, Kubernetes" in research_index + + +def test_service_label_falls_back_to_slug_when_taxonomy_omits_it(taxonomy: dict) -> None: + document = doc( + "guides/unlisted-service/topic/index.md", + title="목록에 없는 서비스", + description="설명", + document_type="guide", + services=["unlisted-service"], + status="current", + last_verified=date(2026, 1, 1), + applies_to=["AKS 1.34+"], + verification_status="verified", + tags=[], + ) + + pages = build_index_pages([document], taxonomy) + guide_index = pages[PurePosixPath("guides/index.md")] + + assert "## unlisted-service { #service-unlisted-service }" in guide_index + + +def test_tags_are_capped_at_three_and_labels_fall_back_to_slug(taxonomy: dict) -> None: + document = doc( + "guides/azure-monitor/many-tags/index.md", + title="태그 많은 가이드", + description="설명", + document_type="guide", + services=["azure-monitor"], + status="current", + last_verified=date(2026, 1, 1), + applies_to=["AKS 1.34+"], + verification_status="verified", + tags=["networking", "monitoring", "latency", "unlisted-tag"], + ) + + pages = build_index_pages([document], taxonomy) + guide_index = pages[PurePosixPath("guides/index.md")] + + tags_block = guide_index.split('class="dg-doc-tags"')[1].split("")[0] + assert tags_block.count('class="dg-tag"') == 3 + assert "unlisted-tag" not in guide_index.split('class="dg-doc-tags"')[1].split("")[0] + + +def test_title_and_description_punctuation_is_escaped_safely(taxonomy: dict) -> None: + document = doc( + "guides/azure-monitor/special/index.md", + title="A & B [의 사례] <위험>", + description='설명 "인용문" & ', + document_type="guide", + services=["azure-monitor"], + status="current", + last_verified=date(2026, 1, 1), + applies_to=["AKS 1.34+"], + verification_status="verified", + tags=[], + ) + + pages = build_index_pages([document], taxonomy) + guide_index = pages[PurePosixPath("guides/index.md")] + + # Description sits in raw (non-markdown) HTML, so it must be HTML-escaped + # and not double-escaped. + assert "<script>alert(1)</script>" in guide_index + assert "&lt;" not in guide_index + assert "