type holyshared = Engineer<mixed>

技術的なことなど色々

React Router v8のミドルウェア

React Router v7からv8に対応させる作業をやっていた。 一部のものだけログイン画面でログインしようとして、リダイレクトを繰り返す様になってしまったので調査と修正をした。

ログイン画面があるものは、v8で有効になるミドルウェアをfutureフラグで対応していて、これを消すだけくらいかなと思っていました。

// react-router.config.ts
export default {
  appDirectory: "src/app",
  ssr: true,
  buildDirectory: "dist"
  buildDirectory: "dist",
  // このフラグを消す
  future: {
    v8_middleware: true,
  },
} satisfies Config;

しかし、ローカルで試すとログイン前にミドルウェアで認証状態の確認を行っている箇所に引っかかってログイン画面にリダイレクトする処理が何度も実行される様になってしまいました。

原因はsingle fetchの挙動が変わったことが原因の様でした。 url.pathnameの末尾に.dataが付くパターンがあり(具体的にはaction)、そのせいで未認証のチェックに引っかかり、無限にリダイレクトするということが起きていました。

これはパスの末尾に.dataがついていることも考慮に入れることで解決しました。 ミドルウェアがloader, actionで実行されることを意識すれば解決しそうな話でした。

import { redirect } from "react-router";
import { getSession, destroySession } from "~/session";
import { type RouterContextProvider } from "react-router";
import { userContext } from "~/context";

export const authMiddleware = async ({
  request,
  context,
}: {
  request: Request;
  context: Readonly<RouterContextProvider>;
}) => {
  const url = new URL(request.url);
  const ctx = context.get(userContext);

  // React Router の single fetch ではデータリクエストのパスが `/sign_in.data`
  // のようになるため、`.data` を除去してから判定する
  const pathname = url.pathname.replace(/\.data$/, "");

  if (pathname === "/sign_in") {
    if (request.method === "GET" && ctx.user) {
      throw redirect("/");
    }
    return;
  }

  if (!ctx.user) {
    const session = await getSession(request.headers.get("Cookie"));
    throw redirect("/sign_in", {
      headers: {
        "Set-Cookie": await destroySession(session),
      },
    });
  }
};

TypeORM 1.0へアップグレードする

TypeORMが1.0になりました。
このため、0.3系から1.0にアップグレートを行いました。

やったことは接続のオプションの型指定と検索時のリレーションの指定方法です。

コネクションの型指定をPostgresConnectionOptionsからPostgresDataSourceOptionsに変更する

変更前

import type { PostgresConnectionOptions } from "typeorm/driver/postgres/PostgresConnectionOptions";

const defaultOptions: PostgresConnectionOptions = {
  type: "postgres",
  database: "example",
  entities: [User],
  synchronize: false,
  logging: true,
  migrations: ["./migrations/*{.ts,.js}"]
};

変更後

import type { PostgresDataSourceOptions } from "typeorm/driver/postgres/PostgresDataSourceOptions";

const defaultOptions: PostgresDataSourceOptions = {
  type: "postgres",
  database: "example",
  entities: [User],
  synchronize: false,
  logging: true,
  migrations: ["./migrations/*{.ts,.js}"]
};

relationsの指定をオブジェクト指定に置き換える

変更前

const results = await repo.findAndCount({
  where: {
    id: "id"
  },
  relations: ["group", "group.owner"]
});

変更後

const results = await repo.findAndCount({
  where: {
    id: "id"
  },
  relations: {
    group: {
      owner: true,
    },
  }
});

RubyGemsのプロジェクトオーナーを変えた

自分の作成したGemにメンテナンスしてないなら引き継ぎたいとメールを頂いたので、オーナーを変えてみました。
最終更新が10年前とかで、使いたい人がいるならいいかなと思い、対応をしてみました。

連絡くれた人のGitHubアカウントを見て、特段怪しそうではなかったので、RubyGemsにログインして、所有者にメールアドレスを指定すると追加できました。

これでRubyGemsの仕様ではオーナーは複数設定できるということ知りました。

設定後無事に新しいバージョンがリリースされていたので、良かったです。

2025年の靴の着用回数を集計した

毎年恒例の履いた靴の集計をしてみました。
今年は夏場暑くて、ジョギングができなかったので、BROOCKSのGhost14やASICSのGEL KAYANO 31の着用頻度が下がってますね。

またSOLOVAIRの靴が2足増えてるのと、Grensonも2足増えてます。
Urban Hikerは旅行の時に便利だったのでこれからも履くと思います。

以前から履いている靴

No. モデル ブランド 2025年までの着用回数 2025年の着用回数
1 GEL KAYANO 31 ASICS 25 22
2 MORPHIC BASE PUMA 30 24
3 WK400 KEEN 47 22
4 Skipton Zip Boot George Cox 61 29
5 Clearcut m trippen 105 28
6 Strike Norman Walsh 73 22
7 SX 78C02 MoonStar 97 24
8 Tanker Pro Nicks 80 22
9 SKIPTON George Cox 97 24
10 ROOTS LOOP II 8.5 Rolling dub trio 128 24
11 Classical Horsehide Mesh Sandals Dapper’s 157 31
12 BRIGHTON Loake 102 24
13 Ghost14 BROOCKS 142 19
14 Overstone Hi Derby Crown Northampton 131 24
15 Neon Blue 8 Eye Derby Boot SOLOVAIR 106 22
16 6Eye Astronaut Boot SOLOVAIR 112 23
17 飛鳥ホールカット KOTOKA 135 25
18 LAW NPS shoes 126 24
19 Harry BROTHER BRIDGE 149 25
20 6inch Boots Reverse Black Odessa Chippewa 130 22
21 BOYS KATSUYA TOKUNAGA 125 22
22 GARRISON TRAIL GORE-TEX HIKING SHOES Timberland 152 23
23 M-43 SERVICE SHOES JOHN LOFGREN 133 21
24 BERLIN BROTHER BRIDGE 152 25
25 CONISTON Crockett&Jones 145 22
26 Morgan BROTHER BRIDGE 143 23
27 Custom Jobmaster 38LTT Wesco 140 21
28 442 MILITARY CAP TOE OXFORD RAMSEY 179 24
29 DOBULE MONK SANDAL Tokyo Sandals 598 73
30 ROOTS Rolling dub trio 197 24
31 Irish Setter Redwing 182 24
32 6inch Service Boots Chippewa 165 22
33 McCLOUD BROTHER BRIDGE 171 23
34 JAMES BROTHER BRIDGE 181 24

新しく購入した靴

No. モデル ブランド 2025年の着用回数
1 Navy Gaucho Urban Hiker SOLOVAIR 11
2 Light Green 8 Eye Derby Boot SOLOVAIR 14
3 Button up shoes plain toe forme 17
4 JED Grenson 24
5 ABEL Grenson 24

手放した靴

No. モデル ブランド 2025年までの着用回数 2025年の着用回数
1 TimberLoop EK Utility Boot Timberland 67 8
2 Solar Wave Leather/Fabric Mid Hiker Boots Timberland 114 9

アプリケーションをS3からGCSへ移行する

自分で運用しているアプリケーションのストレージサービスをS3に完全に移行しました。
S3を利用していたのはアプリケーションがもともとHerokuで運用していたからです。

アプリケーション自体は今はGoogle Cloud Platform上に構築していますが、もともとHerokuで運用してしていました。
なのでストレージサービスはGCSではなく、S3になっていました。

これをさすがGCSに移行することにしました。
移行の理由としては、完全にアプリケーションをGoogle Cloud Platform上に移行したかったからと、請求を一本化したかったからです。

移行方法の検討と実施

S3とGCSにファイルを出力しながら段階的に移行していこうかと考えたのですが、めんどくさかったので下記の手順で実施しました。

  1. アプリケーションのS3の処理をGCSに置き換える。
  2. Terraformで移行先のGCSバケット作成、CORSの設定を行う。
  3. StorageTransferServiceを利用して、S3のバケットのオブジェクトをGCSに転送する。
  4. アプリケーションをデプロイする。
  5. 正常に稼働しているかを確認する。

S3とGCSへのオブジェクト転送

StorageTransferServiceを利用したS3からGCSへ転送ですが、下記の方法で実行しました。 docs.cloud.google.com

  1. SecretManagerに転送用のシークレットを作成する

     {
       "accessKeyId": "AWS_ACCESS_KEY_ID",
       "secretAccessKey": "AWS_SECRET_ACCESS_KEY"
     }
    
  2. 転送元をAmazon S3のバケットにする、ここで作成したシークレットのアクセスキーを利用するようにする

  3. 転送先のGCSのバケットを選ぶ
  4. ネットワークルーティングオプションでマネージドプライベートネットワークを選択する

    ネットワークルーティングオプション

  5. ジョブを実行する

これで転送ジョブが開始されます。

転送結果

6,948ファイル、20GB近くのバケットから転送しましたが、一回では全て転送できませんでした。
2回手動で再実行しました。

今回は一回でやろうとしたので、手動で実行しましたが、本来は定期的に実行するようスケジュールリングして、転送するのだと思います。

ジョブの結果

結果は10分くらいで転送できました。

開始: 2025年11月29日 11:38:34
終了: 2025年11月29日 11:47:45

思いのほか短時間で終わってよかったです。

余談ですが、初回だけ権限エラーが発生してました。
project-[プロジェクト番号]で始まるサービスアカウントなので、GCP側で自動で作成されるサービスアカウントです。

project-[プロジェクト番号]@storage-transfer-service.iam.gserviceaccount.com does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist).

2回目以降はエラーが発生してないので、問題なと思います。

長期的に転送ジョブを利用する場合は、権限の管理を細かくできるサービスアカウントを自前で用意して、ジョブの設定で「ユーザー管理のサービス エージェント」を選択して割り当てる方がいいと思います。

docs.cloud.google.com

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をデプロイして、接続できていれば完了です。

CloudSQLのPostgreSQLのバージョンアップ

PostgreSQLのバージョンをv15からv18にアップグレードした。 仕事でAWSのDBバージョンを上げる作業を見ていて、そういえば今個人で動かしているCloudSQLのバージョンなんだけっけかと思ったらv15でした。

まだサポートされているバージョンですが、使えなくなったりする前に一気にv18にしました。
作業がを開始する前にv15の最新のバックアップをオンデマンドでとっておき、terraformの構成変更を適用して、v18で起動するのを待ちました。

resource "google_sql_database_instance" "shoes_manager" {
  project = module.project.project_id

  name             = "app-db"
  database_version = "POSTGRES_18" // POSTGRES_15からPOSTGRES_18へ
  region           = var.region

  settings {
    tier = "db-f1-micro"

    backup_configuration {
      enabled = true
    }
  }

  deletion_protection = "true"
}

DBのCPU利用状況

一時的に25分くらい落ちてますが、v18へのアップグレートが完了し、正常に稼働してよかったです。