Inside of LOVOT

GROOVE X 技術ブログ

Cloud Run (Next.js) と Cloud SQL の接続構成

はじめに

こんにちは、junya です。Cloud Run上で動作するNext.jsアプリケーションから、Cloud SQL (PostgreSQL) データベースへ接続しようとしたところ、思いのほか苦戦したので、記録として残しておきます。

技術的選択肢と接続ライブラリ

選択肢が多々あり、その選択肢の意味を理解し、適切なものを選び、設定していくのが大変でした。Cloud RunとCloud SQLの連携には、主に以下の要素の組み合わせで構成が決まります。ここでは、Cloud Runからの接続方法を主軸に、それぞれの技術的選択肢とDrizzle ORM (Node.js) で利用するライブラリを整理します。

  • 前提となる選択肢

    • ネットワーク構成:

      • Public IP: Cloud SQLインスタンスにパブリックIPアドレスを割り当てます。

      • Private IP: VPCネットワーク内にのみIPアドレスを割り当てます。VPCコネクタが別途必要となります。

    • PostgreSQL接続方式:

      • TCP: localhost:5432のようなホスト名とポート番号で接続します。

      • Unix Socket: /cloudsql/[INSTANCE_CONNECTION_NAME]のようなファイルシステムのパスで接続します。

    • 認証方式:

      • パスワード認証: データベースにユーザー名とパスワードを設定します。

      • IAMデータベース認証: Google CloudのIAMプリンシパルを利用します。接続方法には主に以下の2種類があります。

        • Cloud SQL Auth Proxy: Proxy(またはConnectorライブラリ)が認証トークンの取得と利用を自動化します。

        • gcloud sql generate-login-token: gcloudコマンドで一時的な認証トークン(パスワードとして使用)を手動で生成します。

  • Cloud Runからの接続方法の選択肢

    • Cloud SQL Auth Proxy経由

      • 概要: Cloud Runの機能として、アプリケーションコンテナと同一環境でCloud SQL Auth Proxyコンテナを起動します。アプリケーションは、TCP (localhost) またはUnixソケット経由でこのProxyに接続します。

      • 利用ライブラリ (Drizzle):

        • drizzle-orm

        • pg (node-postgres) または postgres (postgres.js)

        • @google-cloud/cloud-sql-connector
      • 備考:
        • Cloud Run には Cloud SQL Auth Proxy を自動で動かすオプションがあります。
        • pg (または postgres) 単体では、IAM 認証がうまくいかず、 @google-cloud/cloud-sql-connector の力を借りました。
    • VPC経由の直接接続

      • 概要: VPCコネクタを利用してCloud RunとCloud SQL間のネットワーク経路を確立します。データベースのPrivate IPに直接接続します。認証にはユーザー名とパスワードを使用し、これらの認証情報はSecret Managerで管理するのが一般的です。

      • 利用ライブラリ (Drizzle):

        • drizzle-orm

        • pg (node-postgres) または postgres (postgres.js)

      • 備考: Secret Manager 等で認証情報を管理

    • @google-cloud/cloud-sql-connector の活用

      • 概要: Cloud SQL Proxy の代わりに、 @google-cloud/cloud-sql-connector で proxy する方法です。詳しくは知らないので、気になる方は調べて下さい。

これらの選択肢の組み合わせによって設定方法が異なり、特にライブラリと認証方式の相性によって問題が発生することがあります。

動作した構成

最終的に動作した構成を共有します。

  • 接続方式: Cloud SQL Auth Proxy
    • Cloud Run : Cloud Run 設定で有効化
    • 作業マシン : cloud-sql-proxy コマンド
  • 認証方式: IAMデータベース認証

    • Cloud SQL : IAM認証を有効化
    • Cloud Run : 利用するサービスアカウントに roles/cloudsql.instanceUser 権限を付与
  • ネットワーク構成:

    • Cloud SQL  : Public IPを有効化

    • Cloud Run : 特別な設定なし
  • Cloud Runからの接続: 公式コネクタライブラリ (@google-cloud/cloud-sql-connector) をアプリケーションに組み込みます。

  • Next.jsからの接続:

ハマりポイント

  • Cloud SQL はマネージドサービスなので、GCPプロジェクト下にあるように見えて、実際はGoogleが管理するネットワーク下にあり、VPC Peering 設定をして VPC アクセスコネクタを作らないと、Private ネットワーク経由で Cloud Run から Cloud SQL に接続することは出来ません。これは、Cloud SQL Proxy を利用する場合にも同様です。(今回は Public IP を利用したので、この設定はしませんでしたが、Private ネットワーク経由での接続のほうが、よりセキュアです) 
  • Cloud SQL Proxy を利用するには、Cloud SQL側でIPアドレスが必要ですが、IPさえ発行されていればよく、承認済みネットワークの設定は不要です。
  • IAMグループ認証によって、グループ単位で認証が可能になりますが、接続時に利用するのは、個人のメールアドレスです。
  • Cloud SQL Proxy を利用する場合、Unix Socket 接続になります。アプリケーション側の設定で、TCP 接続を想定した記述になっていると接続できないので、注意してください。また、なぜか node-postgres 単体では接続ができず、 @google-cloud/cloud-sql-connector による補助的なクライアント設定が必要でした。
  • IAMグループ認証で追加されたユーザは、自動で cloudsqlsuperuser 権限が付与されるので、特別なことなどしなくとも、grant 文など発行できます。Cloud SQL で superuser 権限を持つユーザは作れません。

3.1. 本番環境 (Cloud Run)

terraform の google_cloud_run_v2_service リソースで Cloud SQL Auth Proxy を有効化できます。

resource "google_cloud_run_v2_service" "main" {
  ...
  template {
    # Cloud SQL Proxy volume
    volumes {
      name = "cloudsql"
      cloud_sql_instance {
        instances = ["<instance name>"]
      }
    }     containers {       ...
      # Cloud SQL Proxy volume mount
      volume_mounts {
        name       = "cloudsql"
        mount_path = "/cloudsql"
      }
   ...
}

3.2. ローカル開発環境

cloud-sql-proxy --auto-iam-authn <project>:<region>:<instance> --impersonate-service-account <service_account_email>

このように service account になりすまして、

psql "host=127.0.0.1 sslmode=disable dbname=<db_name> user=<service_account から gserviceaccount.com を削除したもの>"

のように接続できます。

Drizzle 設定

動作したコードだけ貼っておきます。

import { AuthTypes, Connector } from '@google-cloud/cloud-sql-connector';
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool, type PoolConfig } from 'pg';
import * as schema from './schema';

let client: Pool;

// see: https://github.com/drizzle-team/drizzle-orm/issues/4165
if (process.env.POSTGRES_INSTANCE_CONNECTION_NAME) {
  // Cloud Run
  const connector = new Connector();
  // @ts-ignore
  const clientOpts = await connector.getOptions({
    instanceConnectionName: process.env.POSTGRES_INSTANCE_CONNECTION_NAME,
    authType: AuthTypes.IAM,
  });
  const poolOptions: PoolConfig = {
    ...clientOpts,
    user: process.env.POSTGRES_USER,
    database: process.env.POSTGRES_DATABASE,
    max: 5,
    idleTimeoutMillis: 30000,
    connectionTimeoutMillis: 2000,
    ssl: false,
  };
  client = new Pool(poolOptions);
} else {
  // localhost
  const connectionString =
    process.env.DATABASE_URL || 'postgres://postgres:password@localhost:5432/db?sslmode=disable';
  client = new Pool({ connectionString });
}

export const db = drizzle(client, { schema });

export type Database = typeof db;

まとめ

Cloud Run + Cloud SQL なんて、枯れたパターンだと思っていたのですが、思いのほか大変でした。どなたかのお役に立てると幸いです。

参考情報