Cloudflare Workersから自前のPostgreSQLを使う:ポートを開けずにつなぎ、1台を複数のサービスで分ける
全文検索とベクトル検索が要るので、Workersのサイトから、VM上のPostgreSQLを使う構成にした。Hyperdrive、Workers VPC、Tunnelでつなぎ、ロールで負荷を分け、バックアップとバッチをVMに置いた記録。
データベースだけは自分で持つ、という選び方もできます。外から入るポートを開けず、Tunnelで内側から外へつなぎ、サイトはHyperdrive経由で読む。サービスごとにロールで時間と接続数を絞れば、1台を複数のサービスで分け合えます。
なぜ、データベースだけ自分で持つのか
サーバーを持たないのが、小さなチームの基本です(個人開発・スタートアップ向けのスタックでは、DBもD1にしました)。けれど、あるサービスでは、次の3つが同時に要りました。
- 日本語の全文検索:題名や人名を、1〜2文字の語でも、部分一致で速く探したい
- ベクトル検索:「疲れた夜に癒されたい」のような言葉に近い商品を、意味で探したい
- 約48万件の商品:それぞれにJSONの詳しい情報があり、毎日、外部のAPIから更新される
PostgreSQLなら、PGroonga(日本語に強い全文検索)とpgvector(ベクトル検索)を、同じテーブルに対して使えます。そこで、サイトはWorkersのまま、データベースだけをVMに置く構成にしました。この記事は、その構成と、運用して分かったことの記録です。
料金、制限、ベータ版の機能は変わります。使う前に、各社の公式ドキュメントを確かめてください。Workers VPCは、2026年10月の時点でベータ版です。
全体の構成
サイトのWorkerから、VMのPostgreSQLまでの経路は、次のとおりです。
- Hyperdrive:Workerからの接続を、PostgreSQLへの少数の接続にまとめる(接続プール)。リクエストのたびに、遠いDBと接続をやり直さずに済む
- Workers VPC:Hyperdriveが、インターネットに公開していない宛先(Tunnelの先の
localhost:5432)に届くための入口 - Cloudflare Tunnel:VMの中の
cloudflaredが、内側から Cloudflareへ接続を張る。だから、VMに外から入るポートは要らない - PostgreSQL:
localhostだけで待ち受ける。インターネットからは、直接つなげない
VMに入る経路は、管理のためのSSHだけです。それも、ファイアウォールでGoogleのIAP(踏み台の仕組み)のアドレス範囲だけを許可し、gcloud compute ssh --tunnel-through-iap で入ります。
以前のつなぎ方との違い
最初は、Hyperdriveの公式手順にある「Tunnel + Access」でつないでいました。Tunnelに公開ホスト名を付け、Cloudflare Accessのサービストークンを持つHyperdriveだけを通す形です。今は、Workers VPCを使う形に変えました。
| Tunnel + Access | Workers VPC | |
|---|---|---|
| 公開ホスト名 | 要る(db.<ドメイン> など) | 要らない |
| Accessのアプリとサービストークン | 要る | 要らない |
| Hyperdriveに渡す秘密 | DBのパスワード、トークンのIDとシークレット | DBのパスワード |
| 状態 | 一般提供 | ベータ版(2026年10月) |
ホスト名とトークンが減ると、ローテーションするものと、設定を間違える場所が減ります。ベータ版であることは承知の上で、移しました。
1台を、複数のサービスで分ける
このVMには、サービスごとにデータベースを作っています。サービスを足すスクリプトは、1つのサービスに、2つのロールを作ります。
- 持ち主のロール(
<name>):テーブルを作り、毎日の更新を書き込む - サイト用のロール(
<name>_web):サイトのWorkerが使う。読むことと、必要な表への書き込みだけ
サイト用のロールには、次の制限を付けています。
CREATE ROLE "<name>_web" LOGIN PASSWORD '...' CONNECTION LIMIT 20;
ALTER ROLE "<name>_web" SET statement_timeout = '3s';
REVOKE ALL ON DATABASE "<name>" FROM PUBLIC;- 3秒で打ち切る:1つのサービスの重いクエリが、ほかのサービスを道連れにしない。サイト側は、打ち切られたら、条件を減らした検索に切り替える
- 接続は20まで:Hyperdriveの接続プールが、DBの接続を食いつぶさない
- ほかのDBに入れない:どのデータベースからも、「誰でも接続してよい」(
PUBLICのCONNECT)を外す。あるサービスのパスワードが漏れても、ほかのサービスのデータは読めない
実際に、ある検索のSQLが遅いプランを引いて、4秒かかっていたことがあります。3秒で打ち切られるので、その検索が失敗するだけで、ほかのサービスには響きません(原因と直し方は、UNION ALLの記事に書きました)。
Terraformで作るもの、作らないもの
VMとネットワークは、Terraformで作っています。
- VM:東京リージョン、
e2-standard-2(2 vCPU、8 GB)、ディスク50 GB - 専用のネットワークと、IAPからのSSHだけを許可するファイアウォール
- ディスクのスナップショット:毎日、30日分
- 予算アラート:プロジェクト全体で、月15,000円(50%と100%でメール)
秘密は、Terraformに入れません。 DBのパスワードや、Tunnelのトークンを変数に入れると、Terraformの状態ファイルに平文で残るからです。秘密は、サーバーに入ってから、画面に出さずに入れます。たとえば、Tunnelのトークンは、手元で入力して、SSH越しに root だけが読めるファイルへ流し込みます。
read -rs -p "Tunnel token for $NAME (not shown): " TOKEN; echo
printf 'TUNNEL_TOKEN=%s\n' "$TOKEN" | gcloud compute ssh pg-main "${Z[@]}" \
--command "sudo tee /etc/cloudflared-$NAME.env >/dev/null && sudo chmod 600 /etc/cloudflared-$NAME.env"
unset TOKENTunnelとHyperdriveは、Workerと同じCloudflareアカウントに置く必要があります。別のアカウントのサイトからも使うため、VMには、アカウントごとに cloudflared を1つずつ動かしています。
バックアップと、毎日のバッチ
データを自分で持つと、バックアップも自分の仕事になります。
- ディスクのスナップショット:毎日、30日分(GCPが取る)
- 論理バックアップ:毎日、全DBを1つずつ
pg_dumpし、ロールと一緒に、オブジェクトストレージ(R2)へ送る。14日分 - 戻せるかの確認:月に1回、別のDBに
pg_restoreしてみる。戻したことのないバックアップは、ないのと同じ
外部のAPIから商品を取り込む処理も、同じVMの systemd タイマーで動かしています。毎日の全体の更新、1日3回の差分の同期、1時間ごとの補足情報の取得です。DBのすぐ隣で動くので、何十万行の書き込みでも、ネットワークを何度も往復しません。
運用して分かったこと
- ベクトルの索引は、メモリに載る大きさで考える:pgvectorのHNSW索引(1024次元をhalfvecで持ち、約48万件で約1.3 GB)が、4 GBのVMのメモリには収まりませんでした。8 GBに上げ、PostgreSQLのメモリ設定(
shared_buffersをメモリの1/4など)を、VMの大きさに合わせて直すスクリプトを用意しました - 手元の開発は、本物と同じ拡張を入れたコンテナで:
wrangler devでは、Hyperdriveの代わりにlocalConnectionStringで手元のDBにつなぎます。PGroongaとpgvectorを入れたイメージを使うと、本番と同じSQLを試せます - 重いバッチと、サイトの検索がぶつからない時間に:毎日の更新は、アクセスの少ない早朝に置きました
- DBを止めずに大きくできる部分と、できない部分を知っておく:ディスクは止めずに増やせますが、VMの種類を変えると数分止まります
これが向く場合、向かない場合
| 向く | 向かない |
|---|---|
| PostgreSQLの拡張(全文検索、ベクトル)が要る | D1などの、管理の要らないDBで足りる |
| データが大きく、毎日まとめて更新する | 書き込みが多く、世界中から速く書きたい |
| バックアップと更新を、自分で見られる | DBの面倒を見る人がいない |
DBを自分で持つと、OSの更新、ディスクの空き、バックアップの確認が、自分の仕事に戻ってきます。それでも、サイトの側は、Workersのまま「サーバーを持たない」形を保てます。必要なものだけを自分で持つ、という分け方です。
確認リスト
- DBのポートを、インターネットに開けていないか(Tunnelで内側から張っているか)
- サイト用のロールに、時間の打ち切りと、接続数の上限があるか
- ほかのサービスのDBに、接続できないようにしたか
- パスワードやトークンが、Terraformの状態ファイルや、シェルの履歴に残っていないか
- バックアップを、実際に戻して確かめたか
- 索引の大きさが、メモリに収まっているか