Cloudflare Pages・Workers + Hono + React + Chart.js で食べたリンゴの割合をグラフ化してみた

今まで、「食べたリンゴの割合をグラフ化するアプリ」をGoogle Cloud Runで動かしてきました。
Python + Django + Highcharts + Coogle Cloud Cloud Run + Cloud Storage + Litestream で食べたリンゴの割合をグラフ化してみた - メモ的な思考的な

 
運用する中で、

  • コールドスタートがやや気になる
    • めったに使わないアプリとはいえ、使うときにはさっと起動すると嬉しい
  • DB(Litestream + SQLite)をCloud Storageに置いているので、テーブルの中身を確認しづらい
  • Cloud Runはほとんど費用負担が発生していない一方で、Cloud Storageで費用がかさんできた

などを感じることがありました。

 
そんな中、Cloudflareがデータベース機能のD1を正式リリースしました。

データベースがあるならば移行先として使えるかもしれないと考え、必要そうな機能を検証してきました。

 
また、 d1-jdbc-driver を使うことで、D1にあるテーブルをJetBrains DataGripから確認することもできました。
JetBrains IDEで外部キーを表示できるよう、Cloudflare D1向けJDBC driver「d1-jdbc-driver」を修正するプルリクを作った - メモ的な思考的な

 
以上より、移行しても問題なさそうと判断しました。

そこで、今までの検証に加えて移行作業を行ったため、内容をメモしておきます。

なお、移行後のWebアプリのURLは以下です。
https://ringosky.thinkami.dev/

 
目次

 

環境

  • Windows 11 WSL2
  • Wrangler 3.60.3
  • Bun 1.1.13
    • Bunのworkspaceを使ってモノレポで管理
  • フロントエンド
    • React 18.3.1
    • Chart.js 4.4.2
    • react-chartjs-2 5.2.0
    • TanStack Router 1.38.1
    • TanStack Query 5.45.1
  • バックエンド
    • Hono 4.4.12
    • Drizzle ORM 0.31.2
    • Drizzle Kit 0.22.7
    • @atproto/api 0.12.23
      • Bluesky API用ライブラリ
  • 開発向けツール
    • Wrangler 3.63.1
    • Biome 1.8.1
      • Linter & Formatter

 
ちなみに、これらのソースコードは、Bunのworkspaceを使ってモノレポで管理しています。

 
また、移行前後で使っている機能は以下の通りです。

Google Cloudだと 役割 Cloudflareでは
Cloud Run アプリをホスト Pages + Workers + Service Binding(RPC)
SQLite + Litestream + Cloud Storage アプリのデータベース D1
Cloud Scheduler SNSへの投稿を定期的に収集 WorkersのCron Trigger + KV

 
また、こんな感じで Cloudflare を使っています。 Pages と Workers は Service Binding (RPC) で連携しています。

なお、上図の各種アイコンは以下のページよりお借りしています。
Brand Icons - Cloudflare Datamining

 
以降では、どんな感じでアプリを作成・移行したのかをメモしておきます。

なお、記事中ではソースコードを必要な部分だけ明示しています。

ソースコード全体は以下のリポジトリで公開しているため、必要に応じてそちらを確認してください。
https://github.com/thinkAmi/cf_ringo_sky

 

ルートディレクトリでのセットアップ

モノレポ内のPagesやWorkersが共通で使うものをセットアップします。

 

ライブラリのインストールと設定ファイルの用意

wranglerとconcurrentlyをインストールします。

$ bun add -d wrangler concurrently

 
Linter & Formatterとして Biome をインストールします。

$ bun add --dev --exact @biomejs/biome

 
Biomeの設定ファイルへコメントを書けるよう biome.jsonc として用意します。
https://github.com/thinkAmi/cf_ringo_sky/blob/main/biome.jsonc

 

Bun workspace を使うために package.jsonを編集

今回のアプリをモノレポで開発できるよう、Bunのworkspace機能を使います。
Workspaces – Package manager | Bun Docs

また、 concurrently を使ってPagesやWorkersを一括起動できるように script も設定します。

{
  "private": true,
  "scripts": {
    "dev": "concurrently \"bun run --filter=\"ringo-db\" dev:db\" \"bun run --filter=\"ringo-web\" dev:web\""
  },
  "workspaces": ["packages/*"],
  // ...
}

 

ringo-db Workersの作成

前述の通り、Service Binding RPCを使ってPagesとWorkersを連携します。

そこで、まずは

  • D1と接続し、SQLを発行する
  • Service Binding RPC向けに、メソッドを公開する

を担当する ringo-db Workersを作成します。

 

ライブラリのインストール

Workersの雛形を作るには、Cloudflare C3で作成するのが便利です。ただ、今回のWorkers向けには不要なものもできてしまいます。

そこで、今回は bun init により最小限のものだけ生成しました。
bun init – Templating | Bun Docs

# packages ディレクトリの中で作業
$ mkdir ringo-db
$ cd ringo-db


# ringo-db アプリを生成
$ bun init
...
package name (ringo-db): 
entry point (index.ts): ./src/index.ts

Done! A package.json file was saved in the current directory.
 + ./src/index.ts
 + .gitignore
 + tsconfig.json (for editor auto-complete)
 + README.md
...

 
続いて、D1接続に必要な Drizzle ORM まわりをインストールします。

$ bun add drizzle-orm
...
installed drizzle-orm@0.31.2

合わせて、開発向けに Drizzle Kit もインストールします。

$ bun add -D drizzle-kit
...
installed drizzle-kit@0.22.7 with binaries:
 - drizzle-kit

 

Cloudflare D1を作成

次に、Cloudflare D1を wrangler で作成します。database leaderは明示的に apac を指定しておきます。
create | D1 | Commands - Wrangler · Cloudflare Workers docs

$ wrangler d1 create ringodb --location apac
...
✅ Successfully created DB 'ringodb' in region APAC
Created your new D1 database.

[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "ringodb"
database_id = "01febb3d-148f-4f3c-8fea-e32445da1ae1"

 

wrangler.toml の編集

D1の情報のほか、 compatibility_dateport などを指定しておきます。
Configuration - Wrangler · Cloudflare Workers docs

name = "ringo-db"
main = "./src/index.ts"
compatibility_date = "2024-06-18"
compatibility_flags = [ "nodejs_compat" ]

[dev]
port = 8788

[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "ringodb"
database_id = "01febb3d-148f-4f3c-8fea-e32445da1ae1"

 

drizzle.config.ts の作成

Drizzle ORMの設定ファイルを作成します。

なお、今回のマイグレーション適用は、Drizzle Kit ではなく Cloudflare D1 の Migrationを使います。
Migrations · Cloudflare D1 docs

そのため、Cloudflare D1のMigrationの仕様に合わせて、 outmigrations にしておきます。

import type {Config} from "drizzle-kit"

export default {
  dialect: "sqlite",
  schema: "./db/schema/*",
  out: "./migrations",
} satisfies Config

 

データベースのスキーマを作成

Blueskyの投稿を保存するためのテーブル feeds 向けのスキーマとして、ringo-db/db/schema/feeds.ts を作成します。

import { sql } from 'drizzle-orm'
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'

export const feeds = sqliteTable('feeds', {
  id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }),
  name: text('name'),
  content: text('content'),
  createdAt: text('created_at').default(sql`(CURRENT_TIMESTAMP)`),
  snsId: text('sns_id'),
})

 

マイグレーションファイルを生成

マイグレーションファイルを drizzle-kit を使って生成します。
Generate migrations | Drizzle ORM - List of commands

$ bun drizzle-kit generate
drizzle-kit: v0.22.7
drizzle-orm: v0.31.2

No config path provided, using default 'drizzle.config.ts'
Reading config file 'path/to/ringo_sky/packages/ringo-db/drizzle.config.ts'
1 tables
feeds 5 columns 0 indexes 0 fks

[✓] Your SQL migration file ➜ migrations/0000_wild_pride.sql 🚀

 

ローカルのD1にマイグレーションを適用

ローカルのCloudflare D1対してマイグレーションを適用する場合、drizzle-kit の機能は使えません。

そこで、今回は wrangler を使ってマイグレーションを適用します。

ちなみに、ローカルに適用する --local オプションも明示的に付けておきます。
https://developers.cloudflare.com/workers/wrangler/commands/#migrations-apply

$ wrangler d1 migrations apply ringodb --local
...
Migrations to be applied:
┌─────────────────────┐
│ name                │
├─────────────────────┤
│ 0000_wild_pride.sql │
└─────────────────────┘
✔ About to apply 1 migration(s)
Your database may not be available to serve requests during the migration, continue? … yes
🌀 Executing on local database ringodb (01febb3d-148f-4f3c-8fea-e32445da1ae1) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
┌─────────────────────┬────────┐
│ name                │ status │
├─────────────────────┼────────┤
│ 0000_wild_pride.sql │ ✅       │
└─────────────────────┴────────┘

 

Cloudflare D1にマイグレーションを適用

続いて、本番環境である Cloudflare D1 にマイグレーションを適用します。

$ wrangler d1 migrations apply ringodb --remote
...
Migrations to be applied:
┌─────────────────────┐
│ name                │
├─────────────────────┤
│ 0000_wild_pride.sql │
└─────────────────────┘
✔ About to apply 1 migration(s)
Your database may not be available to serve requests during the migration, continue? … yes
🌀 Executing on remote database ringodb (01febb3d-148f-4f3c-8fea-e32445da1ae1):
🌀 To execute on your local development database, remove the --remote flag from your wrangler command.
🚣 Executed 2 commands in 0.4022ms
┌─────────────────────┬────────┐
│ name                │ status │
├─────────────────────┼────────┤
│ 0000_wild_pride.sql │ ✅       │
└─────────────────────┴────────┘

 

実装に関するメモ

あとは、 src/index.ts などに必要な機能を実装します。
https://github.com/thinkAmi/cf_ringo_sky/blob/main/packages/ringo-db/src/index.ts

実装で迷ったことは以下です。

 

batch APIを使ってSQLトランザクション相当を実装

以下の記事にある通り、Cloudflare D1にはSQLトランザクションが実装されていないようです。

 
そこで、SQLトランザクションが必要になったときは batch APIを使って実装しました。

 

Drizzle ORMでGroup By や Count する

Drizzle ORMのドキュメントに従い実装しました。

なお ringo-db Workers は Service Binding RPC で利用することを前提にしているため

  • WorkerEntrypoint を継承したクラスに、公開するメソッドを定義
  • エラーになるのを防ぐため、default export するオブジェクトにはダミーの fetch を定義

としています。

 

 

ringo-web Pagesの作成

次に、Pages である ringo-web を作成します。

このPagesでは

  • ringo-db Workers と Service Binding RPC して、D1のデータを受け取る
  • D1のデータを元に、ブラウザで描画する

を担当します。

 

ライブラリのインストール

packages ディレクトリの中で、 create hono によりHonoアプリの雛形を作成します。

# インストール
$ bun create hono ringo-web

create-hono version 0.7.1
✔ Using target directory … ringo-web
? Which template do you want to use? cloudflare-pages
✔ Cloning the template
? Do you want to install project dependencies? yes
? Which package manager do you want to use? bun
✔ Installing project dependencies
🎉 Copied project files
Get started with: cd ringo-web


# ringo-webディレクトリへ移動
$ cd ringo-web

 
次に必要なライブラリをインストールします。

Reactまわりです。

$ bun add react react-dom
...
installed react@18.3.1
installed react-dom@18.3.1


$ bun add -d @types/react @types/react-dom
...
installed @types/react@18.3.3
installed @types/react-dom@18.3.0

 
Chart.js まわりです。

$ bun add chart.js react-chartjs-2
...
installed chart.js@4.4.3
installed react-chartjs-2@5.2.0

 
TanStack Routerまわりです。

$ bun add @tanstack/react-router
...
installed @tanstack/react-router@1.38.1


$ bun add -D @tanstack/router-vite-plugin @tanstack/router-devtools @tanstack/router-cli
...
installed @tanstack/router-vite-plugin@1.38.0
installed @tanstack/router-devtools@1.38.1
installed @tanstack/router-cli@1.37.0 with binaries:
 - tsr

 
TanStack Queryまわりです。

$ bun add @tanstack/react-query
...
installed @tanstack/react-query@5.45.1

 

wrangler.toml の編集

compatibility_date などを設定しておきます。

なお、今回の compatibility_date には、開発を始めた頃の日付 2024-06-18 を設定してあります。

name = "ringo-web"
pages_build_output_dir = "./dist"
compatibility_date = "2024-06-18"

 

実装に関するメモ

YAMLJSONへ変換するのに yq を使った

今までりんごに関する情報は apples.yaml というファイルで管理していました。
https://github.com/thinkAmi/dj_ringo_tabetter/blob/development/apples.yaml

今回、Cloudflareへ移行するにあたり、ソースコードはすべてTypeScriptになりました。そこで、YAMLでの管理をやめ、TypeScriptで管理することを考えました。

 
YAMLJSONへ変換するツールを探したところ、 yq があったため、使ってみることにしました。
https://github.com/mikefarah/yq

まずはインストールします。

$ sudo snap install yq

yq v4.40.5 from Mike Farah (mikefarah) installed

 
続いて、JSONファイルへと変換します。

$ yq -o json ./old_data/apples.yml > ./src/apples.json

 
あとはファイルに保存されたJSONをTypeScriptファイルへと移植・修正して使います。

 

デプロイしようとするとviteが途中でハングするので、それぞれ分ける

過去記事でも書きましたが、Pagesをデプロイしようとするとハングしてしまいます。

そこで、フロントエンド・バックエンド・デプロイの3段階に分けてビルド・デプロイします。

ただ、各ビルドでは途中でハングするので、途中でキャンセルしています。

まずはフロントエンドのビルド

$ bun run build:fe
...
dist/static/index.lazy-B-SIKAr-.js           1.01 kB │ gzip:  0.60 kB
dist/static/month.lazy-mKjT0B0Q.js           1.06 kB │ gzip:  0.64 kB
dist/static/appleLegendPlugin-C9-uDFHk.js  201.77 kB │ gzip: 70.46 kB
dist/static/client.js                      213.00 kB │ gzip: 68.13 kB
✓ built in 1.33s

^C  # ハングしたのでキャンセル

 
続いてバックエンドです。

$ bun run build:be
$ vite build
vite v5.3.1 building SSR bundle for production...
✓ 21 modules transformed.
dist/_worker.js  22.64 kB
✓ built in 196ms

^C  # ハングしたのでキャンセル

 
最後にデプロイします。

$ bun run deploy
$ wrangler pages deploy dist
The project you specified does not exist: "ringo-web". Would you like to create it?"
❯ Create a new project
✔ Enter the production branch name: … main
✨ Successfully created the 'ringo-web' project.
🌏  Uploading... (5/5)

✨ Success! Uploaded 5 files (2.38 sec)

✨ Compiled Worker successfully
✨ Uploading Worker bundle
✨ Uploading _routes.json
🌎 Deploying...
✨ Deployment complete! Take a peek over at https://578e4ac8.ringo-web.pages.dev

 

ringo-bsky Workersの作成

最後に、 Blueskyからリンゴの投稿を定期的に取得する ringo-sky Workersを作成します。

 

ライブラリのインストール

packages ディレクトリの中に ringo-bsky を作成し、その中で bun init します。

$ bun init
bun init helps you get started with a minimal project and tries to guess sensible defaults. Press ^C anytime to quit

package name (ringo-bsky): 
entry point (index.ts): 

Done! A package.json file was saved in the current directory.
 + index.ts
 + .gitignore
 + tsconfig.json (for editor auto-complete)
 + README.md

To get started, run:
  bun run index.ts

 
続いて、Blueskyにアクセスするためのライブラリ @atproto/api をインストールします。
https://www.npmjs.com/package/@atproto/api

$ bun add @atproto/api
bun add v1.1.13 (bd6a6051)

installed @atproto/api@0.12.23

11 packages installed [3.82s]

 

Blueskyのクレデンシャルをローカル・本番環境へ設定

Blueskyの投稿を取得するために、クレデンシャルを設定します。

Workersではクレデンシャルは

  • ローカル環境は .dev.env ファイル
  • 本番環境は wrangler のコマンド

を使います。
Secrets · Cloudflare Workers docs

 
まず、ローカル環境向けには ringo-bsky ディレクトリの直下に .dev.env ファイルを生成します。中身には以下のクレデンシャル情報を設定しておきます。

IDENTIFIER=自分のDID
APP_PASSWORD=アプリパスワードの値

 
次に、wranglerを使って本番環境用のクレデンシャルを設定します。

まずは IDENTIFIER を設定します。

$ wrangler secret put IDENTIFIER
...
✔ Enter a secret value: … ********************
🌀 Creating the secret for the Worker "ringo-bsky" 
✨ Success! Uploaded secret IDENTIFIER

 
次に APP_PASSWORD を設定します。

$ wrangler secret put APP_PASSWORD
...
✔ Enter a secret value: … *******************
🌀 Creating the secret for the Worker "ringo-bsky" 
✨ Success! Uploaded secret APP_PASSWORD

 
なお、 wrangler secret put すると、wrangler.toml の情報をもとに Cloudflare Workersが自動で作成されます。

このときに作成される Workers を確認したところ

  • 名前は wrangler.tomlで指定した name
  • Environment にシークレットが設定済
  • ソースコードexport default { fetch() {} } のみ

という状態でした。

 

KV へ処理済の投稿に関する情報を記録

Blueskyから投稿を取得・保存する際、重複してデータを保存しないよう「前回どこまで保存したか」を記録しておく必要があります。

今まではDBへ保存していました。ただ、Cloudflareには KV というキーバリューストアがあります。
Cloudflare Workers KV · Cloudflare Workers KV

「前回どこまで保存したか」という情報は1個だけ存在していればよいので、今回は KV を使って保存することにします。

 
まずは wrangler でグローバルなKVを作成します。

なお、wrangler kv:namespace コマンドのwarningが出ていますが、まだドキュメントには反映されていないようです。また、warningなので、KVはできているようです。
https://developers.cloudflare.com/workers/wrangler/commands/#create-3

$ wrangler kv:namespace create LAST_CURSOR_KV
▲ [WARNING] The `wrangler kv:namespace` command is deprecated and will be removed in a future major version. Please use `wrangler kv namespace` instead which behaves the same.
...
🌀 Creating namespace with title "ringo-bsky-LAST_CURSOR_KV"
✨ Success!
Add the following to your configuration file in your kv_namespaces array:
[[kv_namespaces]]
binding = "LAST_CURSOR_KV"
id = "f6f608a043

 
続いて、同じコマンドに --preview オプションを付けて、ローカルのKVを作成します。

なお、warningで指示されたコマンド wrangler kv namespace を使ったところ、warningが消えました。

$ wrangler kv namespace create LAST_CURSOR_KV --preview
...
🌀 Creating namespace with title "ringo-bsky-LAST_CURSOR_KV_preview"
✨ Success!
Add the following to your configuration file in your kv_namespaces array:
[[kv_namespaces]]
binding = "LAST_CURSOR_KV"
preview_id = "54ad69fc39bc429597e55ed0fe7acdd9"

 
最後に、wrangler.toml に KV の設定を追加します。

kv_namespaces = [
    { binding = "LAST_SEARCH_KV", id = "f6f608a04340492d87d3a10b0210cfa8", preview_id = "54ad69fc39bc429597e55ed0fe7acdd9" }
]

 
あとは、

// 読み込み
await env.LAST_SEARCH_KV.get(BSKY_KV_KEY)

// 書き込み
await env.LAST_SEARCH_KV.put(BSKY_KV_KEY, latestCreateAt)

のような感じで使います。

 

Cron Trigger による定期実行を設定

Cloudflare Workerでは Cron Trigger を使って定期実行を実現できます。
Cron Triggers · Cloudflare Workers docs

 
そこで、wrangler.toml へ定期実行タイミングの設定を行います。

なお、タイムゾーンUTC であることに注意します。以下の例では、毎日、日本時間の午前3時に起動します。

[triggers]
crons = [ "0 18 * * *" ]

 
あとは、 scheduled ハンドラを持ったオブジェクトを default export します。

import type { ExportedHandler } from 'cloudflare:workers'

export default {
  async scheduled(_event: any, env: Env) {
    const bsky = new Bsky(env)
    await bsky.run()
  },
} as ExportedHandler<Env>

 
ローカルで動作確認するには、 ringo-bsky

$ wrangler dev --test-scheduled

にて起動します。

それに加え、別のターミナルから curl を使って

$ curl "http://localhost:8787/__scheduled?cron=*+*+*+*+*"

のようにアクセスします。
Test Cron Triggers | Cron Triggers · Cloudflare Workers docs

 

データ移行について

アプリを Google Cloud から Cloudflare へ移行するのに伴い、

  • 移行元
  • 移行先
    • Cloudflare D1

というデータ移行が必要になります。

今回は

  • Google Cloud Storageから、ローカルのSQLiteファイルとしてリストア
  • ローカルのSQLiteから、ローカルのD1へデータをリストア
  • ローカルのD1から、Cloudflare D1へリストア

というステップでデータ移行を行います。

 

Litestreamを使い、ローカルファイルとしてリストアする

Litestreamのドキュメントに従い、 litestream restore コマンドでローカルへSQLiteをリストアします。
Replicating to Google Cloud Storage - Litestream

 

リストアしたSQLiteからローカルのD1へデータを投入する

ローカルでの動作確認を可能にするため、LitestreamでリストアしたSQLiteをローカルのD1へ投入します。

データを投入するときの方針は以下です。

  • Cloudflare環境では不要なデータをリストアしない
  • Drizzle ORMを使って、ローカルのD1へデータを投入する

 
そこで、次のようなスクリプトを作成します。

なお、ローカルのD1のファイル名などについては、環境によって異なります。

// @ts-ignore
import { Database } from 'bun:sqlite'
import { sql } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/bun-sqlite'
import { feeds } from '../../db/schema/feeds'

type Tweet = {
  id: number
  name: string
  tweet: string
  tweeted_at: string
  tweet_id: number
}

const main = async () => {
  const fromSqlite = new Database('old_data/ringo_2024_0502.db')
  const fromDb = drizzle(fromSqlite)
  const tweets: Tweet[] = await fromDb.all(
    sql.raw('select * from tweets_tweets'),
  )

  // 環境に応じてファイル名を修正する
  const fileName =
    '2073307253fd76d9e289ad074b54fc751825840a1efddf02aa13a82ecf5305f6.sqlite'
  const toSqlite = new Database(
    `.wrangler/state/v3/d1/miniflare-D1DatabaseObject/${fileName}`,
  )
  const toDb = drizzle(toSqlite)

  // biome-ignore lint/complexity/noForEach: <explanation>
  tweets.forEach(async (t) => {
    await toDb.insert(feeds).values({
      name: t.name,
      content: t.tweet,
      createdAt: t.tweeted_at,
      snsId: t.tweet_id.toString(),
    })
  })

  console.log('finished')
}

main()

 
続いて、 ringo-db ディレクトリの中で、スクリプトを実行します。

$ bun run scripts/development/import_local_db.ts 
finished

 
ローカルのD1を確認すると、必要なデータが投入されていました。

 

本番のD1に対してマイグレーションを実行する

ローカルでの動作確認ができたところで、次は本番環境です。

ここで、 ringo-db Workersを初めてデプロイしたとき、同時にD1も作成されているはずです。

$ wrangler deploy --minify
...
Total Upload: 64.01 KiB / gzip: 18.33 KiB
Your worker has access to the following bindings:
- D1 Databases:
  - DB: ringodb (01febb3d-148f-4f3c-8fea-e32445da1ae1)
Uploaded ringo-db (3.96 sec)
Published ringo-db (4.40 sec)
...

 
ただ、この時点ではD1はあるもののテーブルが存在しません。

そこで、wranglerを使い、本番のD1に対してマイグレーションを実行します。
https://developers.cloudflare.com/workers/wrangler/commands/#migrations-apply

$ wrangler d1 migrations apply ringodb --remote
...
Migrations to be applied:
┌─────────────────────┐
│ name                │
├─────────────────────┤
│ 0000_wild_pride.sql │
└─────────────────────┘
✔ About to apply 1 migration(s)
Your database may not be available to serve requests during the migration, continue? … yes
🌀 Executing on remote database ringodb (01febb3d-148f-4f3c-8fea-e32445da1ae1):
🌀 To execute on your local development database, remove the --remote flag from your wrangler command.
🚣 Executed 2 commands in 0.4022ms
┌─────────────────────┬────────┐
│ name                │ status │
├─────────────────────┼────────┤
│ 0000_wild_pride.sql │ ✅       │
└─────────────────────┴────────┘

 

ローカルのD1からデータをエクスポートする

Cloudflareの以下のドキュメントを参考に、ローカルのD1からデータをエクスポートします。
Export an existing D1 database | Import and export data · Cloudflare D1 docs

なお、ローカルのD1なため、エクスポート時には --local フラグが必要です。

$ wrangler d1 export ringodb --local --output=./old_data/ringodb_local_2024_0706.sql
...
🌀 Exporting local database ringodb (01febb3d-148f-4f3c-8fea-e32445da1ae1) from .wrangler/state/v3/d1:
🌀 To export your remote database, add a --remote flag to your wrangler command.
🌀 Exporting SQL to ./old_data/ringodb_local_2024_0706.sql...
Done!

 
エクスポートしたデータを見たところ、マイグレーション済な本番環境では不要なマイグレーション関係のデータが含まれていました。

そこで、エクスポートしたデータから、以下の内容を削除しました。

CREATE TABLE d1_migrations(
        id         INTEGER PRIMARY KEY AUTOINCREMENT,
        name       TEXT UNIQUE,
        applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
INSERT INTO d1_migrations VALUES(1,'0000_wild_pride.sql','2024-06-17 23:22:59');
CREATE TABLE `feeds` (
    `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
    `name` text,
    `content` text,
    `created_at` text DEFAULT (CURRENT_TIMESTAMP),
    `sns_id` text
);

 

本番のD1に対してインポートする

次に、wranglerを使って、ローカルへエクスポートしたデータを本番のD1へインポートします。

エクスポートしたデータはSQLファイルになっているため、 wrangler d1 execute を使います。
https://developers.cloudflare.com/workers/wrangler/commands/#execute

$ wrangler d1 execute ringodb --remote --file=./old_data/ringodb_local_2024_0706.sql
...
✔ ⚠️ This process may take some time, during which your D1 database will be unavailable to serve queries.
  Ok to proceed? … yes
🌀 Executing on remote database ringodb (01febb3d-148f-4f3c-8fea-e32445da1ae1):
🌀 To execute on your local development database, remove the --remote flag from your wrangler command.
Note: if the execution fails to complete, your DB will return to its original state and you can safely retry.
├ 🌀 Uploading 01febb3d-148f-4f3c-8fea-e32445da1ae1.c8499ae13caabeb9.sql 
│ 🌀 Uploading complete.
│ 
🌀 Starting import...
🌀 Processed 751 queries.
🚣 Executed 751 queries in 0.04 seconds (2240 rows read, 1498 rows written)
   Database is currently at bookmark 00000005-00000000-00004dc6-da501f7be681ab1e632fa9c002d2fc69.
┌────────────────────────┬───────────┬──────────────┬────────────────────┐
│ Total queries executed │ Rows read │ Rows written │ Database size (MB) │
├────────────────────────┼───────────┼──────────────┼────────────────────┤
│ 751                    │ 2240      │ 1498         │ 0.19               │
└────────────────────────┴───────────┴──────────────┴────────────────────┘

 
インポート後、Cloudflare上のD1を確認すると、データが投入されていました。

 

その他

Pagesでカスタムドメインを設定

Cloudflare Pagesでは、アプリにカスタムドメインを設定できます。
Custom domains · Cloudflare Pages docs

 
今回は以下の流れで対応しました。

  • Cloudflare Pagesでの作業
    • Cloudflare PagesのCustom domains をタブを開き、 Set up a custom domain をクリック
    • サブドメインで運用するので、 ringosky.thinkami.dev を入力
    • ドメインDNSは変更しないので、 My DNS providerの Begin CNAME setup を選択
    • NameとTargetが表示されるので、コピー
  • ドメインを管理している Squarespace での作業
    • カスタムレコードの レコードを追加 をクリックし、表示されている値を設定
  • 再度、Cloudflareでの作業
    • Check DNS records をクリック
    • 「Your records for ringosky.thinkami.dev are being rechecked. You’ll be notified by email when your domain is activated.」と表示されるので、しばらく待つ

 
しばらく待つと、カスタムドメインでの運用ができるようになりました。

 

移行前のデータ削除

Cloudflareで運用できるようになったので、Cloud Runで管理しているデータを削除しておきます。

  • Cloud Run
  • Cloud Storage のすべてのバケット
  • Cloud Scheduler
  • サービスアカウント
  • Secret Manager
  • Artifact Registry

 
お世話になりました。ありがとうございました。

 

ログに TypeError: e.env.RINGO_DB_TOTAL.calculateByName is not a function のように表示されたとき

今回、

  • ローカルでは、Service Binding RPC まわりは問題なく動作している
  • 本番環境にデプロイすると、動作しない
    • 本番環境のログに、「TypeError: e.env.RINGO_DB_TOTAL.calculateByName is not a function」が出力されて動作しない

ということが起きました。

 
Service Binding RPC の設定はすべてドキュメント通り行なっているはずでしたが、うまくいきませんでした。

そんな中、wrangler.toml を見たところ、 compatibility_date の値が Pages や Workers の間で差異があることに気づきました。

 
そこで、 compatibility_date の値を 2024-06-18 へ統一したところ、動作するようになりました。

本当にこの対応でよいのか自信はありませんが、手元ではこの方法で解決したため、メモとして残しておきます。

 

動作確認

以下のURLで動作しています。起動も速くなりました。
https://ringosky.thinkami.dev/

 
月ごとの表示はこちら。
https://ringosky.thinkami.dev/month

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi/cf_ringo_sky