diff --git a/monitor/azure-sre-agent.md b/monitor/azure-sre-agent.md index 41fdb2ff..2d9b6036 100644 --- a/monitor/azure-sre-agent.md +++ b/monitor/azure-sre-agent.md @@ -20,7 +20,7 @@ Azure SRE Agent는 단순히 오류 로그를 나열하지 않습니다. 어떤 ## 인시던트가 발생하면 어떻게 조사하나요? -![경고가 발생하면 Azure SRE Agent가 인시던트를 확인하고 로그와 메트릭, 배포 이력을 조사해 근본 원인을 찾은 뒤 해결하거나 담당자에게 넘기는 흐름](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에는 해결 상태가 되어 작업 항목이 갱신되고 팀에 알림이 전달됩니다.](sre-agent-event-lab/assets/official/incident-response-flow.svg) > 출처: [인시던트 대응 자동화](https://learn.microsoft.com/azure/sre-agent/incident-response) @@ -73,6 +73,10 @@ 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) + +> 출처: [Azure 관찰 가능성으로 진단하기](https://learn.microsoft.com/azure/sre-agent/diagnose-azure-observability) + Azure 내부 원격 분석 데이터는 기본 도구만으로도 조회할 수 있습니다. 외부 시스템이나 특정 데이터 원본을 지속해서 사용해야 하는 경우에는 커넥터를 추가합니다. ## 과거 경험과 운영 문서는 어떻게 활용하나요? @@ -81,6 +85,10 @@ Azure 내부 원격 분석 데이터는 기본 도구만으로도 조회할 수 > 출처: [메모리와 지식 관리](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) + +> 출처: [메모리와 지식 관리](https://learn.microsoft.com/azure/sre-agent/memory) + ## 조사가 끝난 뒤 무엇을 학습하나요? ![조사가 끝나면 증상과 해결 단계, 근본 원인, 피해야 할 접근을 추출해 다음 조사에 사용할 수 있도록 학습하는 흐름](sre-agent-event-lab/assets/official/memory-auto-learning.svg) @@ -101,12 +109,22 @@ Azure 내부 원격 분석 데이터는 기본 도구만으로도 조회할 수 - Azure DevOps 저장소와 Work Item - Jira와 같은 관리형 커넥터(미리 보기) 또는 MCP 기반 티켓 시스템 +관리형 커넥터로 연결할 수 있는 대표적인 서비스는 다음과 같습니다. + +![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) + +> 출처: [관리형 커넥터(미리 보기)](https://learn.microsoft.com/azure/sre-agent/managed-connectors) + ### 알림과 협업 - Outlook 메일 - Microsoft Teams 채널 - Slack 채널 +![에이전트가 조사와 권고를 마친 뒤 이어지는 세 가지 대응 경로를 보여주는 다이어그램입니다. 첫째는 크기 조정과 다시 시작, 롤백 같은 조치를 Container Apps, 가상 머신, AKS 등 Azure 리소스에 직접 실행하는 경로이고, 둘째는 Azure DevOps, Jira, GitHub에 상황과 근거를 담은 작업 항목을 만드는 경로이며, 셋째는 Teams, Outlook, Slack 채널에 정리된 요약을 보내는 알림 경로입니다.](sre-agent-event-lab/assets/official/notification-paths.svg) + +> 출처: [조치 실행하기](https://learn.microsoft.com/azure/sre-agent/execute-mitigations) + ### 외부 관찰 도구 - Grafana @@ -134,6 +152,12 @@ Azure SRE Agent는 실행 수준에 따라 권한을 제어합니다. | 검토 모드(Review mode) | 조치 방안을 제안하고 담당자의 승인을 기다립니다. | | 자율 모드 | 허용된 작업을 별도 승인 없이 실행합니다. | +공식 기능인 검토 모드(Review)와 자율 모드(Autonomous)의 차이는 다음 그림에서도 확인할 수 있습니다. + +![검토 모드와 자율 모드를 위아래로 나누어 비교하는 다이어그램입니다. 위쪽 검토 모드에서는 에이전트가 조치를 제안하고 사용자가 승인하거나 거부한 뒤에야 실행되며, 아래쪽 자율 모드에서는 에이전트가 곧바로 실행해 작업을 스스로 완료합니다.](sre-agent-event-lab/assets/official/run-modes-comparison.svg) + +> 출처: [실행 모드](https://learn.microsoft.com/azure/sre-agent/run-modes) + 처음 도입할 때는 **검토 모드로 시작하는 것을 권장합니다.** 조사 결과와 도구 선택이 실제 운영 절차에 맞는지 충분히 확인한 뒤 자동화 범위를 넓히는 편이 안전합니다. 커넥터를 설정할 때도 최소 권한 원칙을 적용해야 합니다. @@ -144,6 +168,12 @@ Azure SRE Agent는 실행 수준에 따라 권한을 제어합니다. - 삭제나 변경 작업은 승인을 받도록 설정합니다. - 자율 모드에서는 일부 승인 절차가 생략될 수 있으므로 별도의 최소 권한 연결을 사용합니다. +관리 ID 권한이 부족할 때 담당자 승인으로 넘어가는 세부 과정은 다음 그림과 같습니다. + +![에이전트가 작업을 수행하기 전에 관리 ID에 필요한 Azure RBAC 역할이 있는지 확인하는 판단 흐름 다이어그램입니다. 역할이 있으면 관리 ID로 그대로 실행하고, 없으면 담당자 승인을 거쳐 사용자 위임 자격 증명을 요청한 뒤 서비스 다시 시작, 리소스 크기 조정, 로그 조회, 구성 변경, 메트릭 읽기 같은 작업을 수행합니다.](sre-agent-event-lab/assets/official/permission-flow.svg) + +> 출처: [권한](https://learn.microsoft.com/azure/sre-agent/permissions) + ## 팀에 맞게 어떻게 확장하나요? ![에이전트가 사용자 지정 스킬을 불러오고 스킬에 연결된 도구로 외부 API를 호출한 뒤 결과를 받아 조사에 활용하는 흐름](sre-agent-event-lab/assets/official/custom-skill-flow.svg) @@ -161,6 +191,10 @@ Azure SRE Agent는 실행 수준에 따라 권한을 제어합니다. 사용자 지정 에이전트는 각자 도구와 커넥터, 사용할 스킬을 따로 지정합니다. 조사 과정에서 다른 전문 에이전트로 작업을 넘기도록 구성할 수도 있어, 인시던트 분류와 상세 조사, 결과 전달을 단계별로 나눌 수 있습니다. +![포털의 캔버스 보기 화면입니다. 왼쪽에는 켜져 있는 PDtrigger 트리거가 있고, 가운데에는 자율 모드로 동작하며 지식 기반이 활성화된 AppErrorshandler 사용자 지정 에이전트가 연결되어 있으며, 오른쪽에는 GetAzCliHelp, RunAzCliReadCommands, RunAzCliWriteCommands, QueryAppInsightsByAppId, QueryAppInsightsByResourceId를 비롯한 도구 10개가 연결되어 있습니다.](sre-agent-event-lab/assets/official/portal-sub-agent-canvas-full.png) + +> 출처: [사용자 지정 에이전트](https://learn.microsoft.com/azure/sre-agent/sub-agents) + ## 인시던트를 담당자에게 어떻게 배분하나요? 대응 계획은 들어온 인시던트를 조건에 따라 적절한 사용자 지정 에이전트로 전달합니다. 다음 조건을 조합할 수 있습니다. @@ -174,6 +208,10 @@ Azure SRE Agent는 실행 수준에 따라 권한을 제어합니다. 인시던트 플랫폼을 처음 연결하면 빠른 시작 대응 계획이 자동으로 만들어집니다. 사용자 정의 대응 계획을 만든 뒤에는 이 계획을 삭제해야 인시던트가 잘못 전달되거나 두 번 처리되지 않습니다. +![인시던트 플랫폼에서 시작해 조치와 효과 측정까지 이어지는 전체 흐름을 보여주는 다이어그램입니다. PagerDuty나 Azure Monitor 같은 플랫폼이 경고를 보내면 필터와 실행 모드를 지정한 대응 계획이 조건에 맞는 항목을 골라 에이전트에게 넘기고, 에이전트가 자동으로 조사해 얻은 결과로 문제를 완화하고 원래 플랫폼의 상태를 갱신한 뒤 처리 결과를 지표로 추적합니다.](sre-agent-event-lab/assets/official/incident-platform-flow.svg) + +> 출처: [인시던트 플랫폼](https://learn.microsoft.com/azure/sre-agent/incident-platforms) + ## 제품에서 기본으로 지원하는 방식 제품의 표준 Azure Monitor 연계는 **Azure Monitor 인시던트 플랫폼 → 대응 계획 → Azure SRE Agent** 순서로 동작합니다. 이 방식에서는 Logic App과 같은 중간 연결이 필요하지 않습니다. @@ -372,7 +410,123 @@ Azure SRE Agent는 조사 도구를 격리된 샌드박스에서 실행하고, - Azure CLI 기반 작업에는 안전 장치가 있어 삭제 계열 명령과 키 자격 증명 모음 접근은 차단됩니다. - 읽기 전용으로 잠근 리소스는 변경하지 않습니다. -Log Analytics 작업 영역이나 AKS를 비공개 네트워크로만 노출한 환경이라면 에이전트의 네트워크 모드를 VNet 통합으로 설정해 사설 엔드포인트에 접근할 수 있습니다. 이 기능은 현재 미리 보기입니다. 공개 엔드포인트만 사용하는 환경에서는 별도 VNet 구성 없이도 조사할 수 있습니다. +Log Analytics 작업 영역이나 AKS를 비공개 네트워크로만 노출한 환경이라면 에이전트의 네트워크 모드를 VNet 통합으로 설정해 프라이빗 엔드포인트에 접근할 수 있습니다. 이 기능은 현재 미리 보기입니다. 공개 엔드포인트만 사용하는 환경에서는 별도 VNet 구성 없이도 조사할 수 있습니다. 자세한 구성 방법은 아래 '비공개 네트워크에는 어떻게 연결하나요?' 절에서 설명합니다. + +## 비공개 네트워크에는 어떻게 연결하나요? + +![작업 영역 구성 화면의 네트워킹 탭을 보여 주는 화면입니다. 제목 옆에는 미리 보기 배지가 있고, 송신 모드로 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) + +> 출처: [네트워크 통합](https://learn.microsoft.com/azure/sre-agent/network-integration) + +Azure SRE Agent가 비공개 네트워크에 있는 리소스에 접근하려면 **VNet 통합**(VNet integration)을 사용합니다. 이 기능의 공식 명칭은 VNet 통합이며, 흔히 쓰이는 표현인 'VNet 삽입(VNet injection)'은 공식 용어가 아니므로 안내나 문서에서 혼용하지 않는 것이 좋습니다. VNet 통합은 현재 미리 보기 상태입니다. + +### 네트워크 제어가 왜 필요한가요? + +기본값인 Unrestricted 모드에서는 에이전트가 인터넷의 모든 엔드포인트에 연결할 수 있습니다. 개발과 테스트 환경에서는 문제가 되지 않지만, 운영 환경에서는 다음 두 가지 위험이 남습니다. + +- **데이터 유출**(data exfiltration): 민감한 내부 데이터를 다루는 에이전트가 인터넷으로 자유롭게 나갈 수 있으면 조직 밖으로 데이터가 빠져나가는 경로가 생깁니다. +- **프롬프트 인젝션**(prompt injection): 공개 인터넷의 악의적인 내용은 에이전트의 판단을 조작하도록 만들어질 수 있으며, 외부 콘텐츠를 가져오는 에이전트는 그 응답을 통한 공격에 노출됩니다. + +VNet 통합을 적용하면 에이전트의 아웃바운드 호출이 조직의 네트워크 인프라를 지나갑니다. 그 결과 네트워크 로그에 기록되어 감사와 모니터링에 활용할 수 있고, 계층 4와 계층 7 방화벽 검사, 사용자 지정 DNS 구성, 기존 송신 규칙이 그대로 적용됩니다. 에이전트는 같은 네트워크의 다른 워크로드와 동일한 규칙 아래에서 동작합니다. + +### 세 가지 네트워크 제어 모드 + +| 모드 | 설명 | 이런 경우에 적합합니다 | +|---|---|---| +| Unrestricted(기본값) | 아웃바운드 트래픽 제한이 없습니다. | 개발, 테스트, 민감하지 않은 워크로드 | +| Limited | 허용 목록에 등록한 호스트로만 아웃바운드 연결을 제한합니다. | 전체 VNet 구성 없이 특정 대상만 제어하고 싶을 때 | +| Azure VNet | 플랫폼 트래픽을 제외한 아웃바운드 트래픽을 사용자의 VNet으로 라우팅합니다. | 감사 추적과 송신 제어가 필요한 운영 환경 | + +### 사전 준비 사항 + +- 에이전트와 같은 리전에 있는 전용 서브넷이 필요합니다. +- 서브넷 크기는 **/28** 이상이어야 하며, 여러 에이전트를 함께 운영하거나 트래픽이 몰릴 때를 대비하려면 **/26**까지 넉넉하게 확보하는 것을 권장합니다. +- 서브넷은 **Microsoft.App/environments**에 위임(delegate)되어 있어야 합니다. +- 서브넷을 연결할 담당자에게는 대상 서브넷에 대한 **Network Contributor** 역할(또는 `Microsoft.Network/virtualNetworks/subnets/join/action` 권한을 포함하는 동급 역할)이 필요합니다. +- 에이전트 리소스에는 **SRE Agent Administrator** 역할이 필요합니다. +- 서브넷은 다른 서비스와 공유할 수 없으며 에이전트 전용으로 사용해야 합니다. + +### 트래픽 경로는 대상마다 다릅니다 + +| 트래픽 대상 | 실제 경로 | 설정 가능 여부 | +|---|---|---| +| Azure 리소스(Log Analytics, Application Insights, AKS, 데이터베이스, Key Vault 등) | 사용자의 VNet | 기본적으로 VNet을 통과합니다. | +| 온프레미스 시스템(ExpressRoute 또는 VPN으로 연결) | 사용자의 VNet | 네트워크 경로가 허용하면 접근할 수 있습니다. | +| 플랫폼 서비스(오케스트레이션, 모델 엔드포인트, 원격 분석) | Azure SRE Agent 인프라 네트워크 | 변경할 수 없으며 항상 관리형 인프라를 거칩니다. | +| 패키지 레지스트리(PyPI, npm, NuGet, apt 등) | 토글을 켜면 인프라 네트워크, 끄면 VNet의 FQDN 방화벽 규칙 | 레지스트리별로 개별 설정할 수 있습니다. | +| 코드 저장소(GitHub, GitHub Enterprise, Azure DevOps) | 토글을 켜면 인프라 네트워크, 끄면 VNet의 FQDN 방화벽 규칙 | 공급자별로 개별 설정할 수 있습니다. | +| 원격 MCP 서버 | 토글을 켜면 인프라 네트워크, 끄면 VNet의 FQDN 방화벽 규칙 | 단일 토글로 설정합니다. | +| 추가 호스트 목록에 등록한 호스트 | 인프라 네트워크 | 호스트 이름이나 `*.example.com` 같은 와일드카드 패턴을 직접 입력해 관리합니다. 구성한 패키지와 커넥터는 각자 사용하는 호스트를 자동으로 허용하므로, 목록에는 레지스트리와 코드 저장소 밖의 대상만 등록하면 됩니다. | +| 커넥터 트래픽(관리형 커넥터 포함) | 공개 인터넷 | 변경할 수 없습니다. 미리 보기 기간에는 VNet을 거치지 않고 공개 인터넷 경로로 전송됩니다. | +| 프라이빗 엔드포인트를 통한 인바운드 연결 | 지원하지 않음 | 이번 미리 보기에서는 아웃바운드 방향만 제어합니다. | + +### 설정 절차 + +1. 에이전트 포털에서 **설정 > 작업 영역 구성 > 네트워킹** 탭을 엽니다. +2. 네트워크 제어 모드로 **Azure VNet**을 선택합니다. +3. 서브넷 찾아보기에서 구독, 리소스 그룹, 가상 네트워크, 서브넷을 순서대로 선택하고 연결합니다. +4. 연결하면 **Use VNet's private DNS** 옵션이 자동으로 켜집니다. 이 옵션을 사용하면 에이전트가 프라이빗 엔드포인트 호스트 이름을 확인할 수 있습니다. +5. 프라이빗 엔드포인트 확인이 정상적으로 동작하려면 VNet에 관련 Azure Private DNS 영역을 연결해야 합니다. Log Analytics에는 `privatelink.ods.opinsights.azure.com`, Key Vault에는 `privatelink.vaultcore.azure.net` 영역을 연결하세요. DNS를 구성하지 않으면 에이전트가 공개 엔드포인트로 돌아가거나 연결에 실패할 수 있습니다. +6. 인프라 네트워크 토글에서 원격 MCP 서버, 패키지 레지스트리, 코드 저장소, 추가 호스트 중 공개 인터넷 경로를 허용할 항목을 선택합니다. +7. 저장한 뒤 공개 접근을 차단한 프라이빗 엔드포인트 리소스(예: 공개 접근을 비활성화한 Log Analytics 작업 영역)를 조사하도록 요청해 실제로 VNet 경로로 연결되는지 확인하세요. + +VNet을 연결한 뒤에 구성을 바꿀 때는 다음 순서를 지켜야 합니다. + +- 송신 모드를 바꾸려면 먼저 VNet 연결을 해제해야 합니다. VNet이 연결되어 있는 동안에는 Unrestricted와 Limited 모드 카드가 비활성화되어 선택할 수 없습니다. +- 서브넷을 바꿀 때도 VNet 연결을 해제한 뒤 새 서브넷으로 다시 연결해야 합니다. + +### 인프라 네트워크 토글은 과도기 수단입니다 + +GitHub는 Azure 서비스가 아니어서 서비스 태그를 제공하지 않고, PyPI와 npm, NuGet, 컨테이너 레지스트리처럼 널리 쓰이는 공개 서비스도 전 세계에 걸친 IP 대역이 자주 바뀝니다. 계층 4 IP 기반 방화벽만으로는 이런 대상의 주소 목록을 최신 상태로 유지하기 어렵고, 목록이 오래되면 에이전트가 필요한 서비스에 도달하지 못합니다. 인프라 네트워크 토글은 이런 대상에 플랫폼 송신 경로로 도달하도록 열어 주는 설정입니다. + +다만 이 토글은 호스트 이름이나 FQDN을 인식하는 송신 필터링을 대신하지 않습니다. FQDN 규칙을 지원하는 Azure Firewall Premium이나 TLS 검사를 지원하는 네트워크 가상 어플라이언스로 옮겨 가는 동안 한시적으로 사용하는 과도기 수단으로 다루고, 네트워크 팀이 방화벽 규칙을 갱신하면 열어 둔 범위를 다시 좁히세요. + +### 패키지 레지스트리 대신 미리 설치를 사용할 수 있습니다 + +에이전트가 조사 중에 실행하는 스크립트가 특정 패키지를 필요로 한다면, 패키지 레지스트리 토글을 켜서 인프라 네트워크로 우회하는 대신 샌드박스 기본 이미지에 필요한 패키지를 미리 설치할 수 있습니다. 이렇게 하면 실행할 때마다 패키지가 준비된 상태로 제공되므로 공개 레지스트리로 나가는 경로를 열지 않아도 됩니다. + +설정 방법은 다음과 같습니다. + +1. 에이전트 포털에서 **설정 > 작업 영역 구성**을 열고 **Packages** 탭을 선택합니다. +2. 패키지 이름을 입력하고 패키지 관리자로 `pip` 또는 `NuGet`을 선택한 뒤 필요하면 버전을 지정합니다. +3. **패키지 추가**를 선택합니다. + +`NuGet` 항목에는 `dotnet-ef`처럼 .NET CLI 도구만 등록할 수 있으며, 라이브러리 패키지는 전역으로 설치할 수 없습니다. + +### 방화벽에서 허용해야 하는 도메인 + +에이전트 포털과 조사 기능을 사용하려면 사내 방화벽과 프록시에서 다음 도메인을 HTTP와 WebSocket 트래픽 모두에 대해 허용해야 합니다. + +| 도메인 | 용도 | +|---|---| +| `*.azuresre.ai` | 에이전트 포털과 API, 실시간 대화(WebSocket) | +| `sre.azure.com` | 에이전트 관리 포털 | +| `portal.azure.com` | Azure Portal(모니터, 로그, 관리 ID 확인) | +| `api.applicationinsights.io` | Application Insights 쿼리 API | +| `api.loganalytics.io`, `api.loganalytics.azure.com` | Log Analytics 쿼리 API | +| `*.ods.opinsights.azure.com` | Log Analytics 작업 영역 데이터 수집 | +| `management.azure.com` | Azure Resource Manager API | +| `login.microsoftonline.com`, `*.login.microsoft.com` | Microsoft Entra ID 인증 | + +Zscaler를 비롯한 일부 사내 프록시는 `*.azuresre.ai`를 기본적으로 차단합니다. 포털이 열리지 않거나 대화 화면이 응답하지 않는다면 이 도메인을 허용 목록에 추가하고, 프록시가 WebSocket 연결을 막고 있지 않은지 확인하세요. 브라우저 확장 프로그램의 영향을 배제하려면 시크릿 창에서도 확인해 보세요. + +### 네트워크가 호출을 차단하면 어떻게 되나요? + +NSG 규칙에 막히거나 경로가 없어 아웃바운드 요청이 거부되면, 에이전트는 같은 서브넷의 다른 워크로드가 받는 것과 똑같은 네트워크 오류를 받습니다. 이때 에이전트는 실패한 사실을 조사 결과에 그대로 남기고(예: Log Analytics 작업 영역에 연결하지 못하고 시간이 초과됐다는 메시지) 접근할 수 있는 도구와 데이터로 조사를 이어 갑니다. 꼭 필요한 데이터 원본에 접근하지 못하면 조사 결과가 완전하지 않다는 사실도 함께 알려 줍니다. + +### 알아두어야 할 제한 사항과 거버넌스 + +- VNet 통합은 아웃바운드 트래픽만 제어하며, 프라이빗 엔드포인트를 통한 인바운드 연결은 이번 미리 보기에서 지원하지 않습니다. +- Azure Policy를 적용해 인프라 네트워크 토글 자체를 제한하거나 비활성화할 수 있어 담당자가 임의로 VNet 밖으로 트래픽을 우회하지 못하도록 통제할 수 있습니다. +- 네트워크 제어 설정을 변경할 수 있는 권한은 SRE Agent Administrator 역할을 가진 사용자로 제한됩니다. 관리 ID, 사용자 위임 자격 증명, Azure RBAC 권한과 함께 네트워크 구성을 묶어서 거버넌스를 설계하세요. +- Key Vault 방화벽 허용은 VNet 통합과 별개의 절차입니다. 인증서 기반 커넥터가 Key Vault에 접근해야 한다면 에이전트 포털의 설정 화면에서 아웃바운드 IP 주소를 확인한 뒤 Key Vault 방화벽의 허용 목록에 각 IP 주소를 개별적으로 추가해야 합니다. + +자세한 절차는 다음 자료를 참고하세요. + +- [네트워크 통합](https://learn.microsoft.com/azure/sre-agent/network-integration) +- [네트워크 제어 구성](https://learn.microsoft.com/azure/sre-agent/configure-network-controls) +- [네트워크 요구 사항](https://learn.microsoft.com/azure/sre-agent/network-requirements) +- [Key Vault 방화벽 허용 목록 구성](https://learn.microsoft.com/azure/sre-agent/allow-list-key-vault-firewall) ## 인시던트 대응 외에 무엇을 자동화할 수 있나요? @@ -402,6 +556,12 @@ Agent Hooks를 사용하면 에이전트가 결과를 반환하기 직전이나 또한 인시던트 지표 화면에서 처리한 인시던트 수, 에이전트가 완화한 비율, 담당자가 처리한 비율, 절감한 시간, 근본 원인 분포를 확인할 수 있습니다. 도입 효과를 정량적으로 설명해야 할 때 활용하기 좋습니다. +이 지표는 왼쪽 사이드바의 Operations Hub 개요 탭에서 한 화면으로 확인할 수 있습니다. + +![Operations Hub 개요 탭 화면입니다. 왼쪽 사이드바에서는 Operations Hub 항목이 선택되어 강조되어 있고 그 아래로 인시던트, 예약 작업, Builder, 모니터, 기능, 설정 메뉴가 이어집니다. 오른쪽 본문에는 개요와 인시던트 분석, 자동화 탭이 있고 기간은 최근 7일로 지정되어 있으며, 원본별 일일 처리량을 선택한 차트 영역에는 값이 그려지지 않고 선택한 기간에 표시할 데이터가 없다는 안내만 나타납니다. 그 아래에는 대기 중인 작업 41건 카드와 실시간 시스템 상태 카드가 놓여 주요 지표를 한 화면에서 확인할 수 있습니다.](sre-agent-event-lab/assets/official/operations-hub-overview-tab.png) + +> 출처: [Operations Hub](https://learn.microsoft.com/azure/sre-agent/operations-hub) + ## 도입 전에 무엇을 확인해야 하나요? ### 조사 범위 @@ -451,7 +611,11 @@ Agent Hooks를 사용하면 에이전트가 결과를 반환하기 직전이나 - [Azure SRE Agent 개요](https://learn.microsoft.com/azure/sre-agent/overview) - [인시던트 대응 설정](https://learn.microsoft.com/azure/sre-agent/tutorial-incident-response) - [인시던트 대응 계획](https://learn.microsoft.com/azure/sre-agent/incident-response-plans) +- [인시던트 플랫폼](https://learn.microsoft.com/azure/sre-agent/incident-platforms) - [근본 원인 분석](https://learn.microsoft.com/azure/sre-agent/root-cause-analysis) +- [Azure 관찰 가능성으로 진단하기](https://learn.microsoft.com/azure/sre-agent/diagnose-azure-observability) +- [조치 실행하기](https://learn.microsoft.com/azure/sre-agent/execute-mitigations) +- [실행 모드](https://learn.microsoft.com/azure/sre-agent/run-modes) - [심층 조사](https://learn.microsoft.com/azure/sre-agent/deep-investigation) - [팀 온보딩](https://learn.microsoft.com/azure/sre-agent/team-onboard) - [사용자 지정 에이전트](https://learn.microsoft.com/azure/sre-agent/sub-agents) @@ -464,6 +628,7 @@ Agent Hooks를 사용하면 에이전트가 결과를 반환하기 직전이나 - [가격과 청구](https://learn.microsoft.com/azure/sre-agent/pricing-billing) - [예약 작업](https://learn.microsoft.com/azure/sre-agent/scheduled-tasks) - [작업 감사](https://learn.microsoft.com/azure/sre-agent/audit-agent-actions) +- [Operations Hub](https://learn.microsoft.com/azure/sre-agent/operations-hub) - [인시던트 가치 추적](https://learn.microsoft.com/azure/sre-agent/track-incident-value) ### 보안과 확장 @@ -471,6 +636,9 @@ Agent Hooks를 사용하면 에이전트가 결과를 반환하기 직전이나 - [보안 개요](https://learn.microsoft.com/azure/sre-agent/security-overview) - [권한](https://learn.microsoft.com/azure/sre-agent/permissions) - [네트워크 통합](https://learn.microsoft.com/azure/sre-agent/network-integration) +- [네트워크 제어 구성](https://learn.microsoft.com/azure/sre-agent/configure-network-controls) +- [네트워크 요구 사항](https://learn.microsoft.com/azure/sre-agent/network-requirements) +- [Key Vault 방화벽 허용 목록 구성](https://learn.microsoft.com/azure/sre-agent/allow-list-key-vault-firewall) - [데이터 보존과 개인 정보](https://learn.microsoft.com/azure/sre-agent/data-privacy) - [Agent Hooks](https://learn.microsoft.com/azure/sre-agent/agent-hooks) - [커넥터](https://learn.microsoft.com/azure/sre-agent/connectors) diff --git a/monitor/sre-agent-event-lab/assets/official/azure-sre-agent-networking-vnet.png b/monitor/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/monitor/sre-agent-event-lab/assets/official/azure-sre-agent-networking-vnet.png differ diff --git a/monitor/sre-agent-event-lab/assets/official/diagnose-azure-services.svg b/monitor/sre-agent-event-lab/assets/official/diagnose-azure-services.svg new file mode 100644 index 00000000..e5b51c5b --- /dev/null +++ b/monitor/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/monitor/sre-agent-event-lab/assets/official/incident-platform-flow.svg b/monitor/sre-agent-event-lab/assets/official/incident-platform-flow.svg new file mode 100644 index 00000000..022541a4 --- /dev/null +++ b/monitor/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/monitor/sre-agent-event-lab/assets/official/knowledge-sources.svg b/monitor/sre-agent-event-lab/assets/official/knowledge-sources.svg new file mode 100644 index 00000000..a9d35f47 --- /dev/null +++ b/monitor/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/monitor/sre-agent-event-lab/assets/official/managed-connectors-icon-grid.png b/monitor/sre-agent-event-lab/assets/official/managed-connectors-icon-grid.png new file mode 100644 index 00000000..b8750f79 Binary files /dev/null and b/monitor/sre-agent-event-lab/assets/official/managed-connectors-icon-grid.png differ diff --git a/monitor/sre-agent-event-lab/assets/official/notification-paths.svg b/monitor/sre-agent-event-lab/assets/official/notification-paths.svg new file mode 100644 index 00000000..188c09bc --- /dev/null +++ b/monitor/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/monitor/sre-agent-event-lab/assets/official/operations-hub-overview-tab.png b/monitor/sre-agent-event-lab/assets/official/operations-hub-overview-tab.png new file mode 100644 index 00000000..f91fb636 Binary files /dev/null and b/monitor/sre-agent-event-lab/assets/official/operations-hub-overview-tab.png differ diff --git a/monitor/sre-agent-event-lab/assets/official/permission-flow.svg b/monitor/sre-agent-event-lab/assets/official/permission-flow.svg new file mode 100644 index 00000000..f030d35a --- /dev/null +++ b/monitor/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/monitor/sre-agent-event-lab/assets/official/portal-sub-agent-canvas-full.png b/monitor/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/monitor/sre-agent-event-lab/assets/official/portal-sub-agent-canvas-full.png differ diff --git a/monitor/sre-agent-event-lab/assets/official/run-modes-comparison.svg b/monitor/sre-agent-event-lab/assets/official/run-modes-comparison.svg new file mode 100644 index 00000000..98f89736 --- /dev/null +++ b/monitor/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/scripts/tests/test_briefing_docs.py b/monitor/sre-agent-event-lab/scripts/tests/test_briefing_docs.py index e4a5a383..ffff4452 100644 --- a/monitor/sre-agent-event-lab/scripts/tests/test_briefing_docs.py +++ b/monitor/sre-agent-event-lab/scripts/tests/test_briefing_docs.py @@ -1,17 +1,193 @@ import re +import unicodedata +from collections import Counter from pathlib import Path REPO_ROOT = Path(__file__).parents[4] BRIEFING = REPO_ROOT / "monitor" / "azure-sre-agent.md" -OFFICIAL_ASSETS = { +OFFICIAL_SVGS = { "incident-response-flow.svg", "root-cause-analysis.svg", "agent-reasoning-flow.svg", "memory-unified-search.svg", "memory-auto-learning.svg", "custom-skill-flow.svg", + "diagnose-azure-services.svg", + "notification-paths.svg", + "incident-platform-flow.svg", + "knowledge-sources.svg", + "run-modes-comparison.svg", + "permission-flow.svg", } +OFFICIAL_PNGS = { + "azure-sre-agent-networking-vnet.png", + "portal-sub-agent-canvas-full.png", + "managed-connectors-icon-grid.png", + "operations-hub-overview-tab.png", +} +OFFICIAL_ASSETS = OFFICIAL_SVGS | OFFICIAL_PNGS +OFFICIAL_ASSET_PREFIX = "sre-agent-event-lab/assets/official/" + + +def windows_after(text: str, anchor: str, size: int = 500) -> list: + """Return a text window following each occurrence of `anchor`. + + Used to check that groups of related keywords appear close together + (i.e. in the same sentence/paragraph), without pinning the test to one + exact, invented sentence. + """ + windows = [] + start = 0 + while True: + idx = text.find(anchor, start) + if idx == -1: + break + windows.append(text[idx : idx + size]) + start = idx + 1 + return windows + + +UNICODE_PUNCTUATION_CATEGORIES = {"Pc", "Pd", "Pe", "Pf", "Pi", "Po", "Ps"} + + +def strip_code(markdown: str) -> str: + markdown = re.sub(r"```.*?```", "", markdown, flags=re.DOTALL) + return re.sub(r"`[^`]*`", "", markdown) + + +def _is_unicode_punctuation(char) -> bool: + return char is not None and unicodedata.category(char) in UNICODE_PUNCTUATION_CATEGORIES + + +def _is_unicode_whitespace(char) -> bool: + return char is None or char.isspace() + + +_THEMATIC_BREAK = re.compile(r" {0,3}(?:\*[ \t]*){3,}") + + +def _is_list_bullet_marker(line: str, match) -> bool: + """True when `match` is a CommonMark ``*`` list bullet, not a delimiter run. + + A single ``*`` starts a list item (and is never an emphasis delimiter) + when it sits at up to 3 spaces of line indentation and is followed by + whitespace or the end of the line — e.g. ``* 항목`` or `` * item``. + """ + if match.group() != "*": + return False + if not re.fullmatch(r" {0,3}", line[: match.start()]): + return False + after = line[match.end() : match.end() + 1] + return after in ("", " ", "\t") + + +def unrendered_emphasis_markers(markdown: str) -> list: + """Emphasis runs GitHub prints literally instead of rendering as bold/italic. + + Implements the CommonMark left/right-flanking delimiter rules. A closing + ``**`` that follows punctuation and is glued to a letter — the common + Korean pattern ``**굵게(bold)**을`` where a particle follows the marker — + is not right-flanking, so it can never close emphasis and GitHub shows the + raw asterisks to the reader. This is a general rule check, not a check for + one known sentence. + + Block-level ``*`` markers — a list bullet (``* 항목``) or a thematic break + line (``***``, ``* * *``) — are CommonMark structure, not inline emphasis + delimiters, so they are excluded from the scan before it runs. + """ + offenders = [] + for number, line in enumerate(strip_code(markdown).splitlines(), start=1): + if _THEMATIC_BREAK.fullmatch(line): + continue + open_runs = [] + for match in re.finditer(r"\*+", line): + if _is_list_bullet_marker(line, match): + continue + before = line[match.start() - 1] if match.start() else None + after = line[match.end()] if match.end() < len(line) else None + left_flanking = not _is_unicode_whitespace(after) and ( + not _is_unicode_punctuation(after) + or _is_unicode_whitespace(before) + or _is_unicode_punctuation(before) + ) + right_flanking = not _is_unicode_whitespace(before) and ( + not _is_unicode_punctuation(before) + or _is_unicode_whitespace(after) + or _is_unicode_punctuation(after) + ) + if right_flanking and open_runs: + open_runs.pop() + elif left_flanking: + open_runs.append(match) + else: + offenders.append((number, match.group(), line.strip())) + offenders.extend((number, run.group(), line.strip()) for run in open_runs) + return offenders + + +def emphasis_glued_to_hangul(markdown: str) -> list: + """Closing ``**`` markers immediately followed by a Hangul letter/particle.""" + pattern = re.compile(r"[^\s*]\*{1,2}[가-힣]") + return [ + match.group() + for match in pattern.finditer(strip_code(markdown)) + if _is_unicode_punctuation(match.group()[0]) + ] + + +def table_rows(markdown: str) -> list: + rows = [] + for line in strip_code(markdown).splitlines(): + line = line.strip() + if not line.startswith("|") or not line.endswith("|"): + continue + if re.fullmatch(r"\|[\s:\-|]+\|", line): + continue + cells = [ + re.sub(r"\[([^\]]+)\]\([^)]*\)", r"\1", cell).replace("**", "").strip() + for cell in line.strip("|").split("|") + ] + if len(cells) >= 2: + rows.append(cells) + return rows + + +def body_sentences(markdown: str, minimum_length: int = 30) -> list: + """Substantive body sentences, with images, captions and code removed. + + Table cells count as body prose, so a sentence copied between a table row + and a bullet list is detected. Short labels and fragments are ignored so + that legitimately repeated table values do not register as duplicates. + """ + markdown = re.sub(r"```.*?```", "", markdown, flags=re.DOTALL) + markdown = re.sub(r"!\[[^\]]*\]\([^)]*\)", "", markdown) + 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"^\[[ x]\]\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 prose_only(markdown: str) -> str: @@ -53,6 +229,75 @@ def test_briefing_uses_natural_korean_terms(): assert not re.search(pattern, prose, re.IGNORECASE), pattern +def test_briefing_has_no_emphasis_markers_github_renders_literally(): + text = BRIEFING.read_text() + + assert unrendered_emphasis_markers(text) == [], unrendered_emphasis_markers(text) + assert emphasis_glued_to_hangul(text) == [], emphasis_glued_to_hangul(text) + + +def test_emphasis_lint_catches_markers_that_commonmark_leaves_literal(): + broken = "이 기능은 **VNet 통합(VNet integration)**을 사용합니다." + fixed = "이 기능은 **VNet 통합**(VNet integration)을 사용합니다." + + assert unrendered_emphasis_markers(broken), "lint must flag unclosable markers" + assert emphasis_glued_to_hangul(broken), "lint must flag Hangul-glued markers" + assert unrendered_emphasis_markers(fixed) == [] + assert emphasis_glued_to_hangul(fixed) == [] + assert unrendered_emphasis_markers("서브넷은 **/28** 이상이어야 합니다.") == [] + assert unrendered_emphasis_markers("**Azure VNet**을 선택합니다.") == [] + + +def test_emphasis_lint_ignores_block_level_star_markers(): + """CommonMark block-level `*` markers are not emphasis delimiters. + + A `*` that starts a list item, or a line of 3+ `*` that forms a + thematic break, must never be reported as an unclosable emphasis + delimiter even though it is neither left- nor right-flanking under the + inline delimiter-run rules. + """ + assert unrendered_emphasis_markers("* 항목") == [] + assert unrendered_emphasis_markers(" * item") == [] + assert unrendered_emphasis_markers("* item") == [] + assert unrendered_emphasis_markers("***") == [] + assert unrendered_emphasis_markers("* * *") == [] + + +def test_briefing_does_not_repeat_substantive_body_sentences(): + sentences = body_sentences(BRIEFING.read_text()) + counts = Counter(sentences) + + assert len(sentences) >= 100, "duplicate check must inspect the whole briefing" + repeated = sorted(sentence for sentence, count in counts.items() if count > 1) + assert repeated == [], repeated + + +def test_briefing_tables_do_not_repeat_row_labels_in_descriptions(): + offenders = [ + row + for row in table_rows(BRIEFING.read_text()) + if len(row[0]) >= 8 and row[0] in row[-1] + ] + + assert offenders == [], offenders + + +def test_briefing_uses_consistent_private_network_terms(): + text = BRIEFING.read_text() + + assert "비공개 네트워크" in text + assert "프라이빗 엔드포인트" in text + for inconsistent in ( + "사설 네트워크", + "사설 엔드포인트", + "개인 엔드포인트", + "전용 엔드포인트", + "비공개 엔드포인트", + "프라이빗 네트워크", + ): + assert inconsistent not in text, inconsistent + + def test_briefing_uses_customer_facing_honorific_style(): prose = prose_only(BRIEFING.read_text()) explanatory_lines = [ @@ -118,9 +363,20 @@ def test_briefing_covers_adoption_critical_product_topics(): assert source in text, source +def rendered_image_targets(text: str) -> list: + """Every image target the briefing actually renders. + + Covers Markdown image syntax and any raw HTML `` fallback, so a file + can never be shown to a reader without its path being verified. + """ + markdown_targets = re.findall(r"!\[[^\]]*\]\(([^)]+)\)", text) + html_targets = re.findall(r"]*\ssrc=[\"']([^\"']+)[\"']", text) + return markdown_targets + html_targets + + def test_briefing_uses_only_local_images_and_no_storyboards(): text = BRIEFING.read_text() - image_targets = re.findall(r"!\[[^\]]*\]\(([^)]+)\)", text) + image_targets = rendered_image_targets(text) assert image_targets assert all(not target.startswith(("http://", "https://")) for target in image_targets) @@ -130,6 +386,23 @@ def test_briefing_uses_only_local_images_and_no_storyboards(): assert (BRIEFING.parent / target).resolve().exists(), target +def test_every_official_asset_is_rendered_as_a_markdown_image(): + text = BRIEFING.read_text() + markdown_targets = re.findall(r"!\[[^\]]*\]\(([^)]+)\)", text) + rendered_official = { + target.rsplit("/", 1)[-1] + for target in markdown_targets + if "assets/official/" in target + } + + assert rendered_official == OFFICIAL_ASSETS, sorted( + OFFICIAL_ASSETS - rendered_official + ) + assert " dict[str, str]: +def official_image_alt_texts() -> dict: pattern = re.compile(r"!\[([^\]]*)\]\(([^)]*assets/official/([^)]+))\)") return { match.group(3): match.group(1) @@ -187,14 +544,70 @@ def official_image_alt_texts() -> dict[str, str]: def test_official_images_describe_themselves_in_alt_text(): alt_texts = official_image_alt_texts() - assert set(alt_texts) == set(OFFICIAL_IMAGE_ALT_KEYWORDS) + assert set(OFFICIAL_IMAGE_ALT_KEYWORDS) == OFFICIAL_ASSETS + assert set(alt_texts) == OFFICIAL_ASSETS for name, keywords in OFFICIAL_IMAGE_ALT_KEYWORDS.items(): alt = alt_texts[name] + assert alt.strip(), name assert len(alt) >= 40, (name, alt) + assert re.search(r"[가-힣]", alt), (name, alt) for keyword in keywords: assert keyword in alt, (name, keyword) +def test_operations_hub_alt_text_matches_the_captured_screen_state(): + alt = official_image_alt_texts()["operations-hub-overview-tab.png"] + + assert re.search(r"Operations Hub[^.]{0,40}(선택|강조)", alt), alt + assert any( + phrase in alt for phrase in ("선택한 기간", "선택된 기간") + ), alt + assert re.search(r"(표시할 )?데이터가 없", alt), alt + + +def test_incident_response_flow_alt_text_follows_the_diagram_timeline(): + alt = official_image_alt_texts()["incident-response-flow.svg"] + + positions = [alt.find(mark) for mark in ("0:00", "0:30", "1:30", "3:00", "5:00")] + assert all(position >= 0 for position in positions), alt + assert positions == sorted(positions), "timeline must be described in order" + assert re.search(r"(제안|권고)", alt), alt + assert "자율" in alt, alt + assert re.search(r"(작업 항목|티켓)", alt), alt + + +def test_every_rendered_image_has_descriptive_korean_alt_text(): + pattern = re.compile(r"!\[([^\]]*)\]\(([^)]+)\)") + images = pattern.findall(BRIEFING.read_text()) + + assert len(images) == 21, len(images) + for alt, target in images: + assert alt.strip(), target + assert re.search(r"[가-힣]", alt), target + + +def test_image_alt_text_is_not_repeated_as_body_prose(): + text = BRIEFING.read_text() + body = re.sub(r"\s+", " ", re.sub(r"!\[[^\]]*\]\([^)]*\)", "", text)) + + repeated = [] + for alt in re.findall(r"!\[([^\]]*)\]\([^)]*\)", text): + for sentence in body_sentences(alt): + if sentence in body: + repeated.append(sentence) + + assert repeated == [], repeated + + +def test_official_image_alt_text_does_not_claim_absent_content(): + alt_texts = official_image_alt_texts() + + for name, forbidden in FORBIDDEN_ALT_CLAIMS.items(): + alt = alt_texts[name] + for claim in forbidden: + assert claim not in alt, (name, claim) + + def test_official_images_do_not_repeat_alt_text_as_body_prose(): text = BRIEFING.read_text() @@ -210,6 +623,9 @@ def test_official_images_do_not_repeat_alt_text_as_body_prose(): "Azure SRE Agent는 과거 조사 대화, 사용자가 기억하도록 지정한 내용", "에이전트는 이 흐름을 따라 판단합니다.", "조사가 완료되면 Azure SRE Agent는 확인한 증상", + "업로드한 운영 문서와 MCP로 연결한 외부 지식 원본도 같은 검색 범위에 포함됩니다.", + "조사를 마친 뒤 에이전트가 선택하는 다음 동작은 조치 실행, 작업 항목 생성, 담당자 알림 세 갈래로 나뉩니다.", + "사용자 지정 에이전트는 포털의 Agent Canvas 화면에서 만들고 연결합니다.", ): assert marker not in text, marker @@ -255,6 +671,19 @@ def test_briefing_marks_preview_capabilities(): assert "미리 보기" in managed +def test_official_asset_set_has_16_selected_files(): + asset_dir = ( + REPO_ROOT + / "monitor" + / "sre-agent-event-lab" + / "assets" + / "official" + ) + + assert len(OFFICIAL_ASSETS) == 16 + assert {path.name for path in asset_dir.glob("*")} == OFFICIAL_ASSETS + + def test_official_sre_agent_svgs_are_stored_locally(): asset_dir = ( REPO_ROOT @@ -264,8 +693,203 @@ def test_official_sre_agent_svgs_are_stored_locally(): / "official" ) - assert {path.name for path in asset_dir.glob("*.svg")} == OFFICIAL_ASSETS - for name in OFFICIAL_ASSETS: + assert {path.name for path in asset_dir.glob("*.svg")} == OFFICIAL_SVGS + for name in OFFICIAL_SVGS: svg = (asset_dir / name).read_text() assert "