バージョン表記に β を付けたベータ版の決め手は、機能が揃ったことではなかった
8月21日、メールを「収受転送」で添付ごと収受文書にできるメール連携が完成した。併せて道具が揃い、残るのは現地でしかできない試験だけになった。だからログイン画面の版数に β が付いた。夜3時の67秒、52時間の誤警報、複製した機体で死んだメール、そして再試験でのログ増分0行。
上げた
ログイン画面の、ボタンの下の小さな一行が変わった。

ver. β1.0.8.1.0 connect 192.168.0.178
β を付けた。 NGW を、ベータ版に上げた。
ロゴが「NGW Linux」になっているのも、この夏の産物だ。初めての Linux サーバを立てて、構築を Linux チームへ渡すところまで来た。接続先の IP が出ているのは、機体を取り違える事故を何度もやりかけたからで、これも今年足したものだ。
上げるまでの流れは、3段だった。
1. メール連携が完成した。 8月21日。長く要望をもらいながら、なかなか手が回っていなかった機能だ。
2. 併せて、道具が充実した。 この夏に作っていたものを並べると、機能より道具のほうがずっと多い。
3. 残るのは、現地でしかできない試験だけになった。
3段目が言えるようになったから、上げた。「作るものが無くなった」ではなく、「残っているのは現地に持って行く分だけだ」と言える状態になった、ということだ。
メールで届いた文書も、収受になった
最初に、その最後の1つの話をする。
受発簿の記事で、収受をこう書いた。「郵便や電子メール等で送られてくる文書を受け付け、担当部署に振り分け、決裁・回覧し、簿冊に保管する作業である」。
「電子メール等」の部分が、長いあいだ言葉だけだった。 実際には、職員がメールを開いて、印刷するか、添付を保存し直して、NGW の収受登録の画面にもう一度打ち込んでいた。文書の入口が2つあって、片方だけが手作業だった。
8月21日に入れたのは、そこをつなぐボタン1つだ。届いたメールを開いて、「収受転送」をクリックする。

押した先は、いつもの収受登録の画面だ。

差出人が受発者に、件名が件名に、本文が本文に入っている。画面の上には、こう出る。
このメールには添付が1件あります。登録すると添付書類に自動で取り込みます。
あとは普段どおり、分掌事務と簿冊を選んで収受文書として登録し、決裁に上げればいい。添付書類の欄には、メールに付いてきたPDFがそのまま入っている。
印刷も、保存し直しも、打ち直しも要らない。メール本体は Roundcube を NGW の中に組み込んで扱っているが、作ったのはメーラーではない。受発簿の入口を、もう1つ開けた、というのがこの機能の位置づけだ。
「動く」と「出せる」のあいだ
そして、2段目の「道具」の話になる。ここが、この記事で書きたいほうだ。
動くものを作るのは、開発の仕事だ。出せるかどうかは、別の質問で決まる。
自治体の閉じたネットワークの中の、Mac mini が NGW のサーバである。現地に直せる人はいない。担当者はいずれ替わる。私もいずれいなくなる。その条件のもとで「出せます」と言うには、4つ答えられないといけなかった。
- 止まらないか — 誰も見ていない夜に、勝手に立ち直るか
- 戻せるか — 壊したとき、どこまで戻れるか
- 気づけるか — 壊れたことに、誰かが気づく仕組みがあるか
- 同時でも正しいか — 何十人が一斉に同じことをしても、正しさが崩れないか
この夏やっていたのは、ほとんどがこの4つを埋める作業だった。
4つ目には、負荷試験の記事で答えを出してある。出てきたのは性能の問題ではなく、正しさの問題ばかりだったという話なので、ここでは繰り返さない。この4つの外にあったCGW との連携も同じで、あちらは「現場の仕事に届くか」という別の問いだった。
答えは全部、道具の形で用意することになった。手順書ではなく、ボタンとして。理由は最後にもう一度書く。
1. 止まらない — 夜3時の67秒
最初に埋めたのが、無人運用だった。毎晩3時に、ひとりでに立ち直る。VM を正常に停止し、Mac 本体を再起動し、自動で復帰する。落ちていたら見張りが立て直す。
仕掛け自体は半日で書けた。時間を食ったのは、そのあとに潰した5つの落とし穴のほうだ。
- MAMP が :80 を握っていると、VM が起動しない。しかも見張りは「MAMP の応答」を VM の生存と誤認する
22 → 22のポート転送が、Mac 自身の SSH を消す- VM を作り直すとホスト鍵が変わり、
poweroffが空振りする。MySQL がフラッシュされないまま電源が抜ける - macOS のオートメーション許可が無いと、
utmctlは権限エラーではなく「そんな VM は無い」という顔で失敗する - 起動していない .app でも、別プロセスからは書き換えられない
5つに共通していたのは、どれも「動いているように見えて、ある日静かに止まる」種類だったことだ。エラーは出ない。ログに1行残るだけで、翌朝サーバは動いている。
潰したあと、実機で通した。
| 時刻 | 出来事 |
|---|---|
| 18:40:00 | 夜間ジョブ発火 |
| 18:40:04 | VM が正常停止(MySQL フラッシュ済み) |
| 18:40:26 | Mac 起動完了(kern.boottime で確認) |
| 18:41:07 | VM 復帰 |
総ダウンタイム 67秒。 以後80回連続で正常。
ここで大事なのは秒数ではなく、確かめ方のほうだった。停止が正常だったかは MySQL の Shutdown complete で、再起動したかは kern.boottime で、復帰したかは HTTP の応答で見た。ログに出た文字ではなく、機械が残した事実で確かめる。 バックアップを「取れている」ではなく「戻せる」で確かめたのと、同じ形だ。
2. 戻せる — 完璧なバックアップは、たぶん無い
次が、戻す手段だった。7月27日と7月29日の2日で、ここをまるごと作り直している。
始める前の時点で、重ねてあったものはこれだけあった。MySQL のレプリケーション。5分ごとの添付ファイル同期。DBダンプ27世代と添付7世代。外付けディスクへの退避。毎晩の無人再起動とウォッチドッグ。
これだけ重ねていて、穴が3つ出た。
- 添付ファイルの同期は、5分ごとに失敗し続けていた(設定にポート番号が無く、VM ではなく Mac 本体の SSH を叩いていた)
- 待機機の監視は、ホスト鍵の食い違いで誤報を出していた
- 本番機から複製したデモ機が、本番の待機機を真のミラーで上書きしかけた
どれも「設定した時点では正しかった」ものだ。鍵を作り直した、レプリケーションを入れた、機体を複製した。構成を変えたときに、古い前提だけが静かに取り残される。
そのうえで、いちばん効いた気づきがこれだった。
レプリカは、バックアップではない。
誤削除は、レプリケーションが正常であるほど確実に待機機へ伝わる。レプリカは事故のコピーを作るだけで、戻せない。戻せるのは世代だけだ。そしてその世代は、待機機の外付けSSD 1台にしかなかった。現在データの複製は2つあるのに、過去に戻れる履歴は1つしかない。
だから、本番機が待機機の世代を毎晩「預かる」ようにした。日次の最新1本と、週次の最新1本。1.4GB を47秒で。最新1本だけを上書きしていくと、事故に気づくのが遅れたときに手元に残るのが「事故後のデータ」になるからだ。
そして最後に、事故の側から引ける表を1枚書いた。
| 起きたこと | 使うもの | どこまで戻るか |
|---|---|---|
| 🔴 本番機が停止した | 待機機を昇格 | 停止の直前まで |
| 🔴 DBを壊した・誤って削除した | 世代から復元 | 最大6時間前 |
| 🟠 添付を1個消した | 世代を横断検索して取り出す | 7日前まで/週次で3週前 |
| 🟠 外付けSSDが壊れた | 本番機の預かり 1.4GB | 昨日/1週間前 |
| 🔴 火災・盗難で2台とも失われた | 戻せません | — |
「バックアップはありますか」に、「何のバックアップですか」と聞き返せる状態になった。 ベータ版に上げると言えたのは、この表が書けたからだと思っている。最後の行を含めて、だ。
3. 気づける — 最初に壊れるのは、見張りのほう
7月29日の朝、ダッシュボードは本番機だけを赤くしていた。
NGWクローン(待機) freshness OK 最新 0h前
NGWメイン(本番) freshness CRIT 最新バックアップが52h前 — 停止の疑い
52時間。丸2日、誰も気づいていなかった。
追いかけたら、バックアップは一度も止まっていなかった。止まっていたのは、それを見に行く先だった。数日前に待機機の置き場を外付けディスクへ移していて、「相手の置き場はここ」という写しを持っていた本番機だけが、取り残されていた。
直し方は2つあった。値を正しく書き換えるか、値を持つのをやめるか。後者にした。監視のたびに相手の設定ファイルを読み直す。相手のことは相手に聞く。写しを持たない。
この日見つけた不具合は、結局どれもバックアップ本体の故障ではなかった。設定の写しがずれた。判定の書式が違った(echo が \t をタブにしないせいで、現役の世代21.9GBが「消していい」と表示された)。正常が警告色で出た。一度の取りこぼしが「落ちた」と履歴に記録された。
全部、見張る側の問題だった。
バックアップは、たいてい黙って動き続ける。壊れるのは、それを見ている仕組みのほうだ。そして見張りが壊れていることは、見張りでは分からない。
だから3台目を用意した。監視される2台のどちらかに置いたら、その機体と一緒に死ぬ。職員の端末で動く、別バイナリの監視アプリ。サーバを操作するコードを一切 import せず、テストでそれを縛った。壊す手段がバイナリに入っていなければ、構造として何もできない。
閉域網ではメールもクラウドも当てにできない。だから通知とは、誰かの視界に入っている画面のことだった。現場の要求も、はっきりしていた。
「色+履歴。事務所内では、音は基本的にミュートです」
無人再起動の時刻を 3:20 ではなく 3:16 にしたのも、同じ話だ。本番機のウォッチドッグは */15 で走る。3:20 だと次の点検まで10分しかなく、復帰が遅れた夜に毎回 CRIT が残る。たった4分だが、これは10年の話だった。毎晩鳴る警報は、1か月で無視されるようになる。そして無視される警報は、本当に鳴った日にも無視される。
4. あとから増えた問い — 複製しても、同じものが立つか
8月21日。メール連携が入った日に、もう1つ問いが増えた。
開発機のイメージから複製してデモ機を作ったら、そのメールが死んだ。
Oops... something went wrong!
複製しただけで、他には何も変えていない。ログには一行だけ出ていた。
DB Error: SQLSTATE[HY000] [1049] Unknown database '46223_imapdb'
46223 は複製元の団体コードだ。この機体は別の団体で、DB も正しい名前で作ってあり、中身も揃っていた。それなのに、存在しない複製元の DB を掴みに行っていた。
DB 名を持っている場所が、2つあったからだ。
| 場所 | 役割 | この機体の状態 |
|---|---|---|
| NGW の定数ファイル | メール設定を読むときに使う | 修正済み |
| Roundcube 側の設定ファイル | 接続先そのもの | 複製元のまま |
直したのは前者だけだった。後者は /opt の下にあって、そもそも「直すもの」として意識されていなかった。そして定数ファイルには、こう書いてあった。
//Webメール用DB(Roundcubeのconfig.inc.phpのDSNと必ず一致させること)
「必ず一致させること」と注意書きがある時点で、設計が負けている。 人間に同期を強いる作りは、いつか必ず食い違う。今回は「いつか」が1日で来ただけだ。
これは、52時間の誤警報とまったく同じ形をしている。あのときも、本番機が相手の設定の写しを持っていて、相手が動いた瞬間に嘘になった。直し方も同じにした。値を持つ場所を1つに絞る。 Roundcube 側は NGW の定数を見に行くだけにした。
そのうえで、定数が無い機体の既定値を、実在しない DB 名にしてある。意図的だ。複製した機体でいちばん怖いのは、落ちることではなく、間違った相手に静かに繋がることだから。
面白いのは、この日同じ構図の壊れ方を3回していることだった。
- 前日に塞いだ Apache の封鎖設定が、レプリカ機には入っていなかった(同期の対象は webroot と DB だけで、
/etcは運ばれない) - 保守コンソールが、2か所にある初期設定スクリプトの古いほうを叩いていた
- メールが、複製元の DB を掴んだ
3つとも原因は同じだ。設定を、機体の上にしか置いていない。 ソースは git にあり、レビューされ、複製され、配られる。だが設定は誰かの手で /etc や /opt に置かれ、そこで止まる。イメージを作り直せば消え、複製すれば古いまま残り、片方だけ直せば食い違う。
だから、機体側の設定をソースと同じリポジトリに入れた。当てるのは専用のスクリプトの仕事で、既定は下見(差分を見せるだけ)、明示したときだけ置き換える。置く前に控えを取り、Apache は構文検査に落ちたら控えから戻して reload しない。Roundcube の設定は php -l を通してから置く。壊れた設定を置けば、メールが丸ごと止まるからだ。
そして肝心なのは、これを保守コンソールの「スクリプト一式を設置」から自動で呼ばれるようにしたことだ。人が思い出して打つ手順は、いつか忘れられる。導入手順の中に埋めてしまえば、忘れようがない。
上げる直前に、ログを1行も出させない
同じ日、ベータ版へ上げるための検査もした。判定の基準を、こうしてある。
エラーが出ないことではなく、ログに1行も出ないこと。
既定値でごまかされて 200 を返しながら中身が壊れている不具合は、HTTP のステータスでは絶対に見つからない。負荷試験で出たのが全部「正しさ」の問題だったのと、同じ理屈だ。
全画面を叩く巡回517本と、30人が同時に収受・起案・決裁を回す負荷試験(2分で8,855リクエスト)を流した。出てきたのは、こういうものだった。
- PHP の識別子は UTF-8 を含められる。 HTML コメントに書いた
$_POST内容が変数として解釈され、警告が出続けていた - macOS は大文字小文字を区別しない。
NgwFooter.phpと書いた include が、Linux では開けずフッターが消えていた - 決裁を1件開くたびに、警告が1回。 2分の試験で176回
- PDF を1枚作るたびに、非推奨の関数で2行
どれも画面は正常に見える。ログを見なければ、気づけない。 全部直して、修正後の再試験ではログの増分が0行になった。
落ちていたテスト9本も直した。中身は3種類とも「テストの都合」だったが、最後のひとつが一番まずかった。配布パックに住民の文書が混ざっていないかを見る検査——コメントに「この機能で唯一許されない失敗」と書いてあるもの——が、絶対パスに正規表現を当てていたせいで誤検知していた。作業ディレクトリの名前にたまたま数字が含まれるだけで「団体データが混ざっている」と言い続ける。
赤が当たり前になっていると、本物の赤に気づけない。 これは毎晩鳴る警報の話と、同じことだった。
ベータ版で、残していること
上げると決めたときに、残っているものも一緒に書き出した。抜けと誤解されないために、承知のうえの割り切りは記録しておく必要がある。
- 火災・盗難で2台とも失われたら、戻せない。 2台は同じ建物にあり、外付けSSDもその場にある。履歴の実体は2箇所になったが、同じ場所の2箇所だ。閉域網ではクラウドへ逃がせない。物理的な持ち出しが唯一の手で、それは「やらない」と判断した
- 負荷試験で見つかった、決裁が止まったままの11件は自動では直せない。 申請者に上程し直してもらう一覧を出すところまでにした
- 団体固有の値を書くファイルだけは、今も手で編集する。 道具にもできたが、編集する場所が明確で、機体ごとに一度きりの作業だ。道具を増やすほうが損だと判断した。 何でも自動化すればいいわけではない
戻せないケースが残っていること自体は、たぶん永久に変わらない。完璧なバックアップというものは無くて、あるのは「壊れたときに気づけるバックアップ」と「気づけないバックアップ」だけだ。
数字で見る
| 項目 | 値 |
|---|---|
| 無人再起動のダウンタイム | 67秒(以後80回連続で正常) |
| 誤警報を出していた期間 | 52時間(原因は設定の写し) |
| 掃除で回収した容量 | 本番機 22GB / 待機機 21GB |
| 本番機が預かる世代 | 1.4GB / 47秒(日次1本+週次1本) |
| ベータ前の巡回検査 | 全画面517本 / 30人・2分で8,855リクエスト |
| 修正後のログ増分 | 0行(決裁1件ごとの警告176回も消えた) |
| 同じ日に同じ構図で壊れた回数 | 3回(設定を機体の上にしか置いていなかった) |
数字を並べて気づくのは、大きい数字がほとんど「減らしたもの」だということだった。52時間の見逃しを潰し、22GB のゴミを回収し、ログの増分を0行に。ベータ版に上げるためにやったことの大半は、足す作業ではなかった。
残っているのは、現地に持って行く分だけ
最後に、上げると決めた3段目に戻る。
メール連携が入って、作るはずだったものは無くなった。道具も揃った。それでも「完成しました」とは言えない。残っているのは、現地でしかできない試験だ。
この夏に踏んだ地雷は、ほとんどが実機でしか出ないものだった。MAMP がポートを握る。ホスト鍵が変わる。macOS が許可を出さない。USB ケーブルが遅い。複製した機体が複製元の DB を掴む。どれも、机の上では一度も起きない。 現地の機体には、現地の癖がある。
だから β を付けた。「作り終えた」の印ではなく、「ここから先は現地で確かめる」の印として。
AIとの協働作業での学び
この夏の記録を通して読み返すと、AI が正しかった場面と、私が正しかった場面が、きれいに交互に来ている。片方が一方的に正しい日は、1日も無かった。
AI 側の間違いは、実機でしか出ない形をしていた。sort -t'\t' はシェルでは2文字になる。echo はタブを出さない——これのせいで、現役のバックアップ世代21.9GB が「消していい」と画面に出た。イメージ拡張のときに無人再起動を止め忘れていて、2分ごとのウォッチドッグが停止中の VM を起こす作りになっていた。
私の側の見落としも、同じくらいあった。実測値を理論値のように図に書いた。添付の同期を「未設定」と報告したが、本当は5分ごとに失敗し続けていた。負荷試験のときは、対策と検証を同じ思い込みで書いて、直っていないものを一度「直った」と報告している。
対策と検証を同じ思い込みで書くと、直っていないものが直ったように見える。
これがこの夏いちばんの収穫だったかもしれない。検証は、対策を書いたのとは別の角度から書かないと意味がない。人と AI で分担する理由が、ここにある。
もうひとつ。一度だけ、道具の側が私を止めた日があった。「この機は定時バックアップを取っていません」という表示を見て、21.9GB を消そうとした場面だ。取っていた。cron に4行あった。表示のほうが間違っていた。直したのは表示だけではなく、判定できなかったときに危険な側へ倒れないようにしたことだ。行が欠けても壊れても、世代の削除は選択肢に出ない。
止められる側と止める側が、その都度入れ替わる。危ないところは、だいたい相手のほうが先に見つけた。
ベータ版に上げるというのは、機能に丸を付けていく作業ではなかった。壊れ方を先に書き出して、そのそれぞれに「気づける形」と「戻せる形」を用意していく作業だった。手順書ではなく、道具の形で。手順書は劣化する。ボタンは劣化しない。 壊れたときに、壊れたと分かる形で壊れるからだ。10年後にこの構成を引き継ぐ誰かに残るのは、動いているコードと、書いてある理由だけになる。
版数の頭に付いた1文字は、そのうち取れる。取れる日を決めるのは、これから現地で出てくるもののほうだ。