たかぎとねこの忘備録

プログラミングに関する忘備録を自分用に残しときます。マサカリ怖い。

Ktor + Exposed + Cloud RunでCloud SQLに接続させる方法

Cloud RunにデプロイしようとしたKtor + Exposedのアプリケーションイメージが、Cloud SQLへ接続できないことが原因で何度もデプロイに失敗してしまった。解決までに数時間要してしまい、ネットを探してもExposedを使った時のCloud SQLへの接続方法を記した記事が日本語と英語も含めてほとんど見つからなかった。なので忘備録として残しておこうと思う。

まずローカルでCloud SQL Auth Proxyを立ち上げてCloud SQLに接続する場合、次のようなURLを構築すると思う。

val jdbcURL = "jdbc:postgresql://0.0.0.0:3306/postgres"

これに倣って、Cloud SQLの公開IPアドレスと取り替えてCloud SQLに接続しようとしてみた。

val jdbcURL = "jdbc:postgresql://公開IPアドレス/postgres"

次のようなエラーが発生した。

org.postgresql.util.PSQLException: The connection attempt failed. at org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:331) at

Cloud RunからCloud SQLに接続する方法として、TCPで接続する方法とUnixドメインソケットを使って接続する方法がある。各URLのフォーマットは次の通り。

TCPで接続する場合

val jdbcURL = "jdbc:postgresql:///$databaseName?cloudSqlInstance=$instanceConnectionname&socketFactory=com.google.cloud.sql.postgres.SocketFactory&user=$user&password=$password"

UNIXドメインソケットを使って接続する場合

val jdbcURL = "jdbc:postgresql:///$databaseName?unixSocketPath=$unixSocketPath&cloudSqlInstance=$instanceConnectionname&socketFactory=com.google.cloud.sql.postgres.SocketFactory&user=$user&password=$password"

Unixドメインソケットを使って接続する場合は、unixSocketPathクエリーパラメーターを渡す必要があるので注意する。

各変数にどんな値を代入すれば良いかは次の通り。

  • databaseName
    • 接続したいデータベース名
      • postgres (例
  • instanceConnectionName
    • Cloud SQLインスタンスの概要ページに表示されている接続名のフィールドの値
      • プロジェクト名:リージョン名:データベースのインスタンス名のフォーマット
  • socketFactory
    • cloud-sql-jdbc-socket-factoryライブラリにあるクラス。
      • com.google.cloud.sql.postgres.SocketFactoryを渡す。
  • unixSocketPath
    • ソケットへのパス
      • /cloudsql/接続名(instanceConnectionName変数と同じ値)
  • user
    • データベースのユーザー名
  • password
    • データベースのパスワード

Socket Factoryを使えるようにするためにcloud-sql-jdbc-socket-factoryを依存関係として追加する。

github.com

dependencies {
    implementation("org.postgresql:postgresql:$postgresql_version")
    ...
    implementation("com.google.cloud.sql:postgres-socket-factory:1.7.2")
}

依存関係を追加しないでクラス名だけ指定した場合は次のようなエラーが発生する。

org.postgresql.util.PSQLException: The SocketFactory class provided com.google.cloud.sql.postgres.SocketFactory could not be instantiated. at org.postgresql.core.SocketFactoryFactory.getSocketFactory(SocketFactoryFactory.java:43) at org....

実際のコードは次の通り。

object DatabaseFactory {
    fun init(
        user: String = "",
        password: String = "",
        databaseName: String = "",
        instanceConnectionName: String = "",
        unixSocketPath: String = "",
    ) {
        val driverClassName = "org.postgresql.Driver"
        val jdbcURL = "jdbc:postgresql:///$databaseName?unixSocketPath=$unixSocketPath&cloudSqlInstance=$instanceConnectionName&socketFactory=com.google.cloud.sql.postgres.SocketFactory&user=$user&password=$password"
        val database = Database.connect(
            url = jdbcURL,
            driver = driverClassName,
            user = user,
            password = password,
        )
    ....
    }
}

各変数の値は環境変数として展開して渡すようにする。そのために、application.confを設定する。

google {
    cloud_sql {
        username = "takagimeow"
        username = ${?DB_USER_NAME}
        password = "takagimeow_pass"
        password = ${?DB_USER_PASS}
        path = "postgres"
        path = ${?DB_PATH}
        database_name = ""
        database_name = ${?DATABASE_NAME}
        instance_connection_name = ""
        instance_connection_name = ${?INSTANCE_CONNECTION_NAME}
        unix_socket_path = ""
        unix_socket_path = ${?UNIX_SOCKET_PATH}
    }
}

設定した環境変数Application.module()で取得し、DatabaseFactory.init()の呼び出し時に渡す。

fun Application.module() {
    DatabaseFactory.init(
        user = environment.config.property("google.cloud_sql.username").getString(),
        password = environment.config.property("google.cloud_sql.password").getString(),
        databaseName = environment.config.property("google.cloud_sql.database_name").getString(),
        instanceConnectionName = environment.config.property("google.cloud_sql.instance_connection_name").getString(),
        unixSocketPath = environment.config.property("google.cloud_sql.unix_socket_path").getString()
    )
    ...
}

それでも接続できない場合

Cloud SQL Admin APIが有効になっていない可能性があるので、必ず有効になっているかを確認する。

NestJS + Prisma + Cloud Run + Cloud SQLを試す

ソケットパスに関して公式ドキュメントでは次の様に書かれている。

注: PostgreSQL スタンダードでは、ソケットパスに .s.PGSQL.5432 接尾辞が含まれている必要があります。一部のライブラリではこの接尾辞が自動的に適用されますが、他のライブラリでは、ソケットパスを以下のように指定する必要があります。

なので、一度/.s.PGSQL.5432をソケットパスの末尾につけて試してみる。

Connect from App Engine standard environment  |  Cloud SQL for PostgreSQL  |  Google Cloud

参考

App Engine から Cloud SQL に接続する - albatrosary's blog

クイックスタート: Cloud Run から Cloud SQL for PostgreSQL に接続する  |  Google Cloud

Cloud Run から接続する  |  Cloud SQL for PostgreSQL  |  Google Cloud

インスタンスの作成  |  Cloud SQL for PostgreSQL  |  Google Cloud

Cloud Run を徹底解説! - G-gen Tech Blog

Identity-Aware Proxy(IAP)とCloud Armorを使用してCloud Runサービスへのアクセス制御を実装する - G-gen Tech Blog

Cloud SQL Auth Proxy を使用して Cloud Run から Cloud SQL に接続する - G-gen Tech Blog

Issue connecting to Google Cloud PostgreSQL from GAE project · Issue #136 · GoogleCloudPlatform/cloud-sql-jdbc-socket-factory · GitHub

Can't connect Cloud Data Fusion with Google Cloud SQL for PostgreSQL · Issue #134 · GoogleCloudPlatform/cloud-sql-jdbc-socket-factory · GitHub

https://kaleidot.net/ktor-ktor-%E3%81%A8-postgres-%E3%82%92%E7%B5%84%E3%81%BF%E5%90%88%E3%82%8F%E3%81%9B%E3%81%A6%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B-c9a2b85353a4

ktor - How connect tot Google Cloud SQL via Exposed - Stack Overflow

I ve been using unix domain sockets in mac with ktor It s be | Ktor | Kotlinlang

Node.js+SequelizeでCloud RunからCloud SQLへ接続する方法

Java を使用してソケット接続を作成する  |  Cloud SQL for PostgreSQL  |  Google Cloud

Google CloudSQL インスタンスに接続する | DataGrip