設計の背景/ Design Rationale
何を守るために、どこに線を引いたか
この章では、記録と検知の作りを決めた判断を順に説明します。どの判断も「素朴に作ると何が空くか」から出発しています。個別の実装は次章以降で扱うので、ここではなぜその形なのかだけを扱います。
解決する業務課題The Problem
やり取りをメールと紙から Web へ移すと、操作そのものは 1 か所で起きるようになります。ただし記録は自動では残りません。残す仕組みを作らなければ、Web に移した分だけ「後から辿れない操作」が増えます。
実際に問い合わせとして起きるのは、次のような場面です。いずれも「その時点で誰かが書き留めていたか」だけが答えを決めます。
| 聞かれること | 答えるために必要な記録 | 記録が無いと |
|---|---|---|
| 取引先に渡した資料の最新版はどれか。誰がいつ差し替えたか | ファイルの差し替えと版の記録 | ファイルの更新日時しか残らず、誰がどの版に替えたかは復元できない |
| この見積を、どなたが受け取って開いたか | ダウンロードとプレビュー、共有リンク経由のアクセスの記録 | 「送ったはず」までしか言えない |
| 先月このフォルダを触ったのは社内の誰と、取引先のどなたか | Box 単位で時系列に並んだ操作の履歴 | 心当たりを人に聞いて回ることになる |
| 退職者が最後に何を持ち出したか | 操作者で絞り込める全社の記録 | 調べる手段が無い |
| 先週の通知メールが届いていないが、送信自体は行われたのか | 配信の状態と、その滞留を判定する仕組み | 本人が申告するまで誰も気づかない |
最後の 1 行は種類が違います。前の 4 つは「起きたことを残す」話ですが、これは「起きていないことに気づく」話です。サーバが落ちればクラウドの監視が拾いますが、サーバは正常に動いていて通知メールだけが出ていない、という故障は外からは見えません。判定にはアプリのデータベースの中を数える必要があります。この資料が扱う 2 つの柱は、この 2 種類にそれぞれ対応しています。
記録を 1 本に集めるOne Table
記録の置き場は 1 本のテーブルだけです。「Box のメンバーに見せる用の履歴テーブル」を別に持つ案は採りませんでした。
別に持つと、同じ出来事を 2 か所へ書くことになります。片方だけ書けた瞬間に食い違いが生まれ、しかもそれはあとから直せません(どちらが正しいかを判定する材料が無い)。書き込みの回数も増えます。読む側の都合で書く側を増やすのは、割に合わない取引でした。
そこで書き込み経路を 1 本に固定し、読む側を 3 つに分けました。分けるのは読み取り時なので、食い違いが起きる余地がありません。読み口ごとに「出す項目」と「見せる相手」が違うだけで、元にしている行は同一です。
業務データと同じ区切りで書く
記録は業務処理の中で、業務データと同じトランザクションで書きます。ファイルの行を作る処理と、その記録を残す処理が同じ区切りに入っているので、「ファイルはできているのに記録が無い」という状態が構造的に生じません。逆に記録に失敗すれば業務処理ごと巻き戻ります。
追記だけを許すAppend-Only
監査の記録は「都合の悪い行を後から消せない」ことに価値があります。松葉ではこれをアプリの実装で守っていません。データベースの権限設計で守っています。
データベースには行ごとのアクセス制御があり、「誰がどの行に対して参照・追加・更新・削除をしてよいか」を規則として書きます。記録のテーブルについては、更新と削除の規則をそもそも書いていません。書いていない操作は既定で拒否されるため、システム管理者のアカウントであっても更新も削除もできません。「権限を絞る」のではなく「規則が存在しない」形にしてあるので、権限の付け替えで緩むこともありません。
三重の防御
消せないことは、次の 3 つが重なって成り立っています。1 つ目は改変の防止、2 つ目は事故からの復旧、3 つ目は保持期間を超えたあとの保全です。
| 層 | 内容 | 守る相手 |
|---|---|---|
| ① 追記だけの規則 | 更新と削除の許可規則を作らない。参照はシステム管理者のみ。Box のメンバーは専用の関数経由だけ | 意図的な改変・アプリの不具合による書き換え |
| ② 自動バックアップ | データベース側の自動バックアップと、一定期間の任意時点への巻き戻し | テーブルごと失う規模の事故 |
| ③ 外部への退避 | 25 ヶ月を超えた月を圧縮した CSV としてオブジェクトストレージへ移し、行数と検査値を突き合わせて保全する | 保持期間を超えた分の消失 |
「参照できない」ことを書き込みの形の強制に使う
記録の追加は全員に許していますが、参照はシステム管理者だけです。この組み合わせには副作用があります。データベースには「行を追加して、追加した行をそのまま返す」書き方がありますが、返す動作は参照に当たるため、一般の利用者がその書き方をすると失敗します。そこで追加は「返さない一括追加」の形だけを使う規律にしています。規律を破ると本番で必ず落ちるので、レビューの見落としがあっても混入しません。
同じ記録を 3 つの読み口で出すThree Readers
読み口ごとに「出す量」を変えています。量を変える基準は、その読み口が何のためにあるか、そして誰が同席するかです。
| 読み口 | 目的 | 出す量とその理由 | 代わりに置いた歯止め |
|---|---|---|---|
| 全社の監査ログ | 不正・事故の調査。退職者の持ち出しの確認 | 生のまま全部。接続元の IP や端末情報を伏せると調査の役に立たないため、伏せない | システム管理者に限定。取り出しは理由を 10〜256 文字で必須、1 時間 5 回まで。閲覧そのものを毎回記録する |
| Box のアクティビティ | 業務の履歴を関係者で共有する | 業務の状態を変えた操作だけ。IP・端末情報・内部の識別子は業務判断に使わないので出さない。1 つの Box には社内と取引先が同席するため、社外の担当者名やメールも伏せ字にする | 権限の判定と項目の絞り込みをデータベース側の関数に閉じ込め、アプリからは迂回できない形にする |
| 退避分の取り出し | 2 年より前を掘る例外的な調査 | その月のまるごと(圧縮した CSV) | 15 分だけ有効な URL を都度発行。理由を必須にし、発行そのものを記録する |
とくに 2 番目には気を配りました。Box は「案件や取引先ごとの共有単位」で、1 つの Box に社内の担当者と取引先の担当者が同時に入ります。ここで内部情報が出ると、取引先 A が取引先 B の担当者名やアドレスを知り得ます。この読み口では、出してよい項目を「対応表」として列挙し、表に無い項目は通り道そのものが無い形にしています(詳細はアクティビティの章で扱います)。
クラウドの監視と別に、自前の検知を持つ理由Two Kinds of Monitoring
クラウド側の監視は稼働しています。それでもアプリ自身の検知を別に持っているのは、両者が見ているものが違うからです。
| 監視 | 見えるもの | 見えないもの |
|---|---|---|
| クラウド側の監視 | サービスの応答が返らない。エラー率が上がった。バッチが異常終了した。キューに未配信が溜まった | 「サービスは正常に応答しているのに、業務が前に進んでいない」状態 |
| アプリ自身の検知 | 送信待ちの通知が 30 分以上そのまま。日次の同期が 2 日連続で失敗。メールの不達率が一定の割合を超えた。削除の当日通知が作られていない。ウイルス検査が一定の件数以上滞留している | インフラそのものの故障(そちらはクラウド側が拾う) |
右の列の事象は、どれもアプリのテーブルを数えないと判定できません。「送信待ちのまま 30 分」は通知の宛先の状態を見る話で、サービスの応答時間には現れません。だからアプリ側に検知を置いています。
実際に起きた例
本番では、ファイルの検査完了を伝えるイベントが、発行の権限が足りないために送れず、再試行の上限に達して溜まりました。画面にはエラーが出ず、通知メールだけが出ないという形の故障です。クラウド側から見ればサービスは正常に応答しており、異常な指標も出ていません。
これは 5 分ごとに回る検知器が拾いました。アラートには内訳(どの種類のイベントが何件、いちばん古いものはいつ発生したか、直近のエラーは何か)が残り、システム管理者が画面から確認できる状態になりました。
設計の四原則Design Principles
以降の章で出てくる個々の判断は、すべて次の 4 つのどれかに紐づいています。
記録は消せないImmutable by Construction
更新と削除の許可規則をデータベースに作らない。権限の設定を間違えても、規則が無いものは緩みません。保持期間を超えた分は、行数と検査値を突き合わせてから外部へ移します。
見せる範囲はデータベース側で固定するAuthorization in the Database
「ダウンロード履歴を一般のメンバーに見せない」といった境界を、アプリの条件分岐ではなくデータベースの関数の中に固定します。アプリ側の不具合で漏れる経路を作りません。
気づけない欠落を作らないNo Silent Gaps
数値が取れなかったときに 0 件と報告しない。検知器は互いに独立に回し、1 つの失敗で残りを止めない。前回の値が読めないときは、黙るのではなく鳴らす側に倒す。
気づいた人がその場で手を打てるActionable in One Place
アラートを表示して終わりにしない。何が起きたかの生データ、次にやることの案内、そして失敗したイベントの再投入や滞留した通知の再送まで、同じ画面から届くようにする。