Advanced (업데이트: 2026. 7. 23.)

요양시설 IT 책임자를 위한 AKS 야간 장애 runbook

Azure Monitor 경보 JSON을 검증하고 요양 앱의 AKS 야간 장애 연락과 판단 경계를 정리합니다.

요양시설 IT 책임자를 위한 AKS 야간 장애 runbook

밤 11시 40분, 요양시설 야간 책임자가 전화했습니다. 예약 화면은 열리지만 돌봄 기록이 저장되지 않습니다. Azure 메일은 도착했지만 인계 메모에는 대상 리소스, 심각도, 경보가 계속 발생 중인지 해소됐는지가 없습니다. IT 위탁사에 보낼 초안에는 입소자 이름까지 남아 있습니다. 운영·IT 책임자는 조사 지연과 개인정보의 2차 확산을 동시에 막아야 합니다.

이 글은 Azure에서 예약 및 돌봄 애플리케이션을 운영하고 야간 에스컬레이션 흐름까지 책임지는 요양시설 운영·IT 리드를 위한 것입니다. 일선 돌봄 직원을 위한 매뉴얼이 아닙니다. 시설의 업무 연속성 판단과 AKS 기술 조사를 섞지 않으면서 하나의 incident 타임라인을 유지하는 runbook을 만듭니다.

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 알림, 영향받은 화면, 연락처 표가 다른 메모에 있으면 밤 11시 40분의 사실이 담당자마다 달라집니다. 첫 기록에는 “예약 조회 성공”, “돌봄 기록 저장 실패”, “종이 기록 전환 완료”, “다음 업데이트 00:10”처럼 화면과 업무 사실을 씁니다. Pod, Node, Deployment 상태는 그 뒤에 기술 증거로 붙입니다.

Azure Monitor는 클라우드와 하이브리드 환경의 메트릭, 로그, 추적, 이벤트를 수집·분석하고 대응에 연결하는 Microsoft의 통합 관찰성 서비스입니다. Azure Monitor Common Alert Schema(공통 경보 스키마)는 서로 다른 Azure Monitor 경보 알림을 일관된 JSON 구조로 받는 규격입니다. data.essentials에는 심각도 같은 공통 메타데이터가, data.alertContext에는 신호 유형에 따른 조사 정보가 들어갑니다.

Microsoft Learn에 따르면 severitySev0부터 Sev4, signalTypeMetric, Log, Activity Log, monitorConditionFired 또는 Resolved입니다. alertTargetIDs는 경보 대상 Azure Resource Manager ID 목록입니다. 이 필드는 인계를 라우팅하는 근거일 뿐, 입소자 안전, 종이 업무 전환, 시설의 업무 복구 선언을 자동으로 결정하지 않습니다.

2026년 7월 23일 확인한 1차 자료는 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를 기술 복구 근거로 받아들일지, 모니터링을 계속할지 결정합니다.

요양시설 장애 책임자 결정

  • 예약을 중지할지, 돌봄 기록을 종이로 전환할지, 어떤 업무를 우선할지 정합니다.
  • 안전 확인, 인수인계, 사후 입력, 가족 또는 외부 관계자 연락을 지시합니다.
  • 공유 가능한 정보 범위, 다음 내부 공지 시각, 업무 복구 선언을 승인합니다.
  • 기술 복구 뒤 누락 기록과 중복 예약을 누가 언제 대조할지 지정합니다.

이 글은 개별 법률 자문이 아닙니다. 개인정보 분류, 보관, 외부 제공, 사고 보고는 시설 규정, 계약, 담당 부서의 판단을 따르십시오.

사용 사례 3가지

사용 사례 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를 읽고, 두 번째는 직접 개인정보와 네 필수 필드 누락을 실제로 거부하는지 확인합니다.

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.mjssanitized-alert.json을 임시 폴더에 추출해 표시한 두 Node.js 명령을 실행했습니다. 정상 fixture는 종료 코드 0으로 통과했습니다. 셀프 테스트는 직접 개인정보와 severity, signalType, monitorCondition, alertTargetIDs 누락을 포함한 다섯 실패 사례를 모두 거부했습니다. 저장소 검사에서는 frontmatter, 내부 링크, 공식 URL, 코드 펜스, 열 개 언어의 공통 slug도 확인합니다. Azure 자격 증명은 사용하지 않았으므로 실제 AKS 접근, Action Group 설정, 경보 전달은 미검증 상태입니다. 먼저 시설의 경보 예시 한 건을 비식별화하고 이 두 명령을 runbook review의 첫 확인으로 실행하십시오.

#claude-code #요양시설 #aks #azure-monitor #장애대응
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.