MCP 云基础设施与 IaC 工具:plan/apply 分离与爆炸半径控制

MCP 云基础设施与 IaC 工具服务器实践:AWS/K8s/Terraform 类工具设计、只读与变更操作分离、plan/apply 工作流、dry-run 与审批、凭证最小权限、成本与爆炸半径控制,让模型安全地操作云资源。

1. 云基础设施工具服务器的定位

云基础设施是「一按就可能烧钱、一删就可能宕机」的领域。把云能力交给模型,收益巨大(自动化运维、快速排障、成本分析),风险也巨大(误删资源、越权访问、账单爆炸)。设计的核心是把「只读诊断」与「变更执行」彻底分开。

1.1 能力分类

# 观察类(安全): 查资源、看指标、读日志、列成本
# 计划类(安全): 生成变更计划(plan/diff),不改任何东西
# 变更类(危险): 创建/修改/删除资源(apply/scale/restart)
# 破坏类(极危): 删库、删集群、改 IAM、改网络
# 工具设计的第一原则: 让模型在"观察+计划"层自由,在"变更"层受限

1.2 三类基础设施工具

领域代表特点风险点
IaCTerraform/OpenTofu声明式、有 planapply 改真实资源
编排Kubernetes命令式 + 声明式删 ns、改 RBAC
云 APIAWS/GCP/Azure面广、粒度细IAM、计费、数据

一句话:云工具的黄金法则是「先看后改、先 plan 后 apply」——把不可逆的 apply 与可逆的 plan 物理隔开。

2. 只读 vs 变更操作

工具清单的第一刀,就是按「是否改变状态」切分。只读工具默认开放,变更工具默认关闭或需审批。

2.1 只读工具

工具作用说明
list_resources列资源按标签/类型过滤
describe_resource详情配置、状态、关系
get_metrics指标时间范围、聚合
get_logs日志脱敏后返回
estimate_cost成本估算当前与预测
get_iam_policy权限查询用于自查

2.2 变更工具与分级

# L1 低危变更: 打标签、改描述、加注解(可逆、无影响)
# L2 中危变更: 扩缩容、重启、更新镜像(有影响但可控)
# L3 高危变更: 改网络/安全组、改 IAM、删无状态资源
# L4 极危变更: 删数据、删集群、改计费、动生产数据库
# 分级决定: 谁可用、是否需审批、是否需 dry-run、是否限流

2.3 统一的风险标注

// 每个工具声明风险级,网关据此套用策略
const TOOL_RISK: Record<string, RiskLevel> = {
  list_resources: "none",
  estimate_cost: "none",
  scale_deployment: "medium",
  update_security_group: "high",
  delete_database: "critical",
};

function guard(tool: string, ctx: CallContext) {
  const risk = TOOL_RISK[tool] ?? "high";   // 未标注一律按 high
  if (risk === "critical" && !ctx.flags.allowCritical) {
    throw new McpError(-32003, "critical operation requires approval");
  }
  if (risk === "high" && !ctx.approvalToken) {
    throw new McpError(-32003, "high-risk operation requires dry-run + approval");
  }
}

3. IaC 工具与 plan/apply 分离

Terraform 类工具天生适合 MCP:它的 plan/apply 工作流天然就是「先看后改」的模型。关键是把 plan 和 apply 做成两个工具,且 apply 必须消费一个具体的 plan。

3.1 plan 与 apply 的职责

# terraform_plan:
#   输入: 工作目录/变量/目标
#   输出: 计划摘要(+N ~M -K)、完整 plan 文件 ID、风险摘要
#   副作用: 无(只读 state 与 provider API)
# terraform_apply:
#   输入: plan 文件 ID(必须是本次生成的、未过期的)
#   输出: 执行结果
#   副作用: 改变真实资源
# 关键: apply 只接受 plan_id,不接受"重新生成计划"

3.2 plan 工具实现

async def terraform_plan(workspace: str, var_file: str | None = None,
                         targets: list[str] | None = None) -> dict:
    ws = resolve_workspace(workspace)             # 白名单校验
    plan = await tf.plan(ws, var_file=var_file, targets=targets, lock_timeout="30s")
    # 解析计划,做风险摘要(尤其是销毁操作)
    summary = {
        "add": plan.count("add"),
        "change": plan.count("change"),
        "destroy": plan.count("destroy"),
        "destroys": plan.resource_addresses("destroy"),  # 将被销毁的资源清单
    }
    # 计划文件短期有效,绑定会话
    plan_id = plan_store.put(ws, plan.file, ttl_seconds=900,
                             bind_to=current_session())
    return {
        "plan_id": plan_id,
        "summary": summary,
        "risk": classify_plan_risk(summary),      # none/low/high
        "expires_in": 900,
    }

3.3 apply 工具实现

async function terraformApply(planId: string, ctx: CallContext) {
  const entry = planStore.get(planId);
  if (!entry) throw new McpError(-32602, "unknown or expired plan_id");
  if (entry.bind_to !== ctx.sessionId) {
    throw new McpError(-32003, "plan belongs to another session");   // 防跨会话盗用
  }
  const risk = classifyPlanRisk(entry.summary);
  if (risk === "high" && !(await approvals.isGranted(ctx, planId))) {
    throw new McpError(-32003, "high-risk plan requires approval");
  }
  audit.record({ op: "tf_apply", plan_id: planId, summary: entry.summary });
  return tf.apply(entry.workspace, entry.file);
}

一句话:apply 必须消费「一个具体的、绑定到本会话的、未过期的 plan」——这三点是防止「模型临时改主意乱 apply」的关键。

4. Kubernetes 工具设计

K8s 工具是双刃剑:kubectl get 无害,kubectl delete ns 能删掉整个环境。

4.1 K8s 工具分级

工具风险说明
get_pods / describe无只读诊断
get_logs无日志(脱敏)
get_events无事件
scale_deployment中扩缩容
rollout_restart中重启
set_image中更新镜像
apply_manifest高应用任意清单
delete_resource高删除资源
delete_namespace极危删命名空间

4.2 命名空间与集群隔离

ALLOWED_NAMESPACES = {"dev", "staging", "ai-sandbox"}
DENIED_NAMESPACES = {"kube-system", "prod", "monitoring"}

def assert_namespace(ns: str) -> None:
    if ns in DENIED_NAMESPACES:
        raise McpError(-32003, f"namespace {ns} is protected")
    if ns not in ALLOWED_NAMESPACES:
        raise McpError(-32003, f"namespace {ns} is out of scope")

def assert_scope(manifest: dict) -> None:
    ns = manifest.get("metadata", {}).get("namespace", "default")
    assert_namespace(ns)
    # 禁止跨集群资源(ClusterRole 等)
    if manifest.get("kind") in {"ClusterRole", "ClusterRoleBinding", "CustomResourceDefinition"}:
        raise McpError(-32003, "cluster-scoped resources are not allowed")

4.3 服务端 dry-run

# K8s 原生 dry-run: 校验但不落地
kubectl apply -f manifest.yaml --dry-run=server -o yaml
# 先 dry-run 校验,再真实 apply —— 让模型"先看会发生什么"
kubectl apply -f manifest.yaml --dry-run=server \
  && kubectl apply -f manifest.yaml

5. 云 API 工具(AWS 类)

云 API 面极广,不可能也不该把全部能力暴露给模型。原则是「按需封装、最小能力」。

5.1 封装策略

# 1) 不暴露通用"执行任意 API 调用"的工具(等于给万能钥匙)
# 2) 按场景封装: "查 EC2 实例"、"看 S3 桶大小"、"列 RDS"
# 3) 每个工具只映射到少数几个只读或低危 API
# 4) 参数做白名单: 只允许安全的过滤条件
# 反面教材: 一个 aws_call(service, action, params) 工具 = 无限制

5.2 只读云工具示例

# 只封装只读的 Describe/List 类 API
READONLY_ACTIONS = {
    "ec2:DescribeInstances", "ec2:DescribeSecurityGroups",
    "s3:ListBuckets", "s3:GetBucketLocation",
    "rds:DescribeDBInstances", "cloudwatch:GetMetricData",
    "ce:GetCostAndUsage",       # 成本查询
}

async def cloud_query(service: str, action: str, params: dict) -> dict:
    full = f"{service}:{action}"
    if full not in READONLY_ACTIONS:
        raise McpError(-32003, f"action {full} is not allowed")
    return await aws_client.call(service, action, sanitize(params))

5.3 IAM 与凭证边界

手段说明
专用角色MCP 服务器用独立 IAM 角色,非个人凭证
只读策略默认只挂 ReadOnlyAccess
资源限定策略 Resource 限定到具体资源 ARN
条件键限制来源 IP、MFA、时间窗
会话标签用 STS 会话标签标记「来自 MCP」
权限边界Permission Boundary 兜底

一句话:永远不要暴露一个「执行任意云 API」的工具——那等于把整个云账号的钥匙交给模型。

6. dry-run 与审批

变更类操作必须能「先看后做」。dry-run 是低成本的预演,审批是高成本的兜底。

6.1 dry-run 的层次

# 1) 平台原生 dry-run: K8s --dry-run=server, AWS DryRun=true
# 2) 计划式: Terraform plan(天然就是 dry-run)
# 3) 模拟式: 自建 diff 引擎,对比"变更前后"状态
# 4) 只读预检: 检查前置条件(配额、依赖、冲突)
# 不是所有操作都支持 dry-run,不支持的要有替代(如 diff 预演)

6.2 变更审批流

async function changeWithApproval(req: ChangeRequest, ctx: CallContext) {
  // 1) 预检 + dry-run
  const preview = await previewChange(req);
  if (preview.errors.length) {
    throw new McpError(-32602, `preflight failed: ${preview.errors.join("; ")}`);
  }
  // 2) 风险评估
  const risk = assessRisk(req, preview);
  if (risk.level === "critical") {
    const ticket = await approvals.request({
      principal: ctx.principal,
      change: req,
      preview: preview.summary,
      blast_radius: preview.blastRadius,
    });
    return { status: "pending_approval", ticket_id: ticket.id, preview: preview.summary };
  }
  // 3) 执行 + 审计
  audit.record({ op: "change", req, preview: preview.summary, risk: risk.level });
  return executeChange(req);
}

6.3 爆炸半径评估

def assess_blast_radius(change: dict) -> dict:
    r = {"level": "low", "affected": []}
    if change["kind"] == "delete_namespace":
        r = {"level": "critical", "affected": ["all pods/services in ns"]}
    elif change["kind"] == "update_security_group":
        r = {"level": "high", "affected": ["network reachability"]}
    elif change["kind"] == "scale":
        r = {"level": "medium",
             "affected": [f"replicas {change['from']}→{change['to']}"]}
    r["prod"] = change.get("env") == "prod"
    return r

7. 凭证最小权限

云工具的安全基石是凭证。凭证越权,前面所有工具层的防护都会被绕过。

7.1 权限最小化原则

# 1) 一工具一角色: 不同工具用不同凭证(读工具只读、写工具限定资源)
# 2) 默认拒绝: 策略从"什么都不允许"开始加白名单
# 3) 资源限定: 不用 "*",写到具体 ARN 或前缀
# 4) 短期凭证: 用 STS 临时凭证,不用长期 AccessKey
# 5) 会话标签: 标记调用来源,便于审计与追责
# 6) 定期轮换: 凭证有效期 + 自动轮换

7.2 最小权限策略示例

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadOnlyForDiagnostics",
      "Effect": "Allow",
      "Action": ["ec2:Describe*", "cloudwatch:GetMetricData", "logs:FilterLogEvents"],
      "Resource": "*"
    },
    {
      "Sid": "ScaleOnlySandboxEKS",
      "Effect": "Allow",
      "Action": ["eks:UpdateNodegroupConfig"],
      "Resource": "arn:aws:eks:ap-northeast-1:123456789012:nodegroup/sandbox/*"
    }
  ]
}

7.3 凭证不进模型上下文

# 凭证的"可见性"边界
# 1) 凭证只在 MCP 服务器进程内使用,绝不作为工具返回值
# 2) 工具输出里若出现 token/密钥 → 脱敏
# 3) 日志里不打印凭证
# 4) 模型无法通过任何工具"读到"凭证(没有这种工具)
# 模型不需要凭证,只需要能力——这是设计的分界线

一句话:凭证最小权限是云工具的「最后一道墙」——工具层的所有校验都可以被绕过,凭证权限绕不过。

8. 成本与爆炸半径控制

云资源「一按就烧钱」,一次失控的扩缩容可能产生天价账单。成本控制必须是工具的一等公民。

8.1 成本护栏

护栏机制
单次变更上限扩容不超过 N 倍 / N 个实例
累计预算本会话/本日的估算成本上限
变更前估算每个变更工具先返回成本估算
异常熔断成本增速异常则暂停变更
标签约束只允许操作带指定标签的资源

8.2 变更前的成本估算

async function scaleDeployment(req: ScaleRequest, ctx: CallContext) {
  const current = await k8s.getDeployment(req.ns, req.name);
  const delta = req.replicas - current.spec.replicas;
  const unitCost = await pricing.nodeHourlyCost(current.nodeSelector);
  const hourlyDelta = delta * unitCost;
  const monthlyDelta = hourlyDelta * 730;

  // 超预算则拒绝,并给出具体数字
  if (monthlyDelta > ctx.budget.maxMonthlyDelta) {
    throw new McpError(
      -32003,
      `scale would add $${monthlyDelta.toFixed(2)}/mo, exceeding budget ` +
      `$${ctx.budget.maxMonthlyDelta}`
    );
  }
  return { status: "ok", preview: { from: current.spec.replicas, to: req.replicas, monthlyDelta } };
}

8.3 爆炸半径清单

# 每次变更前问四个问题
# 1) 影响范围: 一个实例、一个服务、一个环境、还是整个账号?
# 2) 可逆性: 能回滚吗? 回滚需要多久?
# 3) 依赖面: 谁会依赖被改的东西?(上游/下游)
# 4) 时间点: 现在是业务高峰吗? 有变更窗口吗?
# 答不上来 → 不允许自动执行

9. 与 GitOps 结合

最安全的云操作方式,是「不让模型直接改云,而是让模型改 Git,由 GitOps 落地」。

9.1 GitOps 模式

# 传统: 模型 → 云 API(直改,审计分散)
# GitOps: 模型 → Git PR(改声明)→ CI 校验 → 人审 → 合入 → 控制器落地
# 优势:
#   1) 所有变更经过 Git 评审与审计
#   2) 声明与状态一致,可回滚(revert 提交)
#   3) 模型能力限制在"改声明",不碰真实资源
# 模型是"变更作者",Git 是"变更通道",GitOps 控制器是"执行者"

9.2 混合策略

# 按操作类型选择通道
operations:
  declarative:            # 声明式变更走 GitOps
    examples: [deployment, configmap, ingress]
    channel: gitops_pr
  diagnostic:             # 诊断走只读工具
    examples: [get_pods, get_metrics, get_logs]
    channel: readonly_api
  imperative:             # 命令式变更走审批 API
    examples: [rollout_restart, scale]
    channel: approved_api
    require: [dry_run, approval]

10. 常见陷阱

  • 暴露通用云 API 工具:aws_call(action, params) 等于交出账号——按场景封装。
  • apply 重新生成计划:模型 apply 时改了参数——apply 只消费 plan_id。
  • plan 不绑会话:A 会话的 plan 被 B 会话 apply——绑定 + 过期。
  • 只读工具用写凭证:读工具却挂了 AdministratorAccess——一工具一角色。
  • 无 dry-run 直接变更:直接删资源无法预演——强制 dry-run 或 diff。
  • 不估成本就扩容:一次误操作账单翻倍——变更前估算 + 预算上限。
  • 命名空间不隔离:模型能删 kube-system——白名单 + 保护名单。
  • 凭证回传模型:工具输出里带 token——脱敏,凭证永不进上下文。

11. 总结

MCP 云基础设施与 IaC 工具服务器,是收益与风险都极高的一类工具。安全落地的核心是四条铁律:读写分离(只读默认开放、变更分级受限、危险操作审批)、plan/apply 分离(apply 只消费绑定会话的、未过期的具体计划)、dry-run 前置(能预演就预演,不能预演就用 diff 替代)、凭证最小权限(一工具一角色、资源限定、短期凭证、永不回传模型)。在此之上叠加成本护栏(变更前估算、预算熔断)与爆炸半径评估(范围、可逆性、依赖面、时间点),并优先采用 GitOps 模式(让模型改声明而非改真实资源),就能把「模型操作云」从高危动作变成可控流程。这与 https://plumephp.com/mcp-security-practices/ 的安全原则、https://plumephp.com/mcp-production-deployment/ 的部署实践、以及 Terraform 的 IaC 方法论一脉相承。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「AI工程」更多文章

  1. MCP 服务器评估与基准测试:工具选择、参数填充与任务成功率
  2. MCP Git 与 DevOps 工具服务器:从只读查询到 CI/CD 触发
  3. MCP 企业治理:RBAC、审批、审计与影子工具管控