Back to index

Disaster Recovery Runbook

Timeline
Generates documents
Disaster Recovery Runbook

Seen as a design

Take it with you

Copy and paste, or save a file and hand it over. For a machine with no network, one “Everything” file is enough.

Everything

disaster-recovery-runbook-everything.md

Everything in this panel, including the contents behind each URL, in one Markdown file

Prompt

1.6 KB
Build an HTML document with "Disaster Recovery Runbook". This layout runs, so there is no composition to rebuild — what it needs is content.

## What this layout does
Coordinate service recovery dependencies, RTO/RPO, owners, actions, and validation in one ordered runbook.

## What you have to supply
- Define system, RTO/RPO, ordered steps, prerequisites, target time, owner, recovery action, and validation.

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

schema.ts holds the machine-checkable validation. Ask rather than guess when an input is missing.

## When not to use it
- Use an incident response policy to define severity-based response and escalation immediately after an outage.

## How to build it
Put the input in a JSON file and run:

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

The output is one self-contained file and fetches nothing at render time.

## The source
- 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

Files

Template, input schema, sample data and metadata. This document was generated from those four. The generated document

template.tsx

6.9 KB

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

Show contents
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

Show contents
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

Show contents
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

Show contents
{
  "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" }
  ]
}