Hyperdrive で Cloudflare Workers から GCP の PostgreSQL に繋いだ(Tunnel + Access 経由)
Cloudflare Workers から、GCE の VM 上で動いている PostgreSQL に Hyperdrive + Cloudflare Tunnel 経由で接続できたという記録。DB はインターネットに一切晒していない。
背景
CMS を Workers に移すにあたって、DB をどこに置くかが論点だった。DB ごとマネージドに移す案もあったが、DB を動かさないのが一番安く・速く・リスクが低い。問題は「Workers から VM の PostgreSQL にどう繋ぐか」だった。
制約は 2 つ。
- Workers はリクエストごとに isolate が使い捨てられるので、通常のコネクションプールが張れない。毎回接続しに行くと TCP + TLS ハンドシェイクで数百 ms 溶ける
- PostgreSQL をインターネットに公開したくない
Hyperdrive が両方を解く
1 に対して: Hyperdrive は Cloudflare 側でコネクションを温め続けるので、isolate 側は毎回ハンドシェイクしなくて済む。Workers 有料プランに含まれ、クエリ課金も egress 課金もない。
2 に対して: Hyperdrive は Cloudflare Tunnel 経由で非公開の DB に接続できる。VM ではすでに cloudflared が動いていたので、既存トンネルに TCP ルートを 1 本足すだけで済んだ。認証は Cloudflare Access の service token。
結果の経路:
Workers (CMS)
→ Hyperdrive
→ Cloudflare Access(service token 認証)
→ Cloudflare Tunnel
→ GCE VM の PostgreSQL (TLS)
Cloudflare 側と GCP 側が、公開ポートを一切開けずに繋がっている。
設定でハマった点
Access 経由のときはポートを指定しない
wrangler hyperdrive create で Access 経由の設定を作るとき、--access-client-id と --origin-port は併用できない。Tunnel の公開ホスト名経由でルーティングされるため、ポートを入れると繋がらない。
wrangler hyperdrive create <name> \
--host=<tunnel-hostname> \
--database=<db> --user=<user> \
--password <認証情報のため省略> \
--access-client-id='<id>' --access-client-secret='<secret>' \
--sslmode=require
CLI はフラグレベルで排他にしてくれているので気づけるが、ダッシュボードで作る場合はポート欄を空のままにする必要がある。
作成コマンドの成功 = 経路の検証が終わっている
この作成コマンドが成功すること自体が、Tunnel + Access + TLS が全部正しく繋がった証明になる。 Hyperdrive は設定作成時に実接続を試すため。ここが通れば経路側の切り分けは不要。
sslmode は実装によって意味が違う
Hyperdrive は接続先 DB が TLS を話すことを要求するので、VM の PostgreSQL には自己署名証明書を入れてある。
- Hyperdrive 側の設定は
requireのままでよい(Cloudflare の実装は libpq 準拠 = 暗号化するが検証しない) - node-postgres から直接 Tunnel 越しに繋ぐ側は
requireだと弾かれる。node-postgres は libpq と違ってrequireでも検証する
cannot connect to Postgres. Details: self-signed certificate
pg-connection-string の実装を見ると、rejectUnauthorized を外すのは no-verify のときだけだった。
case 'no-verify': {
config.ssl.rejectUnauthorized = false
break
}
同じパラメータ名で意味が違うので混乱しやすい。
アプリ側で必要だったこと
プールは Hyperdrive が持つ
アプリ側のコネクションプールは実質無効化して、リクエストを跨いだコネクション共有を止める。
postgresAdapter({ pg, pool: { connectionString, maxUses: 1 } })
pg を Workers 上で動かす
node-postgres は Workers を検出すると pg-cloudflare の CloudflareSocket を使う。ところがこのパッケージの exports は workerd 条件のときだけ実体を返し、それ以外は空スタブ(exports.default = {})を返す。
"exports": {
".": {
"workerd": { "import": "./esm/index.mjs", "require": "./dist/index.js" },
"default": "./dist/empty.js"
}
}
OpenNext の esbuild は workerd 条件を使うが、その前段の Next.js(webpack)は使わない。webpack がバンドルした時点で空スタブが確定し、b2 is not a constructor で落ちていた。webpack のバンドル対象から外して解決。
// next.config.ts
serverExternalPackages: ['pg', 'pg-cloudflare'],
外部化すると今度は esbuild 後の default import の解釈がずれて pg.Pool が undefined になったので、モジュール実体を明示的に渡して回避した。
import * as pgNamespace from 'pg';
const pg = (pgNamespace as any).default ?? pgNamespace;
結果
| 項目 | 値 |
|---|---|
| API 応答 | 約 70 ms |
| 管理画面の初回応答 | 約 1.9 秒(コールドスタート込み) |
| DB の追加費用 | ゼロ(VM に据え置き) |
| DB の公開ポート | なし(Tunnel の内側のまま) |
| ダウンタイム | ゼロ |
学び
- Workers に載せられないのはアプリではなく DB。DB を動かさない構成にすれば、移行リスクとコストの大半が消える
- Hyperdrive は Tunnel 経由で非公開 DB に繋げる。Cloudflare と GCP を、DB をインターネットに晒さずに繋げられる。Workers 有料プランに含まれ追加課金なし
- Access 経由の Hyperdrive 設定ではポートを指定しない
wrangler hyperdrive createが通れば経路の検証は終わっているsslmodeは実装によって意味が違う。libpq / Hyperdrive のrequireは検証しない、node-postgres のrequireは検証する