🐯

Hono を利用しているプロジェクトで Cloudflare Workers の Cron Triggers を設定する

2024/01/22に公開

どうも、torahack こと稲垣です。

私は最近、「マネジメント:開発 = 7:3」くらいの業務比率で、エンジニアリングマネージャー(?)に近い動きをしています。

開発プロジェクト初期段階に技術選定をしてコードベースを作成するのですが、最近は Cloudflare に注目しています。そして Cloudflare を基盤とするプロジェクトで API 等サーバーサイドの開発をする場合、フレームワークとして Hono を利用している方は多いでしょう。(多少の偏見および思想が入っている。 Hono 最高!という話はまた別の記事でしたい所存)

Hono を利用しているプロジェクトでサーバーサイドの開発を進める中で「定期実行したい処理はどう実現しようか」という問題にぶつかりました。
結論としてたどり着いたのは Cloudflare Workers が提供している Cron Triggers を利用することです。

Cron Triggers とは

Cron Triggers では、scheduled() handler を用いて Workers をスケジュール実行できます。

簡易的なバッチ処理を実装するのに適していると思います。
( e.g. 外部API からデータ取得して D1 のデータベース に同期するなど)

Cron Triggers の一般的な設定方法

公式ドキュメントを参照して Workers に scheduled() handler を定義します。
※以下のサンプルコードは公式ドキュメントと同等です。

index.ts
const handler: ExportedHandler = {
  async scheduled(event, env, ctx) {
    console.log("cron processed");
  },
};
export default handler;

Cron Triggers という名前の通り、一般的な cron 式でスケジュールを定義できます。
Workers を Wrangler で管理している場合、 wrangler.toml に定義してください。

wrangler.toml
[triggers]
# Schedule cron triggers:
# - At every 3rd minute
# - At 3PM on first day of the month
# - At 11:59PM on the last weekday of the month
crons = [ "*/3 * * * *", "0 15 1 * *", "59 23 LW * *" ]

なお、ダッシュボード上でも Cron Triggers を設定できる UI が用意されています。

ちなみに、1つの Workers に複数の Cron Triggers を定義できます。cron 式で分岐させることになるようです。

index.ts
const handler: ExportedHandler = {
  async scheduled(event, env, ctx) {
    // Write code for updating your API
    switch (event.cron) {
      case "*/3 * * * *":
        // Every three minutes
        await updateAPI();
        break;
      case "*/10 * * * *":
        // Every ten minutes
        await updateAPI2();
        break;
      case "*/45 * * * *":
        // Every forty-five minutes
        await updateAPI3();
        break;
    }
    console.log("cron processed");
  },
};

export default handler;

ローカル環境でもテスト実行できます。

npx wrangler dev --test-scheduled

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

Hono を利用しているプロジェクトに Cron Triggers を統合する

本題&結論を書きます。

Cloudflare Workers のランタイムで Hono を利用する場合、一般的にはこう書きますね。

index.ts
import { Hono } from 'hono'
const app = new Hono()

app.get('/', (c) => c.text('Hello Cloudflare Workers!'))

export default app

同じプロジェクト内で Cron Triggers を設定したい場合、こう書けます。

index.ts
import { Hono } from 'hono'
const app = new Hono()

app.get('/', (c) => c.text('Hello Cloudflare Workers!'))

const scheduled: ExportedHandlerScheduledHandler = async (event, env, ctx) => {
  ctx.waitUntil(doSomeTaskOnASchedule())
}

export default {
  fetch: app.fetch,
  scheduled,
}

この実装方法は以下の記事を参考にしました。
https://wp-kyoto.net/add-cloudflare-workers-scheduled-function-into-hono-applicarion/#google_vignette

Hono は Cloudflare Workers で扱える handler のうち fetch handler を提供しています。なので以下は同等ということだそうです。

index.ts
- export default app
+ export default {
+   fetch: app.fetch
+ }

同様の仕様で、Email handler なども統合できるのかなーと思います。

さて、以降はこの実装方法に対して私が感じるメリットと、そのメリットを活かした具体例を紹介します。

Hono プロジェクトに Cron Triggers を統合できると何が嬉しいのか?

私は Hono + Cloudflare D1 + Prisma + Kysely という構成で API を構築しています。
Cron Triggers として定期実行したい関数内で、 Hono で作る API と同様にデータベース操作ができることが嬉しいです。当然、Schema から生成された型定義やクエリも参照できます。

この実装方法を知る前は、別リポジトリを立てたりモノレポにしようと試みたのですが、上述した型定義やクエリを共有するのが面倒でした。

実際に Hono プロジェクトに Cron Triggers を統合した例

私は Cron Triggers でスケジュール実行する関数内で、GCP の OAuth 2.0 クライアントで外部 API をコールして、取得したデータを Cloudflare D1 に保存するということをしました。

OAuth 2.0 クライアントの秘匿情報は wrangler.toml 内に環境変数として定義しています。

wrangler.toml
[vars]
OAUTH_CLIENT_ID = "YOUR_OAUTH_CLIENT_ID"
OAUTH_CLIENT_SECRET = "YOUR_OAUTH_CLIENT_SECRET"
REFRESH_TOKEN = "YOUR_REFRESH_TOKEN"

以下のようにEnv型に環境変数を定義して、ExportedHandlerScheduledHandler<T>のジェネリクスに渡すことで、型安全にenvパラメータにアクセスできます。
また、DB: D1Databaseも環境変数として渡しているので、Cron Triggers として定期実行したい関数内で prisma-kysely のクライアントを利用することができます。

index.ts
import { Hono } from 'hono'

type Env = {
  DB: D1Database
  OAUTH_CLIENT_ID: string
  OAUTH_CLIENT_SECRET: string
  REFRESH_TOKEN: string
}

const app = new Hono<{ Bindings: Env }>()

// `ExportedHandlerScheduledHandler<T>`のジェネリクスに`Env`型を指定する
// スケジュールされたハンドラー関数内で`env`パラメータを使用して、`Env`型で定義された環境変数に型安全にアクセスできます。
const scheduled: ExportedHandlerScheduledHandler<Env> = async (event, env, ctx) => {
  ctx.waitUntil(callExternalApiByOauthClient(env))
}

export default {
  fetch: app.fetch,
  scheduled,
}

まとめ

Hono を利用しているプロジェクトで Cloudflare Workers の fetch handler だけでなく scheduled handler も利用する方法をご紹介しました。

最後に、toraco株式会社では2024年11月1日にエンジニア向けのコミュニティを立ち上げました。
Discord のサーバーで運営しており、以下のリンクから無料で参加できます。コミュニティ内では以下のような投稿・活動がされます!

https://discord.gg/pxfMjDfsge

  • もくもく会・作業ラジオ・雑談部屋などオンライン上での交流
  • オフラインイベントの案内
  • 代表の稲垣(トラハック)が公開するコンテンツの説明・質問回答
  • toraco株式会社からの副業や案件の紹介
  • フロントエンド関連技術の情報共有および議論
  • 生成AI関連技術のキャッチアップ
  • その他、技術領域にこだわらない情報共有および議論
toraco株式会社のテックブログ

Discussion