Advanced (更新: 2026/7/23)

介護施設のAKS夜間障害runbook: Claude Codeで連絡契約を検証

介護施設の運用・IT責任者向けに、AKS夜間障害の連絡順とAzure MonitorアラートJSONの検証手順を示します。

介護施設のAKS夜間障害runbook: Claude Codeで連絡契約を検証

23時40分、介護施設の夜間責任者から「予約画面は開くが、介護記録の保存が失敗する」と電話が入りました。Azure通知の対象リソース、重大度、発火状態が転記されず、IT委託先への連絡文には利用者名まで残っています。

対象は、予約・介護記録アプリをAzure上で運用し、夜間連絡も設計する介護施設の運用・IT責任者です。夜勤職員向けではなく、施設の業務継続とAKS調査の決定者を固定するrunbookを作ります。

Azure Kubernetes Service(AKS)は、コンテナ化したアプリをAzureで配備・管理するマネージドKubernetesサービスです。Azureはコントロールプレーンを担いますが、アプリの影響判定、ワークロード調査、夜間連絡は施設側に残ります。

この記事の要点

  • severityだけで施設の緊急度を決めず、予約・記録・申し送りへの影響を別に判定する。
  • Claude Codeへ渡す前に、Common Alert SchemaのJSONをサニタイズし、必須項目と直接個人データをローカルで検査する。
  • Claude Codeの整理作業、Azure/IT運用者の技術判断、介護施設のインシデント判断を分ける。
  • Azure資格情報なしの検証はAKSへの接続テストではなく、夜間引き渡し契約のテストだと明記する。
  • ROIは架空の改善率を置かず、同程度の障害について連絡時間、差し戻し、手作業時間を導入前後で測る。

夜間障害を1本の引き渡しフローにする

電話、Azure通知、影響画面、連絡表が別々だと、23時40分の事実が担当者ごとに変わります。最初の記録は「予約検索は成功」「介護記録の保存は失敗」「紙の記録票へ切替済み」「次回更新は0時10分」のように、画面と業務で書きます。PodNodeの状態は、その後ろに技術証拠として置きます。

Azure Monitorは、クラウドやハイブリッド環境のメトリック、ログ、トレース、イベントを集めて分析し、通知や対応につなぐMicrosoftの監視サービスです。Azure Monitor Common Alert Schema(共通アラートスキーマ)は、種類の異なるAzure Monitorアラート通知を共通のJSON構造で受けるための仕様です。data.essentialsには重大度などの共通情報、data.alertContextにはシグナル種類ごとの調査情報が入ります。

Microsoft Learnでは、severitySev0からSev4signalTypeMetricLogActivity LogmonitorConditionFiredまたはResolvedとされています。alertTargetIDsは影響対象のAzure Resource Manager ID一覧です。これらは連絡先を選ぶ材料にはなりますが、利用者の安全や紙運用の解除条件を自動決定する値ではありません。

AKSの概要Azure Monitorの概要Common Alert SchemaAKSの監視Service Health alertsを2026年7月23日に確認しました。AKSの用語を先に整理する場合はClaude CodeでKubernetesデプロイを確認する記事、データを渡す境界はClaude Codeセキュリティの実務チェックも参照してください。

flowchart TD
  A["Azure Monitor alertを受信"] --> 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、時刻を抜き出す。
  • 「確認済み」「未確認」「人の判断待ち」を分け、次回更新時刻の空欄を指摘する。
  • 施設向け説明とIT委託先向け技術メモを、同じincident IDの下で別欄に整える。
  • 実行候補のコマンドを提示しても実行せず、観測目的と必要権限を添える。

Azure/IT運用者が決めること

  • アラートルール、対象リソース、監視設定、直近変更、AKSのメトリック・ログを調べる順番。
  • Azure Portal、Container insights、Log Analytics、権限付き端末から追加証拠を取るか。
  • 再起動、スケール、rollback、ベンダーへの技術エスカレーションを実施するか。
  • Resolvedを技術的な復旧候補として受け入れるか、追加監視を続けるか。

介護施設のインシデント責任者が決めること

  • 予約受付を止めるか、介護記録を紙へ切り替えるか、どの業務を優先するか。
  • 利用者の安全確認、申し送り、後入力、家族・関係先への連絡が必要か。
  • 共有可能な情報の範囲、施設内の次回連絡時刻、業務復旧の宣言。
  • 技術復旧後に残る未入力記録や予約重複を、誰がいつ照合するか。

これは法的助言ではありません。個人データの分類、保存期間、外部提供、事故報告は、施設の規程、契約、所管部門の判断に従ってください。

3つのUse case

Use case 1: 予約APIの失敗を夜間担当へ引き渡す

  • 入力: サニタイズ済みCommon Alert Schema fixture、予約画面の確認結果、発生時刻、次回更新時刻。
  • 出力: Azure対象IDと施設影響を分けた1枚の連絡表、未確認項目、IT委託先への依頼。
  • 人の確認: Azure/IT運用者は技術調査を、施設責任者は新規予約の受付方法と重複確認を承認する。

Sev1でも、閲覧失敗と二重送信リスクでは施設判断が違います。Claude Codeは観測事実と承認待ちだけを分けます。

Use case 2: 介護記録の保存遅延をIT委託先へ渡す

  • 入力: 合格済みfixture、失敗する機能名、匿名化した件数、時刻範囲、直近リリースの有無。
  • 出力: 必須キーがそろった技術メモ、直接個人データを含まない再現条件、追加で必要な証拠一覧。
  • 人の確認: Azure/IT運用者はログ取得やrollbackを決め、施設責任者は紙記録と後入力の照合手順を決める。

利用者名入りの画像やログは渡しません。validatorが全ての自由文や画像を判定できないため、目視確認を承認欄に残します。

Use case 3: Resolved後の業務復旧を閉じる

  • 入力: monitorCondition: "Resolved"のfixture、疎通確認、未入力記録一覧、予約重複チェック、施設内連絡履歴。
  • 出力: 技術復旧と業務復旧を分けた終了チェックリスト、残作業、翌朝の確認担当。
  • 人の確認: Azure/IT運用者は監視状態を閉じ、施設責任者は介護記録と予約の整合を確認して業務終了を宣言する。

Common Alert SchemaのResolvedは発火条件の解消であり、紙記録の電子化や折り返し完了ではありません。終了条件を二つに分けます。

動く確認コード: 引き渡しvalidator

次のコードはNode.jsの標準機能だけを使い、コマンドで指定したローカルJSONを読みます。Azureへログインせず、AKS API、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として同じフォルダーへ置きます。ID、時刻、ルール名はテスト用です。customPropertiesにはサニタイズ済みかを記録します。

{
  "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以降で、次のコマンドをそのまま実行します。1本目はファイルを読む正常系、2本目は直接個人データと4つの必須項目欠落を本当に拒否する失敗系です。

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状態、復旧手順を証明しません。実接続にはCommon Alert Schemaを有効にしたAction Group、受信先、資格情報、ネットワーク、権限、テストアラートが必要です。

Pitfall: 固定データの合格を運用テストと呼ばない

根本原因は、ローカルの入力契約テストとクラウドへの実接続テストを同じ「実装」と呼ぶことです。各結果に「何を確認したか」「何を未確認のまま残したか」を併記すれば、次の担当者による証拠範囲の誤解を防げます。

一つ目は、固定オブジェクトだけを検査し「実装済み」と書くことです。ファイル読込、JSON不正、欠落、拒否終了を通すため、runbookレビューで両コマンドを実行します。

二つ目は、validator合格をAKS疎通と呼ぶことです。「handoff contract passed」と記録し、配送、監視、AKS調査は別欄にします。

三つ目は、Sev0からSev4を施設の安全区分へ直結させることです。Azure severity、予約、記録、安全、紙運用を別々に承認します。

四つ目は、Resolvedを終了通知として扱う失敗です。ITと施設の終了条件が埋まるまでincident IDを閉じません。

五つ目は、validatorを個人データ判定の保証として扱う失敗です。入力を最小化し、自由文と添付は人が確認します。

ROIは同程度の障害を導入前後で測る

ROI表は、障害ごとの時刻と作業時間をincident IDで残して作ります。架空の短縮率は置かず、同じ重大度帯、同じ時間帯、近い影響範囲の障害を比較します。障害件数が少ない期間は率を断定せず、1件ごとの経過を併記します。

指標記録方法見たい変化
IT引き渡し時間最初の通知から合格fixtureをIT担当が受領するまで契約不備による待ち時間
影響確定時間通知から予約・記録・申し送りの範囲が承認されるまで技術調査と施設判断の往復
fixture差し戻し率validator不合格回数 ÷ 全fixture提出回数必須項目とサニタイズの定着
初回エスカレーション受理率追加質問なしで調査開始した件数 ÷ 全エスカレーション件数引き渡しの完全性
手作業時間役割別に転記、確認、再入力へ使った分数を合計夜間と翌朝の負担

金額は、短縮時間に承認済み人件費単価を掛け、削減できた手戻りを足し、作成・レビュー・訓練・保守費を引いて投資額で割ります。基準値、期間、除外条件がなければ、時間と差し戻し件数だけを報告します。

よくある質問

Q. このvalidatorはAzure MonitorやAKSへ接続しますか。

A. 接続しません。ローカルJSONを読むだけです。Azureの配送、資格情報、クラスタ状態を検査したという記録には使えません。

Q. なぜseveritysignalTypemonitorConditionalertTargetIDsを必須にするのですか。

A. 夜間の振り分けで、重大度、シグナル種別、発火・解消状態、対象リソースを欠かさないためです。Common Alert Schemaの全項目を独自に必須化しているわけではありません。

Q. 合格すれば個人データはゼロですか。

A. いいえ。validatorは明示的なキー、メール、電話番号、ラベル付き氏名を拒否しますが、画像やラベルのない自由文を保証しません。人の目視と施設の規程が必要です。

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引き渡し契約、三者の判断表、テスト手順、未解決リスク一覧です。個人データと資格情報を除いたアラート例、連絡表、直近1件の時系列を使います。

実際に試した結果

2026年7月23日に掲載コードとfixtureを一時フォルダーへ取り出し、2つのNode.jsコマンドを実行しました。正常系は終了コード0、セルフテストは直接個人データと4必須項目欠落の計5ケースを全て拒否しました。frontmatter、内部リンク、公式URL、コードフェンス、10言語slugもリポジトリで確認します。Azure資格情報は使わず、実AKS、Action Group、配送は未検証です。まず自施設のアラート例をサニタイズし、この2コマンドを実行してください。

#claude-code #介護 #AKS #Azure Monitor #インシデント対応
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。