SSLとDB再起動を「見える化」するステータスページをつくってみた:暗号化されているかと、止まっていた時間を1画面で

対象: 前の記事でSSL強制を試して、「で、運用している人はこれを毎回どう確認するの?」が気になった方
所要時間: 30〜40分 / 使うもの: ブラウザ、ターミナル、Supabaseのプロジェクト(Freeプランで可)
確認日: 2026年10月(画面や料金は変わることがあります)
前の記事(手順編): SupabaseでSSL接続を強制する手順と、設定変更時のデータベース再起動で起きること
ビジネス編(やさしい解説): AIでアプリをつくる人が見落としがちな「通信の暗号化」:データの通り道に鍵をかけて顧客の信頼を守る
サンプルコード一式(GitHub): GitHub
記事の中のコードは日本語コメントです。リポジトリの本体は英語コメントで、日本語コメント版(この記事と同じもの)は ja/ フォルダーにあります。

前の記事では、「SSLを強制」のスイッチを押して、何が起きるかを手作業で確かめました。psql でわざと間違ったパスワードを試したり、SQL Editorで起動時刻を聞いたり。

終わってみて、思いました。これ、毎回やるのは大変だな。そして、これを見たい人は、SQLを書かない人かもしれない。

運用の担当者、情報システムの人、受託開発で「うちのデータ、暗号化されてますか?」とお客さまに聞かれた人。そんな人が見たいのは、きっと1枚の画面です。そこで、前の記事で確かめた3つのこと(暗号化・再起動・止まっていた時間)を、ステータスページにしてみました。

0. 先に伝えておきたいこと(正直ベース)

  • 今回のページが見ているのは、アプリ(Edge Function)からデータベースへの接続だけです。外から psql などでつなぐ接続は、前の記事のスクリプトで確かめます(同じ画面では見られません)。
  • 「止まっていた時間」は、定期ジョブ(cron)の実行が途切れた時間から求めた目安です。ユーザーが実際に困った時間そのものではありません。
  • データは、小さな空のデモプロジェクトのものです。
  • 費用:このページは30秒ごとに security-status 関数を呼びます。1つのタブを開きっぱなしにすると、1日に約2,880回、1か月で約8万6千回の呼び出しです(計算した目安)。Freeプランの月50万回(ドキュメントより)に対して、1枚のタブで約17%です。タブを何枚も開きっぱなしにすると、すぐ増えます。 私は実際の使用量は測っていません。他にかかるお金はありません。
  • セキュリティ:このページには、データベースの稼働状況が出ます。関数はVerify JWTをオフにして、「アプリのpublishableキー」を確認するだけの簡単な守りにしています。社外に公開するページには、本物のログインを付けてください。
  • 未検証:verify-full と証明書での接続、大きなデータベースでの再起動、Fast database reboot の実測、スマホでの表示。

1. つくったもの:接続セキュリティ・ステータス

ステータスページ(再起動の約6分後)

画面は4つのブロックでできています。

ブロック 見えること どこから取る?
アプリ → データベースの接続 暗号化されているか、TLSのバージョン、暗号方式 pg_stat_ssl
データベースの稼働 最後の再起動の時刻、稼働時間 pg_postmaster_start_time()
定期ジョブの実行:直近60分 1分ごとの実行回数(棒グラフ) cron.job_run_details
止まっていた時間帯 直近72時間で、実行が25秒より長く途切れた所 同上
全体像

まず驚いたことがあります。前の記事では、「Edge FunctionからDBへの接続は、最初からSSLだと、ドキュメントに書かれていた」とだけ書いて、私は確かめていませんでした。今回、データベース自身に聞いたら、TLS 1.3、TLS_AES_256_GCM_SHA384(256bit)と返ってきました。「ドキュメントでは…」が、「私の環境で確かめました」に変わりました。

2. つくり方

API(Edge Function)

security-status は、読むだけの関数です。3つのことを聞きます。

// この関数 → データベースの接続が暗号化されているか(pg_stat_ssl は、自分の接続の状態を見られる)
const [conn] = await sql`
  select ssl, version as tls_version, cipher, bits
  from pg_stat_ssl where pid = pg_backend_pid()`;

// Postgresがいつ起動したか(=最後の再起動)
const [db] = await sql`
  select pg_postmaster_start_time() as started_at, now() as now,
         current_setting('server_version') as server_version`;

「止まっていた時間」は、cronの実行履歴から、前の実行との間隔が25秒を超えた所を拾います。10秒ごとのジョブなので、25秒を超えたら、何かあったと考えます。

with runs as (
  select start_time, lag(start_time) over (order by start_time) as prev
  from cron.job_run_details where start_time > now() - interval '72 hours')
select prev as from_ts, start_time as to_ts,
       extract(epoch from (start_time - prev))::int as seconds
from runs where start_time - prev > interval '25 seconds'
order by start_time desc limit 20;

前の記事と同じ考え方(cronはデータベースの中で動くので、DBが止まっていると実行されない)です。ただし、これは10秒ごとのジョブが1つだけある前提です。ジョブが複数あると、間隔の計算がずれます。

関数の入り口では、前の記事のアプリと同じように、publishableキーが付いているかを確認しています(Verify JWTはオフのまま)。

画面

画面は、HTML 1枚です。30秒ごとに関数を呼んで、結果を描き直します。ライブラリは使っていません。棒グラフもSVGを自分で組み立てています。

動かす

  1. 前の記事のように、10秒ごとのcronジョブを用意します(sql/00_heartbeat_cron.sql)。
  2. security-status を、Edge Functionとしてデプロイして、Verify JWTはオフにします。
  3. app/config.example.js を config.js にコピーして、URLとpublishableキーを入れます。
  4. 配信します。 bash cd app python3 -m http.server 8765 --bind 127.0.0.1 # → http://127.0.0.1:8765/index.html

3. 実際にプロジェクトを再起動してみた

見えるようにしたのなら、再起動で何が見えるかを試したくなりました。ただ、その前に一つ、私は勘違いをしていました。

前の記事で測った「約20秒」は、SSL強制のスイッチで起きた再起動です。ドキュメントには、この設定の変更は「fast database reboot(データベースだけの高速な再起動)」を起こす、と書いてありました。一方、ダッシュボードの Project Settings → General → Restart project は、別物のようです。画面には「Your project will not be available for a few minutes.」とあり、横の ▼ を開くと、こう出ます(2026年10月、私の画面での表示です)。

Fast database reboot
Restarts only the database service, with less downtime than a full project restart. Other project services remain running.

つまり、メインの Restart project は、データベースだけでなくプロジェクト全体(データベースの上で動くAPIやログイン、ストレージなどのサービスも含めた一式)の再起動で、fast database reboot は、それよりも軽い別メニューです。内部で何をどの順番で再起動しているのかは、私が調べた範囲では公開された説明が見つかりませんでした(ここは分かりません)。

操作 何が再起動するか 画面・ドキュメントの案内
SSL強制のスイッチ データベースのみ(fast database reboot) ドキュメント:小さなプロジェクトでは通常数秒、大きいと長くなることも。同じページの別の箇所と確認ダイアログには「数分」とも。
Restart project プロジェクト全体 画面:数分間、プロジェクトが使えない
▼ の Fast database reboot データベースのみ Restart projectより短いダウンタイム

そこで、Restart project のほうを押して、外から10秒に1回くらいの間隔で security-status を呼び続けました。

経過 結果
〜3秒 正常
7秒 エラー(HTTP 500)
32秒 応答なし(タイムアウト)
67秒 エラー(HTTP 500)
74秒 正常。起動時刻が新しくなっていた

応答が戻るまで、約70秒でした。ステータスページの「止まっていた時間帯」には、こう出ました。

回 きっかけ ページが示した空白
1回目 前の記事でSSL強制のスイッチ(fast database reboot) 約20秒(前の記事の手作業の測定)
2回目 今回、Restart project 1分17秒
3回目 今回、もう一度 Restart project 1分29秒

1回目と2・3回目は、同じ「再起動」でも別の操作です。最初は「約20秒は運が良かった1回」と考えていましたが、データベースだけの再起動と、全体の再起動の違いのほうが、ありそうな説明です。ただし、▼ の Fast database reboot を、私は自分では測っていません。SSL強制のスイッチが fast database reboot だというのも、ドキュメントの記述からの推測で、画面で確認したわけではありません。どれも小さな空のデモでの1〜2回の結果で、本番のデータ量では変わるはずです。

本番で選ぶなら、「データベースの設定を変えたいだけ」のときは fast database reboot、「全体を立ち上げ直したい」ときだけ Restart project、と使い分けられそうです(私は試していません)。どちらも、止まっている間は security-status のようなAPIも応答しません。

4. 見つけた問題と、直したところ

① 障害のとき、古い表示が「いまの状態」のように見えた

再起動の最中に、ページを更新してみました。

再起動の最中のページ(直す前)

上にはエラーが出ていますが、その下はふだんどおり「暗号化されています」「稼働 6分18秒」のままです。これは状態を伝えるページとして、まずいです。 止まっているのに、緑の丸が出たままで、稼働時間も止まったまま。見た人は「大丈夫そう」と思ってしまいます。

直し方は、取得に失敗したら、表示を灰色にして、「何時のものか」を書くことです。

} catch (e) {
  showErr('状態を読めませんでした:' + e.message + (lastOk ? `。下の表示は ${時刻} 時点の古い情報です。` : ''))
  if (lastOk) document.querySelector('main').classList.add('stale')   // 灰色にする
}

直したあとの画面です(実際の再起動中ではなく、通信を止めて、同じ状況をつくって撮りました)。

失敗したときは、灰色で「古い情報です」と出す

② 古い再起動は「原因は不明」になる

画面の「止まっていた時間帯」で、いちばん新しい再起動だけが「データベースの再起動」と判定されています。2回目の空白は、「不明」です。

再起動の直後。新しい空白だけが「再起動」と判定された

原因は、Postgresが教えてくれる起動時刻が最後の1回分だけだからです。過去の再起動の時刻は、どこにも残っていません。ページの注意書きにも、そう書きました。

ほんとうは、定期的に起動時刻を表に記録しておけば、過去の再起動も判定できそうです(私は試していません)。

③ 「不明」の空白は、私の作業だった

2回目より前の「3分3秒」の空白は、この日、私がcronジョブを消して作り直した時間でした。ステータスページは、それが「再起動」ではなく「ジョブの停止・作り直し」かもしれない、ということまでは分かりません。空白の原因は、ページだけでは決められない、というのが正直なところです。

④ 再起動の直後は、丸を橙にした

起動してから5分未満のときは、稼働時間の丸を橙にして、「再起動して間もありません」と出しました(上の画像)。

5. 実際のWebアプリ(運用画面)にするなら

どれも、私は試していません。

  • ログイン:今は、キーを知っていれば誰でも見られます。社内向けなら、ログインした人だけに。
  • 過去の記録:起動時刻や、空白を表に残すと、半年前の再起動も振り返れます。
  • 通知:空白や「暗号化されていない」を見つけたら、メッセージで知らせる。
  • 外からの確認:「SSLなしでの接続が断られること」を、サーバー側から定期的に試して、結果を表示する。
  • ジョブが複数ある場合:ジョブごとに間隔を見る。

まとめ

  • 暗号化・再起動・止まっていた時間を、1枚のステータスページにまとめられた
  • データベース自身に聞くと、TLS 1.3で接続していることが確認できた(ドキュメントの説明を、自分の環境で確かめられた)
  • Restart project(プロジェクト全体の再起動)では、応答が戻るまで約70秒。止まっていた時間帯は、1分17秒・1分29秒。前の記事の約20秒は、データベースだけの再起動(SSL強制のスイッチ)で、別の操作だった
  • 状態のページは、失敗したときの見せ方が大事。古い表示を、そのまま出さない
  • 空白の原因は、ページだけでは決められないことがある

コードは、上のGitHubにあります。次は、「会社ごとに見えるデータを分ける」(RLS)を、ログインできる請求書ポータルにしてみます。

Leave a Reply

Your email address will not be published. Required fields are marked *