Open8
Cloud Run Jobsを試してみる(TypeScript & Cloud SQL & Drizzle)
What
スケジュール実行の実装のため、Cloud Run Jobsを試してみる。
具体的には、
- rakeタスク
- Cloud Schedulerによるタスク作成(Firebase Functions / Cloud Run Functions)
でやっていたようなタスクをCloud Run Jobsで実装してみる。
Cloud Run Jobsの記事はだいぶ少ない。(出てくるサンプルがNode.js 16...)
このScrapの目標
- Cloud SQLに接続(MySQL)
- 最初のレコードを取得
- ログ出力
を目指す。ORMにはdrizzleを使用。
ローカルセットアップ
$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/node
はdevDependencies
に入れるとエラーになった。
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(),
});
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 /app/node_modules ./node_modules
COPY /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
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
実行
$gcloud run jobs execute test-cloud-run-job
または管理画面から。
まとめ
-
set-cloudsql-instances
でCloud Run同様、CloudSQLへsocket通信できる。 - 実行するmain関数を書くだけで良い。
Dockerfileで動かせるので、Firebase Functions等よりも依存が少ない感想。
ただ、参考記事などのノウハウがあまりないのが懸念点。知らない蹉跌をするリスクあり。
CI/CD
以下の記事が参考になる。