内容
# DevOps 代理 - 通过自定义 MCP 进行网络诊断
一个可部署的演示,其中 **AWS DevOps 代理** 通过调用运行在 EC2 探测主机上的 **自定义 MCP 服务器**,自主排查 VPC 网络问题(DNS、可达性、端口阻塞),并通过 **SSM** 运行 `dig`、`nc`、`ping` 和 `traceroute` 命令 - 返回结构化的发现结果,供代理推理和链接。
它有两个互补的部分:
- **实时活动探测** (`resolve_dns`、`tcp_reachability`、`ping_host`、`traceroute_host`) - 对当前发生的问题具有权威性。
- **历史/法医证据** (`flow_log_query`) - 通过 Athena 查询 **VPC 流日志**,回答“过去一段时间内,流量是 ACCEPT 还是 REJECT,来自谁,在哪个端口,自什么时候” - 唯一一种调查已经解决或间歇性事件的方法,而实时探测无法察觉。
一个通用的 **DevOps 代理技能** 教导代理 **如何** 进行调查 - 选择哪一部分(实时 vs 法医)以及如何读取判决结果。
## 为什么存在
DevOps 代理 **设计上是只读的**。其权限防护栏阻止 `ssm:SendCommand`,即使您在调查角色上授予它,您也不能简单地让代理运行网络探测。AWS 批准的路径是 **自定义 MCP 服务器**:防护栏不控制 MCP 工具。工具路由器 Lambda 持有 SSM 权限,并中介一个固定的命令集,返回结构化的 JSON。
## 架构

代理通过 **自定义 MCP 服务器**(只读防护栏阻止代理角色上的 `ssm:SendCommand`,但不控制 MCP 工具)访问诊断。工具路由器 Lambda 持有 SSM 权限,并在单个标记的探测主机上运行一个固定的命令集,解析原始标准输出为结构化的 JSON。对于 `flow_log_query`,同一个 Lambda 在 VPC 流日志(S3 上的 Glue 表)上运行一个固定的、分区修剪的、仅聚合的 Athena 查询,返回一个简洁的判决结果 - 从不返回原始行。
四个堆栈(都在一个账户/区域中,例如 `us-east-1`):
| 堆栈 | 内容 |
|-------|----------|
| `NetDiagProbeStack` | VPC、探测主机(工具)、目标主机(nginx :80/:5432)、Route53 私有区 `db.corp.internal` |
| `NetDiagFlowLogsStack` | VPC 流日志(自定义格式)-> S3、Glue 数据库/表,具有分区投影,Athena 结果桶 + 工作组(字节扫描上限) |
| `NetDiagMcpStack` | API Gateway + API 密钥 + 工具路由器 Lambda(SSM 作用于探测主机;Athena/Glue/S3 作用于流日志表面) |
| `NetDiagAgentStack` | 代理空间、代理角色(只读)、操作员应用程序角色、监视关联 |
### 代理获取的工具(只读,允许列表)
**实时探测**(通过 SSM 在探测主机上运行命令):
| 工具 | 运行 | 返回 |
|------|------|---------|
| `resolve_dns` | `dig` | dns_status、resolved_ip、records、resolver、query_ms |
| `tcp_reachability` | `nc -zv` | 结果 OPEN / CONNECTION_REFUSED / CONNECTION_TIMEOUT + 补救提示 |
| `ping_host` | `ping -c4` | reachable、loss_pct、rtt_avg_ms、结果 |
| `traceroute_host` | `traceroute -I` | 逐跳列表、last_responding_hop |
**历史/法医**(查询 VPC 流日志通过 Athena):
| 工具 | 运行 | 返回 |
|------|------|---------|
| `flow_log_query` | 固定、分区修剪的 Athena 聚合查询 VPC 流日志 | 结果 PERSISTENT_REJECT / INTERMITTENT / ALL_ACCEPT / NO_DATA,accept/reject 计数,first/last_reject 时间戳,top_peers,top_ports |
`flow_log_query` 在多个维度上灵活 - 过滤任意组合:
- `ip` + `direction` - 固定一个端点。`direction:"to"`(默认)= `ip` 是目标(谁连接到它);`direction:"from"` = `ip` 是源(它连接到哪里)。使用 `resolve_dns` 将主机名解析为 IP 地址 - 流日志记录 IP 地址,不记录名称。
- `port` - 目标端口(省略所有端口;对端口发现有用)。
- `protocol` - `TCP` / `UDP` / `ICMP`(省略所有协议)。
- `action` - `ACCEPT` / `REJECT` / `ALL`.
- `window_minutes` - 从现在开始的回溯窗口(默认 60,最多 180),**或**
- `start_time` / `end_time` - 一个明确的事件窗口(ISO-8601 或 epoch)来精确地括起来客户端报告的范围 - 这会为事件生成一个干净的结果,而不是将其与周围的健康流量混合。结果会回声实际的 `window_start`/`window_end`。
至少需要 `ip` / `port` / `protocol` 之一,因此每个查询都保持聚焦(并且便宜)。查询形状始终为 `GROUP BY ... LIMIT`,带有日期分区过滤器 - Lambda 无法物理发出 `SELECT *` 或无边界扫描,并且工作组强制执行硬字节扫描上限。
## 注意(什么是真实的 vs 模拟的)
- **真实的:** 每个探测都是对真实 EC2 主机上的真实命令执行的真实结果;数据包、超时和 RTT 是真实的。故障(`seed.sh`)是一个真实的安全组规则删除 - 端口 5432 真正被丢弃,而不是模拟的。
- **中介和安全:** 代理永远不会获得 shell。每个工具运行一个固定的命令向量;唯一由代理控制的值是经过验证的主机名和端口(在任何 SSM 调用之前拒绝注入)。Lambda 是唯一可以调用 `ssm:SendCommand` 的主体,并且只对单个标记的探测实例调用 `AWS-RunShellScript`。
- **结构化构建:** 代理永远不会看到原始标准输出 - 只有 Lambda 的每个命令解析器生成的 JSON(稳定的 `finding_id`,类型化的判决结果),因此其推理是确定性和可组合的。
- **流日志是真实和法医的:** `flow_log_query` 读取交付到 S3 的真实 VPC 流日志记录。它们 **滞后于现实约 10-20 分钟**(聚合和交付),因此新的故障可以读取 `NO_DATA`,直到记录落地 - 这是预期的,并且这就是为什么流日志 *补充* 而不是取代实时探测的原因。演示运行 1 分钟聚合以获得快速循环;典型的生产 VPC 运行 10 分钟。因为目标具有公共 IP 地址,流日志还会捕获互联网扫描器击中它 - 工具的 `top_peers` 分解将合法的私有源与公共噪音分开。
## 先决条件
在 macOS 和 Linux 上测试(`scripts/` 是 bash 并使用 `aws` + `jq`;在 Windows 上使用 WSL)。
| 工具 | 版本 |
|------|---------|
| AWS CLI | >= 2.35.9(需要 `aws devops-agent` 子命令;使用 [官方安装程序](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) 升级,如果 `aws --version` 较旧或 `aws devops-agent help` 失败) |
| Node.js | >= 20 |
| AWS CDK | v2(`npm i -g aws-cdk`,>= 2.140) |
| Python | 3.12 |
| Docker | 任何近期版本(用于 `sam build --use-container`) |
| AWS SAM CLI | >= 1.100 |
| jq | 任何近期版本 |
- 一个 AWS 账户 + 一个具有凭证的命名 AWS CLI 配置文件。设置运行时配置文件 `<your-profile>`(脚本断言配置文件解析为预期的账户,然后再进行任何更改)。
- 复制 `.env.example` 到 `.env` 并设置目标账户/区域:
```bash
cp .env.example .env
# 然后编辑 CDK_PROCESSING_ACCOUNT / CDK_PROCESSING_REGION
```
- 安装项目依赖项(克隆后需要一次):
```bash
npm install
```
- 在目标账户/区域中启动 CDK(每个账户/区域需要一次,在第一次部署之前):
```bash
source .env
cdk bootstrap aws://${CDK_PROCESSING_ACCOUNT}/${CDK_PROCESSING_REGION} --profile <your-profile>
```
## 成本
此部署会产生 **计费** 基础设施。在 `us-east-1` 中运行演示约一小时,成本约为几美元;保持运行的成本为每天几美元,主要由两个 EC2 主机和无 NAT 的 VPC 决定。粗略的按需估算(使用 AWS 定价计算器针对您的区域进行验证):
| 资源 | 粗略成本 |
|----------|-----------|
| 2x EC2 `t3.micro`(探测 + 目标) | ~$0.02/hr(~$15/mo) |
| VPC、子网、Route53 私有区 | 可忽略(无 NAT 网关) |
| API Gateway + Lambda | 按需付费;演示量级可忽略 |
| S3(流日志)+ Glue + Athena | 几分钱;Athena 按扫描 TB 计费,工作组字节扫描上限 |
**费用从您部署后开始累计** - 运行 `scripts/teardown.sh` 以结束(见下文)。
**节省成本提示:** 如果您需要在演示运行之间暂停,您可以 **停止**(而不是终止)两个 EC2 实例,以消除计算费用。停止/启动是安全的:私有 IP 地址得以保留(因此 `db.corp.internal` 仍然可以正确解析),SSM IAM 策略针对实例 ID(永远不会更改),而诊断工具二进制文件和 nginx 在 EBS 卷上在停止/启动期间存活。在启动实例后,等待 ~1–2 分钟,然后运行任何脚本或演示提示 - SSM 代理需要一些时间在启动后重新注册。您可以使用以下命令确认两者都已就绪:
```bash
aws ssm describe-instance-information \
--profile <your-profile> --region <your-region> \
--query "InstanceInformationList[*].{id:InstanceId,status:PingStatus}" \
--output table
```
两个实例都应显示 `Online`,然后您再继续。
## 运行它
每个脚本都需要 `PROFILE` - 您的 AWS CLI 配置文件名称,用于您的账户。在每个命令上将其作为前缀(或在 shell 会话中一次导出 `<your-profile>`):
```bash
# 1. 构建 + 部署所有四个堆栈
PROFILE=<your-profile> scripts/deploy.sh
# 2. 将 MCP 服务器注册到代理空间(API 密钥身份验证,所有 5 个工具允许列表)
PROFILE=<your-profile> scripts/register.sh
# 3. 将网络调查技能安装到代理空间(幂等创建/更新)。
# 或在操作员应用程序中手动上传 skills/network-connectivity-investigation/SKILL.md。
PROFILE=<your-profile> scripts/skill.sh
# 4. 注入真正的故障(删除 5432 入站规则)
PROFILE=<your-profile> scripts/seed.sh
# 5. 预览工具链(包括 flow_log_query)+ 获取调查提示
PROFILE=<your-profile> scripts/trigger.sh
# 6. 恢复健康基线
PROFILE=<your-profile> scripts/reset.sh
# 7. 拆除所有内容(删除技能、注销 MCP 服务器,然后 cdk destroy)
PROFILE=<your-profile> scripts/teardown.sh
```
或者,导出一次并省略其余会话的前缀:
```bash
export PROFILE=<your-profile>
scripts/deploy.sh
scripts/register.sh
# ...
```
每个脚本在进行任何更改之前都会验证配置文件是否解析为预期的帐户。
### 构建法医(已解决事件)场景
```bash
# 播种故障,生成“远程客户端”流量,重置 - 然后等待流日志交付。
# 使环境保持健康,并有一个有界定的 PAST 窗口,仅 flow_log_query 可以显示。
# 打印事件窗口 + 代理提示。
PROFILE=<your-profile> scripts/scenario-forensic.sh [inject_minutes] [wait_minutes] # 默认 3, 11
```
## 技能(调查方法论)
`skills/network-connectivity-investigation/SKILL.md` 是一个通用的(非演示特定)DevOps 代理技能,教导代理 **如何** 调查 VPC 连接问题:
- **首先进行分类** - 问题现在发生(使用实时探测)还是已经解决/间歇性(使用 `flow_log_query` 在报告的窗口上,因为实时探测对无法重现的问题是盲的)?
- 一个实时探测决策树(DNS -> ping -> tcp -> traceroute)及其判决解释(例如 `CONNECTION_TIMEOUT` => 怀疑安全组/NACL;通过比较允许的端口进行本地化)。
- 一个流日志手册(方向、端口发现、协议)以及如何读取每个判决。
它是 **方法论优于硬边界的 MCP 工具** - 它引导工具选择,不为代理提供原始 SQL,因此在不削弱输出保证的情况下增加调查范围。使用 `scripts/skill.sh`(资产 API)或在操作员应用程序中上传 `SKILL.md` 安装它。
## 演示拍 1 - 现有问题(正在进行)
播种故障后,向代理(操作员应用程序聊天)提问:
> 一个服务无法连接到 `db.corp.internal` 的端口 5432 上。确定主机是否可达,DNS 是否解析,数据库端口是否打开。
> 使用 VPC 流日志确认流量是否被拒绝以及从何时开始。
> 确定根本原因并建议修复。
代理链接:
`resolve_dns`(NOERROR -> 10.20.0.x)-> `ping_host`(ALIVE,0% 损失 -> 主机启动)->
`tcp_reachability:80`(OPEN -> 路径有效)-> `tcp_reachability:5432`(CONNECTION_TIMEOUT)->
`flow_log_query` 到 :5432(如果窗口干净,则 PERSISTENT_REJECT,否则 INTERMITTENT,如果先前基线流量混合 -> **根本原因:安全组阻止 tcp/5432**),流日志证实并建议恢复入站规则 - 无 shell。请注意,`:80` 流日志查询可能显示来自互联网扫描器(公共 IP 地址)的拒绝;`top_peers` 将确认探针在该端口上没有拒绝。
## 演示拍 2 - 已解决事件(历史,法医)
运行 `scripts/scenario-forensic.sh`(种子,生成客户端流量,重置,等待交付),然后向代理询问 **过去** 的窗口:
> 一个远程客户端报告他们大约在 `<START>` 和 `<END>` 之间无法连接到 `db.corp.internal`(端口 5432)。现在似乎工作正常。调查发生了什么,确认是否是网络问题,并告诉我何时开始和停止。
这是实时探测 **无法** 解决的拍:探测现在返回 `OPEN`(问题不再重现)。在技能的指导下,代理识别事件是历史的,并查找 `flow_log_query` - 将报告的 `start_time`/`end_time` 限定在确切的窗口内 ->
`PERSISTENT_REJECT`,`first_reject`/`last_reject` 限定事件窗口,`top_peers` = 客户端的 IP 地址 => **在该窗口期间,tcp/5432 上有一个安全组块,现已恢复。**
通过验证:实时 `tcp_reachability:5432` = `OPEN`,而 `flow_log_query` 返回过去的 REJECTs 以及正确的时间戳。
## 安全和免责声明
- **演示姿态:** 目标主机具有 **公共 IP 地址** 和 `:80` 上的 `nginx` 以实现可达性故事,并且 MCP 端点是公共 API Gateway URL,由 **API 密钥** 保护。这对于短暂的演示是可以的,但 **不是** 健全的生产姿态 - 不要让它无人值守运行,并将 API 密钥视为秘密。
- **自担风险。** 您负责部署的任何 AWS 资源以及它们产生的费用。查看代码和成本部分后再部署,并在完成后拆除。