NGW開発BLOG
← 記事一覧へ
·#084

バージョン表記に β を付けたベータ版の決め手は、機能が揃ったことではなかった

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

上げた

ログイン画面の、ボタンの下の小さな一行が変わった。

NGW Linux のログイン画面。キーボードのキートップを写した背景の上に、LoginID・Password の入力欄と青い LOGIN ボタンが並ぶ。ボタンの下に「ver. β1.0.8.1.0 connect 192.168.0.178」と版数と接続先、その下に団体を選ぶセレクトボックス

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つだ。届いたメールを開いて、「収受転送」をクリックする。

NGW に組み込んだメール画面。左に受信箱の一覧、右に開いたメールの本文。ツールバーの左端に「収受転送」ボタンがあり、返信・全員に返信・転送・削除・マークと並ぶ。開いているのは添付PDF付きのテストメールで、見本資料.pdf が本文の上に表示されている

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

収受転送で開いた収受登録の画面。上部に「このメールには添付が1件あります。登録すると添付書類に自動で取り込みます。」という帯が出ている。左側は年度・所属と、文書区分・文書記号・分掌事務・簿冊の選択欄、右側は受発者にメールの差出人アドレス、件名にメールの件名、その下にメール本文がそのまま入っている

差出人が受発者に、件名が件名に、本文が本文に入っている。画面の上には、こう出る。

このメールには添付が1件あります。登録すると添付書類に自動で取り込みます。

あとは普段どおり、分掌事務と簿冊を選んで収受文書として登録し、決裁に上げればいい。添付書類の欄には、メールに付いてきたPDFがそのまま入っている。

印刷も、保存し直しも、打ち直しも要らない。メール本体は Roundcube を NGW の中に組み込んで扱っているが、作ったのはメーラーではない。受発簿の入口を、もう1つ開けた、というのがこの機能の位置づけだ。

「動く」と「出せる」のあいだ

そして、2段目の「道具」の話になる。ここが、この記事で書きたいほうだ。

動くものを作るのは、開発の仕事だ。出せるかどうかは、別の質問で決まる。

自治体の閉じたネットワークの中の、Mac mini が NGW のサーバである。現地に直せる人はいない。担当者はいずれ替わる。私もいずれいなくなる。その条件のもとで「出せます」と言うには、4つ答えられないといけなかった。

  1. 止まらないか — 誰も見ていない夜に、勝手に立ち直るか
  2. 戻せるか — 壊したとき、どこまで戻れるか
  3. 気づけるか — 壊れたことに、誰かが気づく仕組みがあるか
  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:04VM が正常停止(MySQL フラッシュ済み)
18:40:26Mac 起動完了(kern.boottime で確認)
18:41:07VM 復帰

総ダウンタイム 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回していることだった。

  1. 前日に塞いだ Apache の封鎖設定が、レプリカ機には入っていなかった(同期の対象は webroot と DB だけで、/etc は運ばれない)
  2. 保守コンソールが、2か所にある初期設定スクリプトの古いほうを叩いていた
  3. メールが、複製元の 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文字は、そのうち取れる。取れる日を決めるのは、これから現地で出てくるもののほうだ。