Advanced (更新: 2026/7/23)

护理机构 IT 负责人用 AKS 夜间故障 runbook

验证 Azure Monitor 告警 JSON,明确护理应用发生 AKS 夜间故障时的联络与决策边界。

护理机构 IT 负责人用 AKS 夜间故障 runbook

晚上 11 点 40 分,护理机构的夜间负责人来电:预约页面能打开,但护理记录无法保存。Azure 邮件已经到达,可交接内容里没有目标资源、严重性,也没有写明告警仍在触发还是已经解除。更糟的是,发给 IT 服务商的草稿里还留着入住者姓名。运营与 IT 负责人此时必须同时阻止调查延误和个人数据的二次扩散。

本文面向负责 Azure 上预约与护理应用,并设计夜间升级流程的护理机构运营或 IT 负责人,而不是一线护理人员。目标是建立一份 runbook:设施业务连续性与 AKS 技术调查使用同一个 incident 时间线,但由不同角色作出决定。

Azure Kubernetes Service(AKS)是 Azure 用来部署和管理容器化应用的托管 Kubernetes 服务。Kubernetes 负责安排、重启和扩缩容器,Azure 承担 AKS 控制平面的运维负担。不过,Azure 不会替机构判断业务影响,也不会自动完成每个工作负载的调查或夜间联络。

本文要点

  • 不要把 Azure severity 直接当作机构故障级别,应分别确认预约、护理记录、交接和安全影响。
  • Claude Code 读取数据前,先对 Common Alert Schema JSON 脱敏,并在本地验证必需字段和直接个人数据。
  • 分开 Claude Code 的整理任务、Azure/IT 运维人员的技术决定、护理机构负责人的业务决定。
  • 没有 Azure 凭据时,只能称为“交接契约测试”,不能声称测试了 AKS 或告警投递。
  • ROI 使用同类故障的时间戳、退回次数和人工分钟数计算,不填写虚构的改善比例。

把夜间故障变成一条交接工作流

电话、Azure 通知、受影响页面和联系人表分散在不同记录里时,同一件 23:40 的故障会出现多个版本。第一张表先写业务事实:“预约查询成功”“护理记录保存失败”“已启用纸质记录”“下次更新为 00:10”。Pod、Node、Deployment 等技术证据排在这些事实之后。

Azure Monitor 是 Microsoft 的统一可观测性服务,用来收集、分析并处理云和混合环境中的指标、日志、跟踪与事件。Azure Monitor Common Alert Schema(通用告警架构)是消费不同 Azure Monitor 告警通知时使用的标准 JSON 结构。data.essentials 保存严重性等公共元数据,data.alertContext 则随信号类型变化,供调查使用。

Microsoft Learn 说明,severity 的取值为 Sev0Sev4signalTypeMetricLogActivity LogmonitorConditionFiredResolvedalertTargetIDs 是告警目标的 Azure Resource Manager ID 列表。这些字段可以帮助分派交接,但不能自动决定入住者安全、纸质流程或机构的业务恢复。

2026 年 7 月 23 日核对的第一手资料包括:AKS 简介Azure Monitor 概述Common Alert Schema监视 AKSService Health 告警。站内还可参考 Kubernetes 部署指南Claude Code 安全检查

flowchart TD
  A["收到 Azure Monitor 告警"] --> B["保存已脱敏的 JSON fixture"]
  B --> C{"本地 validator 通过?"}
  C -- "否" --> D["删除个人数据或补齐必需字段"]
  D --> B
  C -- "是" --> E["Claude Code 整理交接记录"]
  E --> F["Azure/IT 运维人员决定技术调查"]
  E --> G["护理机构负责人决定业务连续性"]
  F --> H["在下次更新时间合并证据"]
  G --> H
  H --> I{"两类结束条件都满足?"}
  I -- "否" --> H
  I -- "是" --> J["记录事后复盘"]

Service Health 是另一项证据,用来查看符合已配置订阅、服务、区域和事件类型的通知。页面为空不能证明自有应用正常。同样,AKS 告警变为 Resolved,也不代表纸质护理记录已经补录或预约已经核对。

Claude Code 与人工决定的边界

交接表需要列出输入、输出、禁止事项和批准人。把 Claude Code 当成恢复负责人,会让没有证据的原因判断和变更命令混入同一次对话。这里仅让它整理已经批准的证据。

Claude Code 的任务

  • 从已通过验证的 fixture 提取 severitysignalTypemonitorConditionalertTargetIDs 和时间。
  • 区分“已确认”“未确认”“等待人工决定”,并指出缺少的下次更新时间。
  • 在同一个 incident ID 下,分别整理机构说明和 IT 服务商技术记录。
  • 可以提出观测命令,但不执行,并写清每个命令需要的证据与权限。

Azure/IT 运维人员的决定

  • 决定告警规则、目标资源、监视配置、最近变更与 AKS 遥测的调查顺序。
  • 决定是否从 Azure Portal、Container insights、Log Analytics 或授权终端获取更多证据。
  • 批准或拒绝重启、扩缩、rollback 和技术升级。
  • 判断 Resolved 是否足以作为技术恢复证据,还是继续监视。

护理机构故障负责人的决定

  • 是否暂停预约、把护理记录切换到纸张、优先保障哪些业务。
  • 安排安全确认、交接、后续补录以及对家属或外部机构的联络。
  • 批准可共享的信息范围、下次内部更新时间和业务恢复声明。
  • 指定技术恢复后由谁核对漏记记录或重复预约。

本文不提供针对个案的法律意见。个人数据分类、保存、对外提供和事故报告,应遵循机构制度、合同和主管部门的决定。

三个使用场景

使用场景 1:夜间交接预约 API 失败

  • 输入:已脱敏的 Common Alert Schema fixture、预约页面检查、故障时间、下次更新时间。
  • 输出:把 Azure 目标 ID 与机构影响分开的联络表、未知项和给服务商的请求。
  • 人工确认:Azure/IT 运维人员批准调查,机构负责人批准预约备用流程与重复检查。

不能因为 Sev1 就关闭所有预约渠道。只读失败和重复写入风险需要不同的机构行动。Claude Code 只分开已观察事实与待批准事项,不替人作出该决定。

使用场景 2:升级护理记录保存延迟

  • 输入:通过验证的 fixture、受影响功能、脱敏后的数量、时间范围、近期是否发布。
  • 输出:含完整路由字段的技术记录、不含直接个人数据的复现条件、待补证据。
  • 人工确认:Azure/IT 运维人员决定日志和 rollback,机构负责人负责纸质记录与补录核对。

含入住者姓名的截图或日志不能直接交给 Claude Code。validator 会阻止显式身份字段、邮箱和电话号码,但无法识别所有无标签姓名、图片或编码标识符,因此目视审核必须留在批准栏。

使用场景 3:在 Resolved 后结束业务恢复

  • 输入:monitorCondition: "Resolved" 的 fixture、连通性检查、待补记录、重复预约检查、联络历史。
  • 输出:分开的技术与业务结束清单、剩余工作和早班负责人。
  • 人工确认:Azure/IT 运维人员结束监视,机构负责人在记录与预约核对后宣布业务恢复。

Common Alert Schema 中的 Resolved 只表示触发告警的条件已经消失,不表示纸质记录已经录入,也不表示回电完成。采用两个结束条件,可避免技术恢复掩盖机构的剩余工作。

可直接运行的交接 validator

下面的 validator 只使用 Node.js 标准库,并读取命令行指定的本地 JSON。它不会登录 Azure,也不会调用 AKS、Azure Monitor API 或 kubectl。测试范围仅限 Microsoft Learn 记录的 Common Alert Schema 外形和本 runbook 的脱敏契约。

// validate-azure-alert-handoff.mjs
import { readFileSync } from "node:fs";

const REQUIRED = ["severity", "signalType", "monitorCondition", "alertTargetIDs"];
const ALLOWED = {
  severity: new Set(["Sev0", "Sev1", "Sev2", "Sev3", "Sev4"]),
  signalType: new Set(["Metric", "Log", "Activity Log"]),
  monitorCondition: new Set(["Fired", "Resolved"]),
};
const DIRECT_PERSONAL_DATA_KEYS = new Set([
  "residentname", "patientname", "carerecipientname", "serviceusername",
  "staffname", "employeename", "familyname", "guardianname",
  "phone", "phonenumber", "telephone", "mobile",
  "email", "emailaddress", "streetaddress", "postaladdress",
  "dateofbirth", "birthdate", "medicalrecordid", "carerecordid",
  "residentid", "patientid",
]);
const EMAIL = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i;
const PHONE = /(?:^|[^A-Za-z0-9])(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)|\d{2,4})[ .-]\d{2,4}[ .-]\d{3,4}(?:$|[^A-Za-z0-9])/;
const LABELED_NAME = /(?:resident|patient|care recipient|staff|employee|family|guardian|利用者|入居者|患者|職員|家族)(?:\s+name|氏名|名)\s*[:=:]\s*\S+/iu;
const ARM_RESOURCE_ID_PATH = /^\$\.data\.essentials\.(?:alertId|alertRuleId|alertTargetIDs\[\d+\])$/;

function normalizeKey(key) {
  return key.replace(/[^a-z0-9]/gi, "").toLowerCase();
}

function findDirectPersonalData(value, path = "$", findings = []) {
  if (Array.isArray(value)) {
    value.forEach((item, index) => findDirectPersonalData(item, `${path}[${index}]`, findings));
    return findings;
  }
  if (value && typeof value === "object") {
    for (const [key, child] of Object.entries(value)) {
      const childPath = `${path}.${key}`;
      if (DIRECT_PERSONAL_DATA_KEYS.has(normalizeKey(key))) {
        findings.push(`${childPath} uses a prohibited direct-personal-data key`);
      }
      findDirectPersonalData(child, childPath, findings);
    }
    return findings;
  }
  if (typeof value === "string") {
    if (EMAIL.test(value)) findings.push(`${path} contains an email address`);
    if (!ARM_RESOURCE_ID_PATH.test(path) && PHONE.test(value)) {
      findings.push(`${path} contains a phone number`);
    }
    if (LABELED_NAME.test(value)) findings.push(`${path} contains a labeled person name`);
  }
  return findings;
}

export function validate(payload) {
  const errors = [];
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
    throw new Error("payload must be a JSON object");
  }
  if (payload.schemaId !== "azureMonitorCommonAlertSchema") {
    errors.push('schemaId must be "azureMonitorCommonAlertSchema"');
  }

  const essentials = payload.data?.essentials;
  if (!essentials || typeof essentials !== "object" || Array.isArray(essentials)) {
    errors.push("data.essentials must be an object");
  } else {
    for (const field of REQUIRED) {
      if (!(field in essentials)) errors.push(`missing data.essentials.${field}`);
    }
    for (const [field, allowed] of Object.entries(ALLOWED)) {
      if (field in essentials && !allowed.has(essentials[field])) {
        errors.push(`data.essentials.${field} has an undocumented value`);
      }
    }
    if ("alertTargetIDs" in essentials) {
      if (!Array.isArray(essentials.alertTargetIDs) ||
          essentials.alertTargetIDs.length === 0 ||
          essentials.alertTargetIDs.some((id) => typeof id !== "string" || id.trim() === "")) {
        errors.push("data.essentials.alertTargetIDs must be a non-empty string array");
      }
    }
  }

  errors.push(...findDirectPersonalData(payload));
  if (errors.length > 0) throw new Error(errors.join("; "));
  return true;
}

function loadJson(filePath) {
  return JSON.parse(readFileSync(filePath, "utf8"));
}

function runSelfTest() {
  const base = loadJson("sanitized-alert.json");
  validate(base);
  const cases = [
    ["direct personal data", (p) => { p.data.customProperties.residentName = "Example Resident"; }],
    ["missing severity", (p) => { delete p.data.essentials.severity; }],
    ["missing signalType", (p) => { delete p.data.essentials.signalType; }],
    ["missing monitorCondition", (p) => { delete p.data.essentials.monitorCondition; }],
    ["missing alertTargetIDs", (p) => { delete p.data.essentials.alertTargetIDs; }],
  ];
  let rejected = 0;
  for (const [label, mutate] of cases) {
    const candidate = structuredClone(base);
    mutate(candidate);
    try {
      validate(candidate);
      console.error(`FAIL self-test: accepted ${label}`);
      process.exitCode = 1;
    } catch {
      console.log(`PASS self-test: rejected ${label}`);
      rejected += 1;
    }
  }
  if (!process.exitCode) console.log(`PASS self-test: ${rejected} rejection cases`);
}

if (process.argv[2] === "--self-test") {
  runSelfTest();
} else {
  const filePath = process.argv[2];
  if (!filePath) {
    console.error("Usage: node validate-azure-alert-handoff.mjs <fixture.json> | --self-test");
    process.exit(2);
  }
  try {
    validate(loadJson(filePath));
    console.log(`PASS fixture: ${filePath}`);
  } catch (error) {
    console.error(`FAIL fixture: ${error.message}`);
    process.exit(1);
  }
}

把下面的完整 fixture 保存为 sanitized-alert.json,与 validator 放在同一文件夹。ID、时间和规则名都是测试数据,不是实际故障结果。customProperties 是 Common Alert Schema 允许的扩展位置,这里用于让审核人员看到脱敏声明。

{
  "schemaId": "azureMonitorCommonAlertSchema",
  "data": {
    "essentials": {
      "alertId": "/subscriptions/00000000-0000-4000-8000-000000000000/providers/Microsoft.AlertsManagement/alerts/11111111-1111-4111-8111-111111111111",
      "alertRule": "care-api-availability",
      "alertRuleId": "/subscriptions/00000000-0000-4000-8000-000000000000/resourceGroups/rg-care-prod/providers/microsoft.insights/metricAlerts/care-api-availability",
      "severity": "Sev1",
      "signalType": "Metric",
      "monitorCondition": "Fired",
      "monitoringService": "Platform",
      "alertTargetIDs": [
        "/subscriptions/00000000-0000-4000-8000-000000000000/resourceGroups/rg-care-prod/providers/Microsoft.ContainerService/managedClusters/aks-care-prod"
      ],
      "configurationItems": [
        "aks-care-prod"
      ],
      "originAlertId": "sanitized-night-incident-001",
      "firedDateTime": "2026-07-23T14:40:00Z",
      "description": "The production booking and care-record API crossed its approved availability threshold.",
      "essentialsVersion": "1.0",
      "alertContextVersion": "1.0"
    },
    "alertContext": {
      "properties": null
    },
    "customProperties": {
      "environment": "production",
      "service": "booking-care-api",
      "sanitized": "true",
      "runbook": "night-incident-v1"
    }
  }
}

使用 Node.js 17 或更高版本运行以下准确命令。第一条读取有效 fixture;第二条证明 validator 会拒绝直接个人数据,以及四个必需字段分别缺失的情况。

node validate-azure-alert-handoff.mjs sanitized-alert.json
node validate-azure-alert-handoff.mjs --self-test

以下是 2026 年 7 月 23 日使用本文代码和 fixture 得到的准确输出,不是预计结果。

PASS fixture: sanitized-alert.json
PASS self-test: rejected direct personal data
PASS self-test: rejected missing severity
PASS self-test: rejected missing signalType
PASS self-test: rejected missing monitorCondition
PASS self-test: rejected missing alertTargetIDs
PASS self-test: 5 rejection cases

验证通过不代表 Azure 配置、真实告警投递、AKS 健康状态或恢复程序已经通过测试。真实连接测试需要在 Action Group 上启用 Common Alert Schema,并准备接收端、Azure 凭据、网络、权限和测试告警。本文没有使用凭据,因此只测试了文档化的交接契约。

陷阱:不要把固定对象通过称为运维测试

第一个错误是在脚本中验证固定对象,却声称已经实现。原因是文件读取、无效 JSON、字段缺失和拒绝退出从未运行。修正方法是在每次 runbook review 中读取本地 fixture,并运行正常与失败两类命令。

第二个错误是把 validator 通过称为 AKS 连通测试。原因是把输入契约和云连接使用同一个测试名称。应记录“handoff contract passed”,并把告警投递、监视与 AKS 证据放入独立栏位。

第三个错误是直接把 Sev0Sev4 映射为护理安全等级。Azure 严重性与机构影响是两套尺度。Azure 严重性、预约影响、记录影响、入住者安全和纸质备用流程需要分别批准。

第四个错误是把 Resolved 自动广播为故障结束。告警条件可能已经解除,但漏记仍未处理。只有技术与机构两个结束条件都完成后,才能关闭 incident ID。

第五个错误是把 validator 当作个人数据保证。键名和文本模式无法检测每张图片、不透明标识符或无标签姓名。应最小化输入,人工检查自由文本与附件,并遵循机构的信息治理制度。

用同类故障测量 ROI

ROI 表应从 incident ID、时间戳和人工分钟数生成。只比较严重性范围、夜间时段和影响范围相近的故障。事件太少时,展示每次时间线,不要断言一个不稳定的比例。

指标记录方法要回答的问题
IT 交接时间第一次通知到 IT 收到合格 fixture契约缺陷造成多少等待?
影响确认时间通知到预约、记录、交接范围获批技术与运营之间有多少往返?
fixture 退回率validator 失败提交数 ÷ 全部提交数必需字段和脱敏是否形成习惯?
首次升级受理率无需补问即可调查的件数 ÷ 全升级件数交接是否足够完整?
人工作业时间各角色用于转记、确认和补录的分钟数夜间和早班还剩多少工作?

需要折算金额时,用节省的分钟数乘以机构批准的综合人工单价,加上获批的返工减少额,再减去 runbook 设计、审核、演练和维护成本,最后用净收益除以投入成本。若没有基线、期间和排除条件,就报告时间与退回趋势,不要把它叫作 ROI。

常见问题

Q. 这个 validator 会连接 Azure Monitor 或 AKS 吗?

A. 不会。它只读取本地 JSON,不能证明 Azure 投递、凭据或集群状态。

Q. 为什么要求四个 essentials 字段?

A. 夜间分派需要严重性、信号类型、触发或解除状态与目标资源。它没有把 Common Alert Schema 的全部字段变成私有必填规范。

Q. 通过后就能证明没有个人数据吗?

A. 不能。它拒绝显式键名、邮箱、电话和带标签的姓名,图片和无标签自由文本仍需人工审核。

Q. 应让 Claude Code 执行 Azure 变更命令吗?

A. 本 runbook 不允许。Azure/IT 运维人员先确认权限、影响、rollback 和批准,再按既有变更程序执行。

Q. Service Health 为空就能证明 AKS 正常吗?

A. 不能。Service Health alerts 反映已配置条件,还需结合应用、工作负载、网络和监视证据。

咨询 CTA:incident runbook review

ClaudeCodeLab 培训与咨询页面提交护理预约或记录应用的夜间流程时,请指定“incident runbook review”。交付物包括现有 runbook 批注、Common Alert Schema 交接契约、三类角色决策表、测试步骤和未解决风险清单。只需提供已脱敏的告警示例、联系人表和最近一次时间线,不要包含凭据或个人数据。

实际测试结果

2026 年 7 月 23 日,我们把文中 validate-azure-alert-handoff.mjssanitized-alert.json 提取到临时文件夹,按原样运行两条 Node.js 命令。有效 fixture 以退出码 0 通过;自测拒绝了直接个人数据,以及缺少 severitysignalTypemonitorConditionalertTargetIDs 的五种失败情况。仓库检查还覆盖 frontmatter、站内链接、官方 URL、代码围栏和十种语言共用 slug。测试没有使用 Azure 凭据,因此真实 AKS 访问、Action Group 配置和告警投递仍未验证。下一步是先对机构的一份告警示例脱敏,再把这两条命令作为 runbook review 的第一项检查。

#claude-code #护理机构 #aks #azure-monitor #故障响应
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

让 Claude Code 真正进入可验证的工作流

先用免费 PDF 固定基础,再用 Gumroad 教材复用工作流;如果涉及团队导入、权限或收入路径,可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。