← 記事一覧へ

Cloudflare Workersから自前のPostgreSQLを使う:ポートを開けずにつなぎ、1台を複数のサービスで分ける

全文検索とベクトル検索が要るので、Workersのサイトから、VM上のPostgreSQLを使う構成にした。Hyperdrive、Workers VPC、Tunnelでつなぎ、ロールで負荷を分け、バックアップとバッチをVMに置いた記録。

AI作成 · 約11分 · Workersと、PostgreSQLを少し触ったことがある人向け
PostgreSQLCloudflareHyperdriveTerraform
この記事で扱うこと

データベースだけは自分で持つ、という選び方もできます。外から入るポートを開けず、Tunnelで内側から外へつなぎ、サイトはHyperdrive経由で読む。サービスごとにロールで時間と接続数を絞れば、1台を複数のサービスで分け合えます。

なぜ、データベースだけ自分で持つのか

サーバーを持たないのが、小さなチームの基本です(個人開発・スタートアップ向けのスタックでは、DBもD1にしました)。けれど、あるサービスでは、次の3つが同時に要りました。

  • 日本語の全文検索:題名や人名を、1〜2文字の語でも、部分一致で速く探したい
  • ベクトル検索:「疲れた夜に癒されたい」のような言葉に近い商品を、意味で探したい
  • 約48万件の商品:それぞれにJSONの詳しい情報があり、毎日、外部のAPIから更新される

PostgreSQLなら、PGroonga(日本語に強い全文検索)とpgvector(ベクトル検索)を、同じテーブルに対して使えます。そこで、サイトはWorkersのまま、データベースだけをVMに置く構成にしました。この記事は、その構成と、運用して分かったことの記録です。

料金、制限、ベータ版の機能は変わります。使う前に、各社の公式ドキュメントを確かめてください。Workers VPCは、2026年10月の時点でベータ版です。

全体の構成

サイトのWorkerから、VMのPostgreSQLまでの経路は、次のとおりです。

WorkersのサイトからVMのPostgreSQLまでの経路訪問者CloudflareサイトのWorker画面と検索のAPIHyperdrive接続をまとめて使い回すWorkers VPC公開していない宛先への入口接続はVMの内側から張るVM(GCP 東京)cloudflaredTunnelの、VM側の端PostgreSQL 17localhostだけ。PGroonga、pgvectorsystemdタイマー毎日の取り込みバックアップpg_dump → R2外から入れるのは、IAP経由のSSHだけ
図1 サイトはWorkersのまま、DBだけをVMに置く。VMは外から入るポートを開けず、cloudflaredが内側からCloudflareへ接続を張る。取り込みとバックアップは、DBの隣で動く。
  • 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 + AccessWorkers VPC
公開ホスト名要る(db.<ドメイン> など)要らない
Accessのアプリとサービストークン要る要らない
Hyperdriveに渡す秘密DBのパスワード、トークンのIDとシークレットDBのパスワード
状態一般提供ベータ版(2026年10月)

ホスト名とトークンが減ると、ローテーションするものと、設定を間違える場所が減ります。ベータ版であることは承知の上で、移しました。

1台を、複数のサービスで分ける

このVMには、サービスごとにデータベースを作っています。サービスを足すスクリプトは、1つのサービスに、2つのロールを作ります。

  • 持ち主のロール(<name>):テーブルを作り、毎日の更新を書き込む
  • サイト用のロール(<name>_web):サイトのWorkerが使う。読むことと、必要な表への書き込みだけ

サイト用のロールには、次の制限を付けています。

add-database.sh(抜粋)
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 だけが読めるファイルへ流し込みます。

add-cloudflared.sh(抜粋)
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 TOKEN

Tunnelと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の状態ファイルや、シェルの履歴に残っていないか
  • バックアップを、実際に戻して確かめたか
  • 索引の大きさが、メモリに収まっているか

公式リファレンス

Cloudflare Hyperdrive — Documentation ↗Hyperdrive — Connect to a private database using Workers VPC ↗Cloudflare Workers VPC ↗Cloudflare Tunnel ↗Google Cloud — Using IAP for TCP forwarding ↗PGroonga ↗pgvector ↗
PAIBOT / DEVELOPMENT JOURNAL記事一覧に戻る →

知りたいことを探す

タイトル・タグ・本文から検索 esc で閉じる