Open8

Cloud Run Jobsを試してみる(TypeScript & Cloud SQL & Drizzle)

hosaka313hosaka313

What

スケジュール実行の実装のため、Cloud Run Jobsを試してみる。

具体的には、

  • rakeタスク
  • Cloud Schedulerによるタスク作成(Firebase Functions / Cloud Run Functions)

でやっていたようなタスクをCloud Run Jobsで実装してみる。

Cloud Run Jobsの記事はだいぶ少ない。(出てくるサンプルがNode.js 16...)

hosaka313hosaka313

このScrapの目標

  • Cloud SQLに接続(MySQL)
  • 最初のレコードを取得
  • ログ出力

を目指す。ORMにはdrizzleを使用。

hosaka313hosaka313

ローカルセットアップ

$npm init
$npm i drizzle-orm mysql2 dotenv @types/node @t3-oss/env-core valibot
$npm i -D typescript ts-node

t3-envがStandard Schema対応したのでValibotも使えるようになった。)

@types/nodedevDependenciesに入れるとエラーになった。

src/index.ts
import 'dotenv/config';
import mysql from 'mysql2/promise';
import { drizzle } from 'drizzle-orm/mysql2';
import { desc } from 'drizzle-orm';
import { testSchema } from './db/schema.js';
import { env } from './env.js';

async function main() {
  let pool: mysql.Pool | undefined;
  try {
    console.log('Connecting to Cloud SQL…');
    pool = mysql.createPool({
      socketPath: `/cloudsql/${env.INSTANCE_CONNECTION_NAME}`,
      user: env.DB_USER,
      password: env.DB_PASS,
      database: env.DB_NAME,
      connectionLimit: 1
    });

    const db = drizzle(pool);
    console.log('Connected');

    console.log('Fetching latest data…');
    const [latest] = await db
      .select()
      .from(testSchema)
      .orderBy(desc(testSchema.id))
      .limit(1);

    if (latest) {
      console.log(`Latest data created at: ${latest.createdAt}`);
    } else {
      console.warn('No rows found');
      process.exitCode = 2;
    }
  } catch (err) {
    console.error('✖  Unexpected error:', err);
    process.exitCode = 1;
  } finally {
    if (pool) {
      await pool.end().catch(e => console.error('pool.end() failed', e));
    }
  }
}

await main();
src/env.ts
import { createEnv } from "@t3-oss/env-core";
import * as v from 'valibot';

export const env = createEnv({
  server: {
    DB_USER: v.string(),
    DB_PASS: v.string(), // secret
    DB_NAME: v.string(),
    INSTANCE_CONNECTION_NAME: v.pipe(
      v.string(),
      v.regex(
        /^[a-zA-Z0-9-]+:[a-zA-Z0-9-]+:[a-zA-Z0-9-]+$/,
        'インスタンス接続名は "project-id:region:instance-id" の形式で入力してください。'
      )
    )
  },
  runtimeEnv: process.env,
  emptyStringAsUndefined: true,
});
src/db/schema.ts
import { mysqlTable, serial } from 'drizzle-orm/mysql-core';

export const testSchema = mysqlTable('test', {
  id: serial('id').primaryKey(),
});
hosaka313hosaka313

Docker

# ---------- build stage ----------
FROM node:22-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY tsconfig.json ./
COPY src ./src
RUN npx tsc --project tsconfig.json

# ---------- runtime stage ----------
FROM gcr.io/distroless/nodejs22-debian12 AS runner
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
ENV NODE_ENV=production

CMD ["dist/index.js"]

compose.yml

CloudSQL Auth Proxyのイメージを使用。

compose.yml
services:
  proxy:
    image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.16.0-alpine # health checkでwgetを使うため。
    command:
      - ${INSTANCE_CONNECTION_NAME}
      - --unix-socket=/cloudsql
      - --health-check
      - --http-address=0.0.0.0
      - --http-port=9090
      - --credentials-file=/credentials.json
    volumes:
      - ${GOOGLE_APPLICATION_CREDENTIALS}:/credentials.json
      - ./cloudsql:/cloudsql
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:9090/readiness"]
      interval: 3s
      timeout: 5s
      retries: 12
  app:
    build: .
    volumes:
      - ./cloudsql:/cloudsql
    environment:
      - INSTANCE_CONNECTION_NAME=${INSTANCE_CONNECTION_NAME}
      - DB_USER=${DB_USER}
      - DB_PASS=${DB_PASS}
      - DB_NAME=${DB_NAME}
    depends_on:
      proxy:
        condition: service_healthy
hosaka313hosaka313

Deployコマンド一例

env.yml
INSTANCE_CONNECTION_NAME: <YOUR_PROJECT>:<REGION>:<INSTANCE>
DB_NAME: NAME
DB_USER: USER
deploy.sh
REGION=asia-northeast1
PROJECT_ID=<ProjectID>
INSTANCE_CONNECTION_NAME="<YOUR_PROJECT>:<REGION>:<INSTANCE>"

gcloud run jobs deploy test-cloud-run-job \
  --source . \
  --region $REGION \
  --service-account <CloudSQLへのアクセス権のあるサービスアカウント> \
  --set-cloudsql-instances $INSTANCE_CONNECTION_NAME \
  --env-vars-file .env.yml \
  --set-secrets DB_PASS=<SECRET_NAME>:latest \
  --memory 512Mi
hosaka313hosaka313

実行

$gcloud run jobs execute test-cloud-run-job

または管理画面から。

hosaka313hosaka313

まとめ

  • set-cloudsql-instancesでCloud Run同様、CloudSQLへsocket通信できる。
  • 実行するmain関数を書くだけで良い。

Dockerfileで動かせるので、Firebase Functions等よりも依存が少ない感想。

ただ、参考記事などのノウハウがあまりないのが懸念点。知らない蹉跌をするリスクあり。