Cloudflare WorkersのCron Triggersでリリース当番通知botを作った話

こんにちは、syumaiです!

ベースマキナでは、現在Cloudflare WorkersのCron Triggersを活用したリリース当番通知botを社内で運用しています。このbotは、リリース対象日の朝にリリース担当メンバーにメンションを行います。実装はTypeScriptで行われています。

今回の記事では、

  • なぜリリース当番通知botを作ることにしたのか
  • なぜCloudflare Workersを使ったのか
  • Cloudflare Workersによる定期実行Workerの実装例

などについて紹介させていただきます。

なぜリリース当番通知botを作ることにしたのか

もともと、ベースマキナでは、リリース担当のメンバーを特に決めていませんでした。リリース担当が決まっていないと、自然と「直近機能開発を行ったメンバー」がリリースを自主的に行うようになります。すると、タスクの持ち具合によって、設計主体のメンバーはリリース作業の頻度が低く、開発主体のメンバーはリリース作業の頻度が高くなり、不均等な状態が生まれていました。

リリース作業の頻度が不均等になると、

  • リリースそのものにかかる作業時間
  • 作業工程に対する理解度

についても同様に均等性が失われてしまうという問題があります。リリース作業は極力自動化されているとはいえ、リリース前に確認しないといけないことが色々ありますし、作業中の待ち時間が発生することもあります。また、リリースの作業工程に対する理解度が低いと、よくリリース作業を行っているメンバーが欠席した場合や、緊急時のHotfix対応が必要になった場合に作業がうまく行えないリスクがあります。

こうした問題を防ぐために、「リリース当番を輪番制で自動的に割り当て、チームメンバー全員が確認しやすい形で通知する」というのが今回実現したいことでした。

なぜCloudflare Workersを使ったのか

Cloudflare Workersは、実は「定期的にJavaScriptを実行する基盤」として非常に手軽です。

本来、Cloudflare WorkersはCDNエッジでJavaScriptの実行を行い、クライアントからのリクエストや、オリジンサーバーからのレスポンスに対して加工を行なう、リバースプロキシ的な用途が主体となっています。 しかしながら、ここ近年、リバースプロキシ的な用途だけに限らず、Cloudflare Workers単体でアプリケーションとして利用するのに十分な機能が提供されています。 例えば、Cloudflare WorkersからJavaScript SDKで簡単に呼び出せるWorkers KV (キーバリューストア)、D1 (リレーショナルデータベース)、R2 (オブジェクトストレージ)などがあります。

いまや、Cloudflare Workersは単にJavaScript製アプリケーションの実行環境としてみなすことができ、リバースプロキシ的な使い方はあらゆる用途の中の一つでしかないと考えるのが適切でしょう。

今回は、Cloudflare WorkersのCron Triggersという機能をWorkerの定期実行に利用しました。

Cron Triggersとは

Cloudflare Workersは、Triggersと呼ばれる機構によって起動されます。 設定できるTriggersの種類の一覧は、Cloudflareのダッシュボード内にあるWorkerの設定ページの、「Triggers」タブから確認できます。 Cloudflare Workersを起動するための最も一般的な方法はWorkerのURLに対するHTTPリクエストですが、これもTriggerのひとつとして見ることができます。そして、HTTPリクエストなしでWorkerを起動する方法もあります。 例えば、メールの受信をTriggerとしてWorkerを起動するEmail Triggersがあります。 こうしたHTTPリクエスト以外の方法で起動されるWorkerは、HTTPによるエンドポイントを完全に無効化して使うこともできます。

Cron Triggersは、cronスケジュールを使ってWorkerを定期実行するためのTriggerです。 無料プランでは全部で5つまで、有料プランでは250個までと、作成数に限りはありますが、追加料金無しで気軽に使うことのできる機能となっています。

Cloudflare Workersによる定期実行Workerの実装例

実装したのは以下の機能です。

  • Slack通知
  • 当番の割り当て
  • 土日・祝日と、その前日のSlack通知の停止機能

完成形のコードは以下のリポジトリにて公開しています。

github.com

Slack通知

jsx-slackを使っています。jsx-slackを使うと、Slackのメッセージを表現するBlockをJSXで簡単に書くことができます。 本来、SlackのメッセージのBlockはJSONで表現する必要があり、直接JSONを書くか、何かしらのライブラリを導入した上で(JSXではなく)コードで組み立てる形になります。 これをJSXによって代替することにより、大幅に学習コストを下げられていると感じます。メッセージの構造が視覚的にわかりやすいので、レビューもしやすいです。

export const postReminder = async (webHookUrl: string, members: string) => {
  const parsedMembers = parseMemberSlackIds(members);
  const blocks = (
    <Blocks>
      <Section>
        <a href={`@${getTodayMemberSlackAccountId(parsedMembers)}`} />
        <br />
        本日のリリース確認作業をしましょう!
      </Section>
    </Blocks>
  );
  return await postBlocksToSlack(webHookUrl, blocks);
};

https://github.com/basemachina/release-reminder-worker/blob/85044854e6ba795b6b738fd9cef83be56b254433/src/reminder.tsx#L24-L36

実際に送信されるメッセージの表示例

メッセージの送信処理は、単にWebHook URLにPOSTでJSON化したBlockを送っているだけです。 Cloudflare Workersからは、(当然ですが)このようにfetchを呼び出して外部にリクエストを送ることが簡単にできます。 ややこしいのがメッセージのBlockを組み立てる箇所だけだったので、Slack通知関連でjsx-slack以外のライブラリは導入せずに済みました。

export const postBlocksToSlack = async (webHookUrl: string, blocks: any) => {
  return await fetch(webHookUrl, {
    body: JSON.stringify({ blocks }),
    method: "POST",
    headers: { "Content-Type": "application/json" },
  });
};

https://github.com/basemachina/release-reminder-worker/blob/85044854e6ba795b6b738fd9cef83be56b254433/src/slack.ts#L1-L7

当番の割り当て

当番の割り当ては、単にメンバーのリストを日で割って行っています。今は均等に回せていますが、メンバー数が7の倍数になると曜日が固定になってしまうので、何かしらの調整方法を考えます…! メンバーのリストは、名前:SlackIDの組でCloudflare Workersの環境変数に持つようにしています。これはハードコードしてしまってもよかったのですが、公開リポジトリに切り替えやすくしたかったためこのようにしています。 実際の運用としては、以下のような設定内容をwrangler.tomlに直接commitしています。

[vars]
# `名前:Slack ID`の形式で改行区切りで指定する
MEMBER_SLACK_IDS = """
example1:UXXXXXXXXXX
example2:UXXXXXXXXXX
"""

https://github.com/basemachina/release-reminder-worker/blob/85044854e6ba795b6b738fd9cef83be56b254433/wrangler.toml#L13-L18

土日・祝日と、その前日のSlack通知の停止機能

土日・祝日と、その前日は原則リリース作業を行わないため、通知を行っていません。

まず、wrangler.tomlに設定するCron Triggersのスケジュール自体を、金・土・日を除外した内容にします。

[triggers]
crons = ["0 1 * * MON-THU"] # 日本時間の月〜木の朝10時に実行

https://github.com/basemachina/release-reminder-worker/blob/3eba148ddc2bccfa84259c544c821519b3eee46f/wrangler.toml#L7-L11

この上で、祝日を考慮した除外処理を追加します。 日本の祝日情報を配布しているholiday_jpと言う便利なライブラリがあったので、今回はこちらを利用させていただきました。

export const isHolidayOrBeforeHoliday = () => {
  const today = new Date();
  const tomorrow = getTomorrow();
  return holiday.isHoliday(today) || holiday.isHoliday(tomorrow);
};

https://github.com/basemachina/release-reminder-worker/blob/1c74af5b0865574b1aa565e675e88014d363de2d/src/holiday.ts#L10-L14

あとは、Cron Triggersによって起動されるscheduledハンドラーに、この除外処理によるフィルタを行った上で送信処理を実装するだけです。

const postMessages = async (webHookUrl: string, members: string) => {
  if (isHolidayOrBeforeHoliday()) {
    return;
  }
  await postReminder(webHookUrl, members);
};

export default {
  scheduled: async (_, env) => {
    await postMessages(env.SLACK_WEBHOOK_URL, env.MEMBER_SLACK_IDS);
  },
} satisfies ExportedHandler<Env>;

開発・運用してみた感想

開発自体は、元々Cloudflare Workersを使ったことがあったのですが、やはり楽でした。

まず、Cloudflare Workersの開発・デプロイに使うWranglerは、TypeScriptのトランスパイル、成果物のバンドルを巻き取ってくれるので、Webpackの設定などを特に書く必要がなく非常に手軽でした。

また、Cron Triggersのデバッグも、開発サーバーをwrangler dev --test-scheduledのフラグ付きで起動すれば、Cronスケジュールを待たずに済む、デバッグ用のエンドポイントを生やしてくれるので便利でした。(curl "http://localhost:8787/__scheduled?cron=*+*+*+*+*" で呼べます)

特に不具合もなく、運用もつつがなく回っています。日次でメンションされたメンバーが担当者となることでリリース機会が均等化されましたし、「あとでリリースしないと」という意識を持たなくても自然とリリースが行われるようになったので、心理的負荷が減ったと感じています。

おわりに

Cloudflare Workersは、思った以上に手軽に導入できる、あらゆる用途に利用可能なJavaScriptアプリケーションの実行環境です。 今回のように運用オペレーションの改善にも活用することができます。 JavaScriptを定期的に実行したいと思った時に、ぜひ選択肢として検討してみてください。

少々個人的な宣伝ですが、Software Design 2024年8月号にて、Cloudflare WorkersのCron Triggersについてのより詳しい解説を執筆しました。もし興味があれば、ぜひこちらもご覧ください。

gihyo.jp