type holyshared = Engineer<mixed>

技術的なことなど色々

TypeORMでCloud SQLにIAMユーザーで接続する

Cloud SQLで組み込みの認証を利用せず、IAMベースの認証でDBに接続できるかやってみました。
IAMベースの認証にすると、パスワードが不要になり、SSL/TLSで接続できるようになります。

今回は下記の条件で試しました。

  • プリンシパルではなく、サービスアカウントを利用したIAM認証
  • Cloud SQL
    • PostgreSQL v18
    • SSL接続のみを許可する(信頼できるクライアント証明書を必須にするが本来はいい)
    • パブリック IP接続
    • IAM認証を有効にする
  • Cloud Run
    • Nodejsアプリケーション

手順としては、DBインスタンスを用意して、接続できるサービスアカウントを用意し、アプリケーションをIAM認証で接続する様に変更する感じです。

Cloud SQLのインスタンスを起動する

ssl_modeをENCRYPTED_ONLYにするのとcloudsql.iam_authenticationフラグを有効にして、インスタンスを起動する。
「SSL 接続のみを許可する」になっているのと、データベースのフラグとパラメータのcloudsql.iam_authenticationがonになっているのを確認する。

resource "google_sql_database_instance" "app_db" {
  project = var.project_id

  name             = "app-db"
  database_version = "POSTGRES_18"
  region           = var.region

  settings {
    tier = "db-f1-micro"

    backup_configuration {
      enabled = true
    }
    // サービスアカウントで接続できるようにフラグを有効にする
    database_flags {
      name  = "cloudsql.iam_authentication"
      value = "on"
    }
    ip_configuration {
      // SSL/TLSで接続するが、クライアント証明書は検証しない
      ssl_mode = "ENCRYPTED_ONLY"
    }

    deletion_protection_enabled = true
  }

  deletion_protection = true
}

resource "google_sql_database" "database" {
  project = var.project_id

  name     = "app-db"
  instance = google_sql_database_instance.app_db.name
}

Cloud SQLの接続の為にサービスアカウントを作成する

DBに接続するサービスアカウントを用意する。
今回はCloud RunからCloud SQLに接続するので、Cloud Runで指定するサービスアカウントに対して、ロールとDBユーザーを用意する。

// サービスアカウント
resource "google_service_account" "app" {
  project  = var.project_id
  account_id = "app-server"
}

// DB接続に必要なロールを指定する
// 必要に応じてそのほかのロールを追加する
resource "google_project_iam_member" "client" {
  project = var.project_id
  role    = "roles/cloudsql.client"
  member  = "serviceAccount:${google_service_account.app.email}"
}

resource "google_project_iam_member" "instance_user" {
  project = var.project_id
  role    = "roles/cloudsql.instanceUser"
  member  = "serviceAccount:${google_service_account.app.email}"
}

// DBユーザーをサービスアカウントで追加する
resource "google_sql_user" "app_sql_user" {
  project  = var.project_id
  name     = trimsuffix(google_service_account.app.email, ".gserviceaccount.com")
  instance = google_sql_database.database.name
  type     = "CLOUD_IAM_SERVICE_ACCOUNT"
}

サービスアカウントで接続できるかを確認する

サービスアカウントの権限借用を使用するプリンシパルにロールのroles/iam.serviceAccountTokenCreatorを割り当てる。
そいうしないと、権限借用する際のアクセストークンを取得できない。

gcloud auth application-default login \
  --project [PROJECT_NAME] \
  --impersonate-service-account [SERVICE_ACCOUNT_EMAIL]

次はプロキシーを起動する。

cloud-sql-proxy --auto-iam-authn \
  --address 127.0.0.1 \
  --port 5432 \
  [CONNECTION STRING]

プロキシーを起動したらサービスアカウントで接続してみる。
接続できれば問題ないが、権限が何も割り当てられていないので、テーブルのデータをSELECTしたりすることができない。
なので、必要な権限を別途GRANTで割り当てる。

psql -h 127.0.0.1 -p 5432 -U [サービスアカウント(.iamまで)] app-db

アプリケーションのDBの接続方法を変える

言語コネクタ@google-cloud/cloud-sql-connectorが提供されているので、それを利用する。
TypeORMでの接続方法は@google-cloud/cloud-sql-connectorのexamplesにある。
prismaやsequelizeのもあるので、それぞれ参考にするといいかもしれない。

import "reflect-metadata";
import { PostgresConnectionOptions } from "typeorm/driver/postgres/PostgresConnectionOptions";
import { DataSource } from "typeorm";
import {
  Connector,
  AuthTypes,
  IpAddressTypes,
} from "@google-cloud/cloud-sql-connector";

export const AppDataSource = new DataSource({ type: "postgres" });

export const connect = async () => {
  const connector = new Connector();
  const clientOpts = await connector.getOptions({
    instanceConnectionName: process.env.INSTANCE_CONNECTION_NAME,
    ipType: IpAddressTypes.PUBLIC,
    authType: AuthTypes.IAM,
  });
  const datasourceOptions: PostgresConnectionOptions = {
    type: "postgres",
    database: process.env.DB_NAME,
    entities: [],
     synchronize: false,
    logging: true,
    migrations: [],
    username: process.env.IAM_DB_USERNAME,
    extra: clientOpts,
  };
  const ds = await AppDataSource.setOptions(datasourceOptions).initialize();
  return {
    async close() {
      await ds.destroy();
      connector.close();
    },
  };
};

コードの変更が完了したら、Cloud Runで環境変数を指定する。
ユーザー名、接続名、データベース名などはSecretManagerで管理して、環境変数はSecretManagerから取得するようにすると良い。

Cloud Runをデプロイして、接続できていれば完了です。