2026年9月22日、携帯電話向けの軽い防災サイト bosai.ktai.jp を公開しました。地震・津波・警報・注意報を、1ページあたり約1〜2KB(gzip 圧縮後)で配っています。

https://bosai.ktai.jp/

この記事では、気象庁の電文を受信してから画面に出すまでの仕組みと、作る中で分かった「防災情報XMLの扱い方」「Cloudflare で配るときの注意点」をまとめます。気象庁の情報を使ったサービスを作る方の参考になればうれしいです。

作ったもの

画面内容
/ トップ警報が発表中の府県、最新の地震、津波(津波の警報・注意報の発表中は津波が先頭)
/w 警報・注意報58 の府県予報区等を地方ごとに一覧。府県の画面は 一次細分区域 → 地域 → 市町村 の順に、市町村ごとの発表中の種別
/q 地震最近の地震 20 件と、各地震の詳細・市区町村ごとの震度
/t 津波予報区ごとの津波警報・注意報・予報と観測
/aboutこのサイトについて(編集の責任・出典)

心がけたのは次の4つです。

  • 軽いこと。 公開した日の夜に測ると、トップ 1,326 B、警報・注意報 2,008 B、地震 1,629 B、津波 1,129 B(いずれも gzip 後)でした。上限を「平常時の画面は 2,048 B」「大地震・津波の警報中など行が増える画面は 12,288 B」と決め、試験で確かめています。HTTP Archive の Web Almanac 2024 によると、スマートフォン向けページの重さの中央値は 2,311 KB です(2024年10月の計測・出典)。
  • 外部のファイルを使わないこと。 JavaScript・CSS ファイル・画像・Web フォントを読み込まず、HTML 1つで完結します。文字のフォントは端末に入っているもの(BIZ UDPゴシック → メイリオ → 端末のゴシック体)を指定しています。
  • 気象庁の文字を変えないこと。 種別名・区域名・見出し・付加文は、気象庁が発表した文字列を1文字も変えずに載せます。
  • 止まらないこと。 受信から配信まで、東と西の2台のサーバーで同じものを動かしています。

背景と経緯

大きな災害が起きると、回線がつながりにくくなります。月末には通信量の制限で速度が落ちている人もいます。そういうときでも開けるサイトを目標にしました。対象は「速度制限や輻輳で遅くなった回線のスマートフォン」です。

以前、同じ目的で試験用のプロトタイプ(k-tai.bousai)を作っていました。bosai.ktai.jp はその置き換えで、気象庁の電文の受信から画面の配信までを自分のサーバーで持つ構成にしました。

時期(2026年)出来事
8月下旬受信(argus)を作って本番で動かす。2020年11月以降の過去の電文 約54万通を取り込む
9月上旬配備の仕組み(talos)と、解析・保存(mnemon)を作って本番で動かす
9月中旬配信(hermes)に着手
9月22日mnemon を「現在の状態を表に書く」形に作り直し、hermes とともに本番へ。bosai.ktai.jp を公開

なお、SNI(TLS の拡張)に対応しない古い携帯電話(2011年ごろより前のフィーチャーフォン)は対象外です。Cloudflare の無料プランの証明書は SNI が必須で、SNI なしで受けるには上位のプランが要るためです。

システムの構成

気象庁
  ↓ 防災情報XML の電文
DMDATA.JP
  ↓ WebSocket(東と西のサーバーがそれぞれ受信)
argus(受信)        電文の原本を DB に保存する
  ↓ 同じサーバー内の UDP 通知 + 0.5 秒ごとの確認
mnemon(解析・保存) 電文を解析し、履歴の表と「現在の状態」の表を書く
  ↓ 同じサーバー内の UDP 通知 + 0.5 秒ごとの確認
hermes(配信)       画面を組んでメモリに持ち、HTTP で返す
  ↓ Cloudflare Tunnel(サーバーごとに1本)
Cloudflare(Load Balancing + キャッシュ)

利用者(https://bosai.ktai.jp/)

東と西のサーバー(Linode・Ubuntu)にそれぞれ argus・mnemon・hermes が1つずつ入っていて、DB(Azure SQL Database)は1つを共有しています。どちらか1台が止まっても、もう1台で受信から配信まで続けられます。

受信・解析と保存・配信を別のプログラムに分けたのは、1つのプログラムの不具合で他が止まらないようにするためです。hermes を止めても配信が止まるだけで、受信と保存は続きます。

名前役割名前の由来
argus受信ギリシャ神話の百の目の巨人。眠るときも目を半分ずつしか閉じず、全部は眠らない。片方が止まっても受信を止めないという要件に合わせた
mnemon解析・保存古代ギリシャの都市国家で、契約や判決を記録し保管した公職(μνήμονες)の名
hermes配信神々の伝令。4つのうち唯一、利用者の目に触れる名前

受信(argus)

  • DMDATA.JP から WebSocket で電文を受け、原本のまま DB の表に保存します。東のサーバーは DMDATA の東京の接続先、西は大阪の接続先に固定しています。
  • 東西の2台は同じ電文をそれぞれ受け、同じ表に書きます。電文の番号(OriginalId)を主キーにして IGNORE_DUP_KEY = ON を付け、先に書いた側の行だけが残り、後から書いた側は 0 行で終わるようにしています。重複を調べる問い合わせは要りません。
  • 接続が切れたときは、最後に受けた時刻の 5 分前から、DMDATA の電文リストの API で切れていた間の電文を取り直します。
  • 2020年11月18日以降の過去の電文(約54万通)も一括で取り込んであり、原本の表は 70 GB を超えています。
  • argus は電文の意味(震源や震度など)を解釈しません。解釈は mnemon の仕事です。

解析と保存(mnemon)

原本は2段階で読む

新着を探すときは索引(区分・受信時刻・電文種別)だけを読み、電文の番号を集めます。本文は、扱う種別の電文だけを主キーで1通ずつ取ります。

こうしているのは、最初の版で事故を起こしたからです。未処理の電文を探す問い合わせが、70 GB を超える本文から先に読む実行計画になり、DB の I/O が 100% のまま下がらず、argus の保存まで止まりました。プログラムを分けていても、DB を共有していると影響は伝わります。大きな本文を持つ表では、範囲検索に本文の列を混ぜないのが安全です。

新着にすぐ気付く

新着の確認は2つの方法を併用しています。

  1. 0.5 秒ごとの確認。 索引のシーク1回で、新着が無ければ 0 行で終わります。
  2. 同じサーバー内の UDP 通知。 argus は電文を保存した直後に、127.0.0.1 の UDP ポートへ1行送ります。mnemon はそれを受けたら待たずに 1 を行います。

通知の中身には頼らず、「何か来たので今すぐ確認する」合図としてだけ使います。UDP なので届かないこともありますが、そのときは 0.5 秒ごとの確認で気付きます。mnemon から hermes へも同じ形で知らせています。

DB 側から通知を受ける方法も考えましたが、Azure SQL Database は Service Broker に対応していないため(Microsoft の機能比較表)、Service Broker を使う通知は使えません。

「現在の状態」は、書き込むときに確定する

電文が届くたびに、履歴の表への追加と「現在の状態」の表の書き換えを、1つのトランザクションで行います。配信側は「いま何が発表中か」を、主キーか索引のシーク1回で読めます。

最初の版では、全履歴から最新の電文を選ぶビューを配信側に読ませていました。区域などの条件を付ければ 1 ミリ秒で返りますが、付けないと 60 秒たっても返りません。配信側が欲しいのは「いまの状態」を1回の単純な読み取りで得ることだったので、9月22日に今の形へ作り直しました。

東西の2台で二重に処理しない

両方の mnemon がすべての電文を処理しようとし、処理済みの台帳の行(主キーは電文の番号)を先に書けた側が処理します。

ここで本番でデッドロックが起きました。最初は東西が同じ地震の続報を同時に書き換えたことによるもので、地震ごとに排他制御をかけましたが、それでも起き続けました。開発機に本番の電文 1,200 通を複製し、2つの mnemon を同時に動かして再現すると 111 件起き、原因が分かりました。一括 INSERT(SqlBulkCopy)の外部キーの検査が、相手が書いている途中の、連番が隣の履歴の行まで読みに行き、互いに待ち合っていました。

排他制御を系統(地震・津波・警報・注意報)ごとに1つ(SQL Server の sp_getapplock)にすると、再現の試験では 0 件になりました。同じ系統の電文は東西で交互に1通ずつ書くことになりますが、1通の処理は 0.1〜1 秒なので、新着が遅れることはありません。

スポンサーリンク

防災情報XMLの扱い方

いちばん時間をかけたのは、「どの電文が最新か」「いつ消えるか」を決めることでした。自分で決めず、気象庁と DMDATA の資料に書かれているとおりにしています。資料に書かれていない所だけ自分で決め、「資料に定めが無いので、こう決めた」と設計書に残しています。

第1版で扱う電文は次のとおりです。

系統電文
地震震度速報・震源に関する情報・震源・震度に関する情報・震源要素更新のお知らせ・長周期地震動に関する観測情報(VXSE51/52/53/61/62)
津波津波警報・注意報・予報、津波情報(VTSE41/51/52)
警報・注意報気象警報・注意報(VPWW55〜61)と、集約通報(VPWS50)

全系統に共通の規則

規則出典
最新は発表時刻が最も新しい電文。通番(serialNo)の順では決めない気象庁「防災情報XMLフォーマット運用指針」2.1.3.2
取消の電文が最新なら、その情報を現在の状態から消す。前の電文には戻さない同 2.1.3.4
訂正・遅延の電文は、発表と同じに扱う同 2.1.3.4
電文に失効時刻(validDateTime)があれば、その時刻に「発表なし」になる同 2.1.3.3
それ以外に、時間の経過だけで情報を消す規則は置かない気象庁 FAQ「警報や注意報は…解除されるまでは有効」(出典
訓練・試験の電文は履歴にだけ入れ、現在の状態には使わないDMDATA の共通ヘッダの定義(通常以外は内部利用にとどめる)

警報・注意報に期限を付けない

最初の版では、「最後の発表から 48 時間たった警報・注意報は、現在の状態から外す」という規則を、念のための規則として入れていました。

しかし気象庁の FAQ は「警報や注意報は…解除されるまでは有効」と書いています。本番のデータで確かめると、48 時間を超えて新しい電文が無いまま発表中だった例が 146 件あり、調べた時点で、発表中の注意報が 3 組、現在の状態の表から消えていました。

いまは、消えるのは「解除」「取消」「電文に書かれた失効時刻」の3つだけです。

地震:項目ごとに採る電文を決める

1つの地震について、震度速報・震源に関する情報・震源・震度に関する情報などが別々に届きます。情報の単位は(地震の識別番号 EventId, 電文種別)で、たとえば「震源・震度に関する情報」の取消は、その種別だけを消し、先に出た震度速報は消えません。

画面に出す項目は、項目ごとに採る電文の順を決めています。

項目採る順
震源(震央地名・緯度経度・深さ・規模)震源要素更新のお知らせ(VXSE61)> 震源・震度に関する情報(VXSE53)> 震源に関する情報(VXSE52)
最大震度と区域ごとの震度VXSE53 > 長周期地震動に関する観測情報(VXSE62)> 震度速報(VXSE51)
見出しと付加文VXSE53 > VXSE51 > VXSE52

注意したいのは見出しと付加文です。「最後に届いた電文」の見出しを出すと、後から届く VXSE61 や VXSE62 の見出しになり、「この地震による津波の心配はありません」のような付加文が画面から消えます。

震度速報しか届いていない地震は震源がまだ無いので、地震の時刻は仮に targetDateTime を使い、震源が届いたら発生時刻(originTime)に置き換えます。

津波:識別番号を分解し、最新1通を全量として扱う

  • 津波の電文の eventId は、複数の地震にかかわるとき空白で区切って並びます(「A B」)。この電文は A と B の両方に属するものとして扱い、取消の照合も個々の番号で行います(気象庁「地震火山関連XML電文解説資料」共通別紙)。
  • 津波警報・注意報・予報(VTSE41)は、最新の1通がその時点の全量です。前の電文にあって次の電文に無い予報区は、次の電文の時点で何も発表されていません。
  • 解除の予報区も行として残し、「解除」と明示された事実を出せるようにしています。
  • 若干の海面変動だけになったときは、電文に失効時刻(validDateTime)が付きます。その時刻を過ぎたら「発表なし」として出します。
  • ただし、失効時刻の無い津波予報の電文が出ていた時期があります。気象庁が 2023年12月14日に不具合と認め、2024年2月15日に解消したものです。当該電文については、データベースパッチを実施して、失効状態にすることで、画面に表示されないように修正しています。

警報・注意報:現象別の電文と集約通報

  • 2026年5月28日の体系整理の後は、現象別の気象警報・注意報(VPWW55〜61)と集約通報(VPWS50)で現在の状態を作れます(DMDATA の weather-warning の概要)。
  • VPWW55〜61 は1通が、発表した気象台の担当区域全部について、その現象の状態の全量を持ちます。届いたら、その電文が載せた区域の、その現象の種別を置き換えます。
  • 集約通報(VPWS50)は約10分ごとに届き、全国の全区域・全現象の状態を持ちます(1通に約 2,400 区域)。起動直後の状態はこれで作ります。届いたときは、区域と現象ごとに、持っている状態より発表時刻が新しいときだけ置き換えます。
  • 気象警報の取消の電文は、資料に運用の記述が無く本文もありません。現在の状態は変えずに記録だけ残し、10 分以内に届く次の集約通報が状態を正します。

配信(hermes)

  • mnemon の「現在の状態」の表を読み、全画面を組んでメモリに持ちます(素の HTML と gzip 済みの両方と ETag)。要求が来たらメモリから返すだけで、要求のたびに DB を読みません。
  • DB に届かない間も、手元にある最後の画面を配り続けます。
  • 変わったことには、0.5 秒ごとの変更記録の確認と、mnemon からの UDP 通知で気付きます。変わった系統の画面(地震なら トップ・地震一覧・その地震の詳細)だけを組み直し、15 分に1回は全画面を組み直します。
  • DB には読み取り専用のユーザーで接続します。インターネットに公開される部品に、書き込みの権限を持たせないためです。

画面に出すときに守っていることは次のとおりです。気象庁の情報を使うサービスでは、どれも確かめておく価値があります。

守っていること出典
気象庁の文字列(種別名・区域名・見出し・付加文・震央地名)は1文字も変えない。縮めない・組み替えない気象庁ホームページの利用規約(国が作成したかのような形で公表・利用しない)、運用指針 3.1
各画面に、編集したのが bosai.ne.jp であることと出典(気象庁)を書く運用指針 3.1(編集していること、その責任が編集者にあることの明示)
サイト自身の文は「当サイトの注記:」で始め、警戒・避難などの呼びかけの語を書かない気象業務法 第23条(気象庁以外の者による警報の禁止)
画面の時刻は「9/22 22:23:30 現在」のように日付つきで書く。「N 秒前」のような相対の表現は使わない組んだ時点で固まる文字列なので、相対の表現は時間がたつと正しくなくなる
「発表なし」と「情報が届いていない」を区別する区域の行が無いときに「発表なし」と言い切らない

Cloudflare で配る

bosai.ktai.jp(Cloudflare for SaaS の custom hostname)
  → Load Balancing(東西2台・/health を 60 秒ごとに確認)
  → Cloudflare Tunnel(サーバーごとに1本)
  → hermes(各サーバーの 127.0.0.1:8080)
  • サーバーは外からの接続を受けるポートを開けていません。Cloudflare からは Tunnel(サーバーから Cloudflare へ張る接続)だけで届きます。
  • 東西の振り分けは Load Balancing(月 5 ドル)です。hermes の /health は「全画面を組み終えていて、DB の確認が 10 秒以内に成功している」ときだけ 200 を返し、そうでない台には振り分けられません。
  • 公開の名前は ktai.jp のドメインに、設定と契約は bosai.ne.jp のドメインに置いています。bosai.ktai.jp を bosai.ne.jp の Cloudflare for SaaS の custom hostname として受けると、bosai.ne.jp のゾーンの設定(Load Balancing・キャッシュの規則・消去)が効きます。

応答のヘッダは、ブラウザ向けと Cloudflare 向けで分けています。

Cache-Control: no-cache
CDN-Cache-Control: max-age=60, stale-if-error=300
ETag: W/"…"

ブラウザは毎回問い合わせ、中身が同じなら 304 だけを受け取ります。Cloudflare は 60 秒持ち、hermes がエラーを返す間は 300 秒まで前の版を出します。

画面の中身が変わったら、hermes が Cloudflare のキャッシュの消去(Purge)API を呼びます。0.3 秒の間に変わった分をまとめて送り(1回の要求に 100 URL まで)、4 秒後に同じ URL をもう一度送ります。http と https の両方の URL を消します(キャッシュの鍵に scheme が含まれるため)。17 分の観測では、画面が変わった 3 秒後に Cloudflare が新しい版を取りに来ていました。

設定の中で分かったことをまとめます。

分かったこと対応
HTML を書き換える機能(Email Obfuscation・Automatic HTTPS Rewrites・Server-side Excludes・Replace insecure JS libraries)が有効だと、応答から ETag が消え、304 が使えない4つとも無効にした。Cloudflare が自分で 304 を返すようになった
custom hostname のキャッシュを消すには、SaaS を設定した側(bosai.ne.jp)のゾーンに送る必要がある。ktai.jp のゾーンに送ると success: true が返るのに、何も消えないhermes は起動時に送り先のゾーンの名前を確かめ、違えば起動しない
SaaS の fallback origin に、Load Balancer の名前を直接には指定できない(プロキシ有効の A / AAAA / CNAME である必要がある)プロキシ有効の CNAME を1つ作り、Load Balancer の名前へ向けた
Load Balancer のプールに、同じ Tunnel を2回入れることはできないTunnel をサーバーごとに1本作った
すべてのプールが不健康で fallback pool が無いと、Cloudflare は 530 を返すfallback pool を同じプールにし、両方の台が不健康でも配り続けるようにした

配備(talos)

talos は、argus・mnemon・hermes の配備の手順を1か所にまとめたリポジトリです。各リポジトリは talos のワークフロー(GitHub Actions の再利用ワークフロー)をタグ(@v1)で固定して呼ぶだけにしています。@main で呼ぶと、talos の変更がそのまま各サービスの本番の配備に影響するためです。

  • タグ v* を push すると、ビルド → 試験 → 片系ずつ配備 → 起動ログの版の照合、と進み、版が違えば自動で前の版に戻します。
  • 各プログラムは起動時に DB の表の版を確かめ、合わなければ終了コード 3 で止まります。この場合も前の版に戻ります。
  • 戻すだけのワークフローはビルドを通しません。壊れているときに使うものだからです。
  • GitHub には root の権限を渡していません。預けるのは配備用ユーザーの鍵だけで、できることは sudoers に書いた systemctl の数コマンドに限られます。DB のパスワードなどの秘密は GitHub を通さず、各サーバーの設定ファイルに置いています。

速さの実測

9月22日に、気象庁が電文を発信した時刻から、画面が新しくなるまでを測りました。

区間結果
気象庁の発信 → argus が DB に保存中央値 地震 1.2 秒(165 通)、警報・注意報 1.7 秒(1,313 通)
気象庁の発信 → 公開の画面が新しくなる地震 5.1 秒、警報・注意報 6.1 秒と 8.0 秒(30 分の観測で見えた例)

起点には電文の Control/DateTime を使いました。DMDATA の JSON の pressDateTime と秒まで一致します。発表時刻の reportDateTime や、DMDATA の封筒の head.time は分単位なので、秒単位の測定の起点には使えません。

使っている技術

部分使っているもの
言語C#(.NET 10)
サーバーLinode 2台(東西・Ubuntu)、systemd
DBAzure SQL Database(Standard S0)
電文の入手DMDATA.JP(WebSocket と電文リストの API)
公開の入口Cloudflare(Free プラン + Load Balancing、Cloudflare for SaaS、Tunnel、Cache Rule)
配備GitHub Actions(talos)
ログjournald、Better Stack

これから

  • 気象警報・注意報の時系列(市町村ごとの、今後1〜2日の危険度の予想)
  • 指定河川洪水予報・竜巻注意情報・気象防災速報
  • パソコン向けの画面
  • 警報・注意報の履歴

いずれも mnemon に電文の系統を足してから、hermes に画面を足す順で進めます。

まとめ

  • 受信・解析と保存・配信・配備を別の部品に分け、東西2台で動かしています。
  • 配信側が読む「現在の状態」は、電文を保存するときに確定して表に書いています。
  • 情報をいつ消すかは自分で決めず、気象庁と DMDATA の資料に書かれているとおりにしています。消えるのは解除・取消・電文に書かれた失効時刻だけです。
  • 画面はメモリに持ち、Cloudflare のキャッシュと消去の API で、軽さと速さを両立させています。

災害のときに回線が遅くても開ける防災サイトとして、ブックマークしていただけるとうれしいです。

https://bosai.ktai.jp/