モノレポ環境の Next.js を Cloud Run にデプロイして社内のメンバーだけが閲覧できるようにするまで
構成
- next.js: 13.4.2
- react: 18.2.0
- react-dom: 18.2.0
- pnpm: 8.5.1
pnpm workspace + turbo でモノレポ管理されている想定
社内メンバーだけということで、管理画面などを想定する
app router は普通にわいわいと書けばいい。
管理画面を想定しているので、直接 Cloud SQL と繋ぐためローカル開発の際は cloud sql proxy を経由する。
または docker compose とかで DB を立てる
query builder として kysely を使った
import {
ColumnType,
Generated,
Insertable,
Selectable,
Updateable,
} from 'kysely';
export interface UserTable {
id: Generated<number>;
email: string;
created_at: ColumnType<Date, string | undefined, never>;
updated_at: ColumnType<Date, string | undefined, never>;
}
export type User = Selectable<UserTable>;
export type NewUser = Insertable<UserTable>;
export type UpdateUser = Updateable<UserTable>;
export interface Database {
users: UserTable;
}
こんな感じで table のカラムなどを定義しておく
DB への接続はこんな感じでできる
import type {Database} from './types';
import {Pool} from 'pg';
import {Kysely, PostgresDialect} from 'kysely';
const dialect = new PostgresDialect({
pool: new Pool({
database: process.env.DB_NAME,
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
port: process.env.DB_PORT,
max: 10,
}),
});
export const db = new Kysely<Database>({dialect});
ローカルで開発環境のDBを繋いで開発してる時は、cloud sql proxy を使う前提で行くと
DB_HOST: 127.0.0.1
になる。
Cloud Run 経由で Cloud SQL に繋ぐ時は以下の記事のままやっていけばできる
DB を postgres にしてる場合、 kysely では INSTANCE_UNIX_SOCKET
に当たるものをそのまま host
に設定すればよいため、環境変数で /cloudsql/xxxxxx
をつっこんでしまえば動く
例えばユーザー数を取得する場合は以下のように書ける
export async function getAllUserCount() {
const res = await db
.selectFrom('users')
.select(u => u.fn.count<number>('id').as('count'))
.executeTakeFirstOrThrow();
return res.count;
}
kysely では fn.count("id").as("count")
のような形で SQL の関数を実行する。ただしこの返り値は string | number | bigint
になるので、 number
に型を合わせている。
ただこれは問題で実際、ランタイム上では string になる
これはドキュメントにある通りで、ドライバの設定により変わるため TS の型上でそれを表現できないので広めにとってるっぽい。
なので実際は string で合わせにいって、 Number.parseInt()
とかを噛ませるのが無難
Docker のビルドだが、最近の Next.js は以下のような設定をすることで使っている dependency を .next
に持っていってくれるので Dockerfile で pnpm install
などを行わなくていい
そのため Dockerfile は以下のような形が最小構成になりそう
FROM node:18-alpine
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY ./packages/admin/next.config.js ./packages/admin/next.config.js
COPY ./packages/admin/public ./packages/admin/public/
COPY ./packages/admin/.next/static ./packages/admin/.next/static/
COPY ./packages/admin/.next/standalone ./
USER nextjs
CMD ["node", "apps/admin/server.js"]
モノレポなので COPY
をしてくる元のファイルや、そのコピー先もモノレポの時と同じように持っているが多分 flat に持っても大丈夫だと思う(未検証)
アプリケーションのビルドは
- pnpm build
- docker build
の 2 回行うことになるが、ローカル開発の際にもDB_HOST
などの環境変数を参照するので.env
を作るのが多いと思う。
この.env
をおいたままpnpm build
してしまうと.next
の方にも.env
がコピーされてしまうため、手元でdocker build
を行って Cloud Run に一旦 deploy しようみたいに考えると、参照してる env が.next
にある.env
をロードしてしまい参照できない
つまり
DB_HOST=127.0.0.1
env DB_HOST=postgres://xxxx pnpm build
docker build -t <Tag> -f packages/admin/Dockerfile .
みたいにビルドをしても、 DB_HOST
は 127.0.0.1
のままになる
そのため手元でビルドする時は .env
にローカル用と dev 環境用みたいに作っておく方がよいかも
ビルドして、cloud run へのデプロイは env を除いて以下のように実行
pnpm build
docker build -t <artifact registry の URL>/<gcp project id>/<folder>/<id>:<tag> --platform linux/amd64 -f apps/admin/Dockerfile .
docker push <image>
gcloud run deploy admin --project=<project id> --image=<image> --region=<region> --platform managed
この時、例えば僕の PC は Apple M2 だが、普通に docker build
して成功しても cloud run に deploy すると 8080 port が使えないよみたいなエラーで怒られるが、これは PORT を開けてるかとかの問題ではなくだいたいは platform の問題なので
--platform linux/amd64
をつけるとうまく行く
IAP の説明は以下の記事がとても参考になるのでこれを見ると良い
証明書エラーがありそうならここら辺を読むといい
上記の通り進めてるが、うまくOAuthは動いてる。そしてGoogle認証も使える。
ただ認証突破した後、403が返ってくる...😬
この問題を解決したらクローズする
ふと公式のドキュメント探してたらこれがあった。怪しい
CloudRun起動元の権限をSAに付与したらいける説
IAM によるアクセス制御の手順に沿って、IAP がトラフィックを Cloud Run バックエンド サービスに送信することを承認します。
プリンシパル: service-[PROJECT-NUMBER]@gcp-sa-iap.iam.gserviceaccount.com
ロール: Cloud Run 起動元
この IAM どこにあるねん... って思ってたけどプロジェクト情報にあるプロジェクト番号をいれて IAMと管理
から Google 提供のロール付与を含める
のチェックをつけると出てくるはず。
アクセス権をを付与
から Cloud Run 起動元を付与すればいい感じになる