护理机构 IT 负责人用 AKS 夜间故障 runbook
验证 Azure Monitor 告警 JSON,明确护理应用发生 AKS 夜间故障时的联络与决策边界。
晚上 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 的取值为 Sev0 到 Sev4,signalType 为 Metric、Log 或 Activity Log,monitorCondition 为 Fired 或 Resolved。alertTargetIDs 是告警目标的 Azure Resource Manager ID 列表。这些字段可以帮助分派交接,但不能自动决定入住者安全、纸质流程或机构的业务恢复。
2026 年 7 月 23 日核对的第一手资料包括:AKS 简介、Azure Monitor 概述、Common Alert Schema、监视 AKS和Service 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 提取
severity、signalType、monitorCondition、alertTargetIDs和时间。 - 区分“已确认”“未确认”“等待人工决定”,并指出缺少的下次更新时间。
- 在同一个 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 证据放入独立栏位。
第三个错误是直接把 Sev0 到 Sev4 映射为护理安全等级。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.mjs 与 sanitized-alert.json 提取到临时文件夹,按原样运行两条 Node.js 命令。有效 fixture 以退出码 0 通过;自测拒绝了直接个人数据,以及缺少 severity、signalType、monitorCondition、alertTargetIDs 的五种失败情况。仓库检查还覆盖 frontmatter、站内链接、官方 URL、代码围栏和十种语言共用 slug。测试没有使用 Azure 凭据,因此真实 AKS 访问、Action Group 配置和告警投递仍未验证。下一步是先对机构的一份告警示例脱敏,再把这两条命令作为 runbook review 的第一项检查。
相关文章
Claude Code 首个 Bug Report Runbook:把模糊问题变成安全修复
把模糊 bug 报告转换为有范围、验证命令和 CTA 的 Claude Code 修复流程。
上门护理机构如何用 Claude Code 给走访记录和护理员指示书提速|实战手册
面向上门护理机构服务负责人:用生成式 AI 给走访记录誊清和护理员指示书提速,附我的现场踩坑经历、可复制提示词和校验代码。
养老院护理记录与家属报告:用 Claude Code 整理的实务流程
把潦草的护理记录整理成能读的文字,再把家属报告草稿一并写出,从养老院一线视角整理流程。附可直接复制的提示词和校验脚本。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
让 Claude Code 真正进入可验证的工作流
先用免费 PDF 固定基础,再用 Gumroad 教材复用工作流;如果涉及团队导入、权限或收入路径,可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。