索引に戻る

災害復旧手順・依存順序台帳

時系列
生成可能
災害復旧手順・依存順序台帳

作品として見る

持ち帰る

コピーして貼るか、ファイルに保存して渡す。ネットにつながらない環境には「全部入り」を 1 つ渡せば足りる。

全部入り

disaster-recovery-runbook-everything.md

このパネルの中身を、URL の先にあるファイルの中身まで含めて 1 つにまとめた Markdown

依頼文

1.8 KB
「災害復旧手順・依存順序台帳」でHTML資料を作ってください。この型は実行できるので、版面を組み直す必要はありません — 必要なのは中身です。

## この型がすること
サービス復旧の依存順序、RTO/RPO、責任者、実施操作、復旧検証をひと続きの手順にまとめる。

## 渡す必要があるもの
- システム、RTO/RPO、順序づけた工程、先行工程、目標時刻、責任者、復旧操作、検証条件を定義する。

## 入力の形
- `title` — string — 復旧計画の結論
- `subtitle` — string? — 対象環境・演習日・承認版
- `system` — string — 対象システム
- `rtoMinutes` — number — サービス復旧目標時間
- `rpoMinutes` — number — 許容データ損失時間
- `steps` — array (3–12) — 依存順序、目標時刻、操作、責任者、検証、準備状態を持つ復旧工程

入力の機械可読な検証は schema.ts が持ちます。足りない入力は推測せず質問してください。

## 向かない場合
- 障害発生直後の重大度別応答と担当引き継ぎを定義する場合は、インシデント対応方針を使う。

## 作り方
入力を JSON にまとめ、次を実行します。

```bash
node scripts/render.mjs disaster-recovery-runbook <入力.json> --theme ink -o out.html
```

出力は 1 ファイルで完結し、外部から何も読み込みません。

## 原本
- https://design-vault.pages.dev/designs/disaster-recovery-runbook/source/template.tsx
- https://design-vault.pages.dev/designs/disaster-recovery-runbook/source/schema.ts
- https://design-vault.pages.dev/designs/disaster-recovery-runbook/source/sample-data.json
- https://design-vault.pages.dev/designs/disaster-recovery-runbook/sample.html

ファイル

テンプレート・入力スキーマ・サンプルデータ・メタ情報。この文書はこの 4 つから生成されています。 生成された文書

template.tsx

6.9 KB

/designs/disaster-recovery-runbook/source/template.tsx

中身を見る
import type { Props } from "./schema";

const CSS = String.raw`
.asset-disaster-recovery-runbook{font-family:var(--font-sans);color:var(--color-primary);background:var(--color-surface);max-width:var(--container-slide);margin:auto;padding:3rem 2.5rem;line-height:1.55;font-size:var(--text-body-sm)}
.asset-disaster-recovery-runbook *{box-sizing:border-box}.asset-disaster-recovery-runbook header{margin-bottom:1.1rem}.asset-disaster-recovery-runbook .eyebrow{font-family:var(--font-mono);font-size:var(--text-label);color:var(--color-tertiary);margin:0 0 .6rem}.asset-disaster-recovery-runbook h1{font-size:var(--text-h1);line-height:1.25;max-width:28em;margin:0}.asset-disaster-recovery-runbook .subtitle{color:var(--color-secondary);margin:.65rem 0 0}.asset-disaster-recovery-runbook .objectives{display:flex;gap:1.5rem;flex-wrap:wrap;padding:.7rem 0;border-block:1px solid var(--color-outline);margin-bottom:1rem}.asset-disaster-recovery-runbook .objectives p{margin:0;color:var(--color-secondary);font-size:var(--text-label)}.asset-disaster-recovery-runbook .objectives strong{font-family:var(--font-mono);font-size:var(--text-h3);color:var(--color-primary);margin-right:.3rem}.asset-disaster-recovery-runbook .steps{border-left:2px solid var(--color-outline);margin-left:.65rem}.asset-disaster-recovery-runbook .step{position:relative;padding:0 0 1rem 1.4rem}.asset-disaster-recovery-runbook .step:before{content:"";position:absolute;left:-.37rem;top:.25rem;width:.58rem;height:.58rem;background:var(--color-tertiary);border-radius:50%}.asset-disaster-recovery-runbook .head{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap}.asset-disaster-recovery-runbook code{font-family:var(--font-mono);font-size:var(--text-label);color:var(--color-tertiary)}.asset-disaster-recovery-runbook .head h2{font-size:var(--text-body-md);margin:0;flex:1}.asset-disaster-recovery-runbook .badge{font-size:var(--text-label);border:1px solid var(--color-outline);padding:.03rem .4rem;color:var(--color-secondary)}.asset-disaster-recovery-runbook .summary{display:grid;grid-template-columns:minmax(0,.6fr) minmax(0,1.3fr) minmax(0,1.3fr);gap:.65rem;padding:.55rem .65rem;margin-top:.5rem;background:var(--color-surface-subtle)}.asset-disaster-recovery-runbook .field{min-width:0}.asset-disaster-recovery-runbook .field span{display:block;font-size:var(--text-label);color:var(--color-secondary)}.asset-disaster-recovery-runbook .field p{margin:.08rem 0 0;overflow-wrap:anywhere}.asset-disaster-recovery-runbook .details{display:grid;grid-template-columns:1fr 1fr;gap:.5rem 1.1rem;margin-top:.5rem}.asset-disaster-recovery-runbook .details p{margin:.08rem 0 0;overflow-wrap:anywhere}.asset-disaster-recovery-runbook .validation{padding-top:.35rem;border-top:1px dashed var(--color-outline)}.asset-disaster-recovery-runbook .deps{font-family:var(--font-mono);font-size:var(--text-label)}.asset-disaster-recovery-runbook .still-svg{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
@media(max-width:680px){.asset-disaster-recovery-runbook{padding:1.5rem 1rem}.asset-disaster-recovery-runbook h1{font-size:var(--text-h2)}.asset-disaster-recovery-runbook .summary{grid-template-columns:minmax(0,1fr)}.asset-disaster-recovery-runbook .details{grid-template-columns:minmax(0,1fr)}.asset-disaster-recovery-runbook .objectives{gap:.8rem}}
@media print{.asset-disaster-recovery-runbook{max-width:none;padding:1rem}.asset-disaster-recovery-runbook .step{break-inside:avoid}}
`;

const stateLabel = { planned: "予定", ready: "手順準備済", blocked: "阻害あり", exercised: "演習済" };
const compact = (value: string, max: number) => [...value].length > max ? `${[...value].slice(0, max - 1).join("")}…` : value;

function Still({ data }: { data: Props }) {
  return <svg className="still-svg" data-layout-still="true" width="1280" height="720" viewBox="0 0 1280 720" aria-hidden="true">
    <rect width="1280" height="720" fill="var(--color-surface)" />
    <text x="48" y="43" fontSize="13" fontFamily="sans-serif" fill="var(--color-secondary)">{data.system} · RTO {data.rtoMinutes}m / RPO {data.rpoMinutes}m</text>
    <text x="48" y="82" fontSize="25" fontWeight="700" fontFamily="sans-serif" fill="var(--color-on-surface)">{compact(data.title, 68)}</text>
    <text x="48" y="117" fontSize="12" fontFamily="sans-serif" fill="var(--color-secondary)">復旧順序 · 前提工程 · 目標時刻 · 責任者 · 正常性検証</text>
    {data.steps.slice(0, 8).map((step, index) => {
      const y = 164 + index * 62;
      return <g key={step.id}>
        <line x1="48" x2="1232" y1={y - 19} y2={y - 19} stroke="var(--color-outline)" />
        <text x="52" y={y + 3} fontSize="11" fontFamily="monospace" fill="var(--color-tertiary)">{String(step.sequence).padStart(2, "0")} · {step.id}</text>
        <text x="190" y={y + 3} fontSize="12" fontWeight="700" fontFamily="sans-serif" fill="var(--color-on-surface)">{compact(step.service, 31)}</text>
        <text x="500" y={y + 3} fontSize="11" fontFamily="sans-serif" fill="var(--color-secondary)">T+{step.targetMinutes}m · {stateLabel[step.state]}</text>
        <text x="730" y={y + 3} fontSize="11" fontFamily="monospace" fill="var(--color-on-surface)">{step.dependsOn.join(", ") || "開始"}</text>
      </g>;
    })}
    <text x="48" y="704" fontSize="12" fontFamily="sans-serif" fill="var(--color-secondary)">{data.steps.length}復旧工程 · 依存順序と検証条件を記録</text>
  </svg>;
}

export default function Template(data: Props) {
  return <section className="asset asset-disaster-recovery-runbook">
    <style>{CSS}</style>
    <header>
      <p className="eyebrow">{data.system} · 災害復旧手順</p>
      <h1>{data.title}</h1>
      {data.subtitle && <p className="subtitle">{data.subtitle}</p>}
    </header>
    <div className="objectives" aria-label="復旧目標"><p><strong>{data.rtoMinutes}</strong>分 RTO</p><p><strong>{data.rpoMinutes}</strong>分 RPO</p><p><strong>{data.steps.length}</strong>復旧工程</p></div>
    <div className="steps">
      {data.steps.slice().sort((a, b) => a.sequence - b.sequence).map((step) => <article className="step" key={step.id}>
        <div className="head"><code>{String(step.sequence).padStart(2, "0")} · {step.id}</code><h2>{step.service}</h2><span className="badge" data-state={step.state}>{stateLabel[step.state]}</span></div>
        <div className="summary">
          <div className="field"><span>目標時刻</span><p>T+{step.targetMinutes}分</p></div>
          <div className="field"><span>先行工程</span><p className="deps">{step.dependsOn.length ? step.dependsOn.join(" · ") : "初動工程"}</p></div>
          <div className="field"><span>実施責任者</span><p>{step.owner}</p></div>
        </div>
        <div className="details"><div className="field"><span>復旧操作</span><p>{step.action}</p></div><div className="field validation"><span>復旧後の確認</span><p>{step.validation}</p></div></div>
      </article>)}
    </div>
    <Still data={data} />
  </section>;
}

schema.ts

3.1 KB

/designs/disaster-recovery-runbook/source/schema.ts

中身を見る
import { z } from "zod";

const text = z.string().trim().min(1).max(160);
const short = z.string().trim().min(1).max(48);
const stepSchema = z.object({
  id: short.describe("復旧工程の一意な識別子"),
  sequence: z.number().int().min(1).max(12).describe("復旧順序"),
  service: short.describe("復旧するサービスまたはデータ資産"),
  dependsOn: z.array(short).max(4).describe("先に完了すべき復旧工程ID"),
  targetMinutes: z.number().int().positive().describe("障害検知からの復旧目標時刻"),
  action: text.describe("担当者が実施する復旧操作"),
  owner: short.describe("復旧操作の担当者またはチーム"),
  validation: text.describe("復旧後の正常性確認"),
  state: z.enum(["planned", "ready", "blocked", "exercised"]).describe("手順準備・演習状態"),
}).strict();

export const schema = z.object({
  title: text.describe("復旧計画の結論"),
  subtitle: text.optional().describe("対象環境・演習日・承認版"),
  system: short.describe("対象システムまたは業務サービス"),
  rtoMinutes: z.number().int().positive().describe("サービス復旧目標時間(分)"),
  rpoMinutes: z.number().int().nonnegative().describe("許容データ損失時間(分)"),
  steps: z.array(stepSchema).min(3).max(12).describe("依存順序を持つ復旧手順"),
}).strict().superRefine((data, ctx) => {
  const ids = data.steps.map(({ id }) => id);
  const sequences = data.steps.map(({ sequence }) => sequence);
  if (new Set(ids).size !== ids.length) {
    ctx.addIssue({ code: "custom", path: ["steps"], message: "復旧工程IDは一意にすること" });
  }
  if (new Set(sequences).size !== sequences.length) {
    ctx.addIssue({ code: "custom", path: ["steps"], message: "復旧順序は一意にすること" });
  }
  const byId = new Map(data.steps.map((step) => [step.id, step]));
  data.steps.forEach((step, index) => {
    if (new Set(step.dependsOn).size !== step.dependsOn.length) {
      ctx.addIssue({ code: "custom", path: ["steps", index, "dependsOn"], message: "前提工程は重複できない" });
    }
    step.dependsOn.forEach((dependency) => {
      const prerequisite = byId.get(dependency);
      if (!prerequisite) {
        ctx.addIssue({ code: "custom", path: ["steps", index, "dependsOn"], message: `未登録の前提工程: ${dependency}` });
      } else if (prerequisite.sequence >= step.sequence) {
        ctx.addIssue({ code: "custom", path: ["steps", index, "dependsOn"], message: "前提工程は依存先より前の順序にすること" });
      }
    });
    if (step.targetMinutes > data.rtoMinutes) {
      ctx.addIssue({ code: "custom", path: ["steps", index, "targetMinutes"], message: "各復旧目標は全体RTO以内にすること" });
    }
  });
  const ordered = [...data.steps].sort((a, b) => a.sequence - b.sequence);
  if (ordered.some((step, index) => index > 0 && step.targetMinutes <= ordered[index - 1].targetMinutes)) {
    ctx.addIssue({ code: "custom", path: ["steps"], message: "復旧目標時刻は順序に沿って増加させること" });
  }
});

export type Props = z.infer<typeof schema>;

meta.yaml

1.9 KB

/designs/disaster-recovery-runbook/source/meta.yaml

中身を見る
id: disaster-recovery-runbook
name:
  ja: 災害復旧手順・依存順序台帳
  en: Disaster Recovery Runbook
purpose:
  ja: サービス復旧の依存順序、RTO/RPO、責任者、実施操作、復旧検証をひと続きの手順にまとめる。
  en: Coordinate service recovery dependencies, RTO/RPO, owners, actions, and validation in one ordered runbook.
status: published
still: ./still.png
category: operations
catalogOrder: 870
jobs:
  - plan-sequence
  - assign-governance
useCases:
  - process
  - status
layoutKinds:
  - timeline
  - stack
tags:
  - disaster-recovery
  - business-continuity
  - rto
  - rpo
  - recovery-exercise
whenToUse:
  - ja: 複数サービスにまたがる停止から、依存順序と復旧目標時間を守って業務を戻すとき。
    en: Use to restore a multi-service operation in dependency order against recovery targets.
avoidWhen:
  - ja: 障害発生直後の重大度別応答と担当引き継ぎを定義する場合は、インシデント対応方針を使う。
    en: Use an incident response policy to define severity-based response and escalation immediately after an outage.
requiredInputs:
  - ja: システム、RTO/RPO、順序づけた工程、先行工程、目標時刻、責任者、復旧操作、検証条件を定義する。
    en: Define system, RTO/RPO, ordered steps, prerequisites, target time, owner, recovery action, and validation.
relatedLayouts:
  - incident-escalation-policy
  - system-landscape-map
dataSchema:
  title: string — 復旧計画の結論
  subtitle: string? — 対象環境・演習日・承認版
  system: string — 対象システム
  rtoMinutes: number — サービス復旧目標時間
  rpoMinutes: number — 許容データ損失時間
  steps: array (3–12) — 依存順序、目標時刻、操作、責任者、検証、準備状態を持つ復旧工程
sources: []

sample-data.json

1.7 KB

/designs/disaster-recovery-runbook/source/sample-data.json

中身を見る
{
  "title": "決済復旧は認証・キュー・台帳の順で行い、4時間以内に注文受付を再開する",
  "subtitle": "Production · exercise 2026-09-19 · approved runbook v3.2",
  "system": "Northstar Payments",
  "rtoMinutes": 240,
  "rpoMinutes": 15,
  "steps": [
    { "id": "DR-01", "sequence": 1, "service": "Identity provider", "dependsOn": [], "targetMinutes": 30, "action": "DRリージョンの認証クラスタを昇格し、ルーティングを切り替える。", "owner": "Identity On-call", "validation": "合成ログインが成功し、セッション発行遅延がSLO内。", "state": "ready" },
    { "id": "DR-02", "sequence": 2, "service": "Payment queue", "dependsOn": ["DR-01"], "targetMinutes": 75, "action": "永続キューを最新スナップショットから復元し、未処理イベントを再投入する。", "owner": "Payments Platform", "validation": "キュー長が安定し、イベント重複が発生しない。", "state": "exercised" },
    { "id": "DR-03", "sequence": 3, "service": "Order ledger", "dependsOn": ["DR-02"], "targetMinutes": 180, "action": "台帳を復元し、決済プロバイダーとの未照合取引を再照合する。", "owner": "Finance Operations", "validation": "残高照合が許容差内で、テスト注文を完了できる。", "state": "planned" },
    { "id": "DR-04", "sequence": 4, "service": "Checkout entry point", "dependsOn": ["DR-01", "DR-03"], "targetMinutes": 220, "action": "ヘルス確認を通した後、注文受付を段階的に再開する。", "owner": "Commerce Incident Lead", "validation": "地域別成功率とエラー率が通常SLO内である。", "state": "planned" }
  ]
}