STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

開発フローでちょっと便利なツールをCloudflare Workersで作る

STORES 予約 でエンジニアリングマネージャーをしています、ykpythemindです。この記事は STORES Advent Calendar 2022 20日目です。

STORES 予約 では以前ご紹介した、細かくPull Requestを積み重ねてデプロイをしていく戦略 で開発をしています。今回はCloudflare Workers を用いて私達の開発を少し改善にした方法をご紹介します。

今回つくったもの

以前の記事 にあるように私達はPull Requestをマージした後、Slack上のデプロイ専用のチャンネルでスラッシュコマンドを打つことでデプロイを発火しています。

デプロイしている図

Pull RequestのAuthorがマージをし、その人がデプロイするフローにしています。しかし、

  • マージしたあと本人がデプロイを忘れることがある
  • スラッシュコマンドだけ見てもこの後なにのデプロイが行われるかパッと見分からない

などの課題がありました。今回は GitHubからの Webhook (Pull Request event) を受け取って、マージした人にSlack上でメンションしてくれる通称「デプロイしてね」くんをCloudflare Workerを使って建ててみました。

メンションが来るし、デプロイコマンドも教えてくれるようになった

プロジェクトの初期化

wrangler というCloudflare謹製のCLIを使ってWorkersプロジェクトを初期化します。

$ yarn add -D wrangler
$ yarn run wrangler init .

TypeScriptを使うか聞かれるので y を選び、 Fetch handlerを作成するようにします。

開発用環境を立ち上げる

初期化したてのプロジェクトにすでに以下のようなコードがあるので、これをローカル環境で動かしてみましょう。

export default {
    async fetch(
        request: Request,
        env: Env,
        ctx: ExecutionContext
    ): Promise<Response> {
        return new Response("Hello World!");
    },
};
$ yarn wrangler dev

この際Cloudflareへのログインを求められることがありますが、お持ちのアカウントでログインしておきましょう。

$ curl http://127.0.0.1:8787
Hello World!

リクエストすると返ってきますね。

実装

実際にGitHubからのWebhookイベントを受け取る実装を書きます。

$ yarn add -D @octokit/webhooks-types

Webhookの型をインストールし、src/index.tsを書き換えます。

import { PullRequestEvent } from "@octokit/webhooks-types";

export interface Env {
  SLACK_WEBHOOK_URL: string;
}

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext
  ): Promise<Response> {
    const json = (await request.json()) as PullRequestEvent;

    if (json.action !== "closed") {
      console.log("[ignored] not closed.");
      return new Response("");
    }

    if (!json.pull_request.merged) {
      console.log("[ignored] not merged.");
      return new Response("");
    }

    const baseRef = json.pull_request.base.ref;

    if (!["main", "master"].includes(baseRef)) {
      console.log("[ignored] not main branch");
      return new Response("");
    }

    const ghUser = json.pull_request.merged_by?.login ?? "unknown";
    const repo = json.repository.name;

    const command = findCommandByRepo(repo);
    const userDef = users.find((v) => v.github === ghUser);
    const slackUserId = userDef?.slack;

    const webhookUrl = env.SLACK_WEBHOOK_URL;

    const body = `
[${repo}] "${json.pull_request.title}" merged by ${ghUser} ( ${json.pull_request.html_url} )

<@${slackUserId}> デプロイを実行してください! ${command}
    `;

    await fetch(webhookUrl, {
      body: JSON.stringify({ text: body }),
      method: "POST",
      headers: { "Content-Type": "application/json" },
    });

    return new Response("ok");
  },
};

// 使用しているrepository名で適宜調整してください
function findCommandByRepo(repo: string) {
  switch (repo) {
    case "rsv-rails":
      return "`/deploy_rsv_rails production`";
    case "rsv-dashboard":
      return "`/deploy_rsv_dashboard production`";
    // ....略
    default:
      return "";
  }
}

const users = [
  { github: "ykpythemind", name: "ykpythemind", slack: "U019L5WSJJE" },
  { github: "waniji", name: "waniji", slack: "UAQAKQGSG" },
  { github: "necocoa", name: "nat", slack: "U0248481LQ5" },
  // ......
] as const;

コードの肝としては、Pull Requestイベントの中でもmaster/mainブランチへのマージ以外のものは処理しないというところです。 また、GitHubのidとSlack user idのマッピングをハードコードしておき、そちらを用いてSlackメンションが可能になるようにしています。 *1

また、Slackの管理画面からIncoming webhookを投げられるURLを取得しておき、これを環境変数経由で用います。開発環境では .dev.vars ファイルに記入すると Workersの実行環境にbindingされます。

# .dev.vars
SLACK_WEBHOOK_URL=xxxx

GitHub repository -> Settings -> Webhooks から、新規Webhookを登録します。

  • Payload URL は ngrok http 8787 などでローカルにフォワーディングしているURLを一旦入れる
  • Content type: application/jsonを選択
  • Secret : 適当に生成しておく(後で使います)
  • Which events would you like to trigger this webhook? : Pull Requestを選択

こうして適当なPull Requestを開いてマージすると、Slackに通知が飛ぶようになりました。

Signatureの検証

このままだと任意のリクエストを受け付けてしまうので、さきほどWebhook画面で設定した署名を検証しましょう。

こちらはcloudflare-worker-github-app-example(license: ISC)のコードを利用します。

https://github.com/gr2m/cloudflare-worker-github-app-example/blob/5a0d0bec9c096f5a922acbbaa9fe2da882093985/lib/verify.js#L1 の内容を保存し、importして使います。

.dev.varsにさきほどひかえたWEBHOOK_SECRETを記入して、署名を検証できるようになりました。

import { verifyWebhookSignature } from "./verify.js";

export interface Env {
  SLACK_WEBHOOK_URL: string;
  WEBHOOK_SECRET: string;
}

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext
  ): Promise<Response> {
    const payloadString = await request.text();
    const json = JSON.parse(payloadString) as PullRequestEvent;

    const secret = env.WEBHOOK_SECRET;
    const signature = request.headers.get("x-hub-signature-256") ?? "";

    try {
      await verifyWebhookSignature(payloadString, signature, secret);
    } catch {
      return new Response("invalid signature", { status: 400 });
    }
    // ... 略 ...

wrangler publishで配布する

wrangler publish と打つとすぐにアップロードされ、CloudflareのCDN edgeで使用可能になります。

また、

yarn wrangler put WEBHOOK_SECRET
yarn wrangler put SLACK_WEBHOOK_URL

をそれぞれ実行し、秘匿変数を設定しましょう。

設定している秘匿変数のキーは wrangler secret list で見ることができます。後はGitHubの管理画面からWebhook URLをCloudflare Workersのものに変えれば完成です!

まとめ

今回Cloudflare Workersを使い、素早くこういったツールを作るのにとても便利で洗練されている印象を受けました。 弊社では Cloudflare Waiting Roomの導入実績 や、アドベントカレンダー3日目にも「Cloudflare Workers を使って社内で趣味を楽しんでいる話」があるなど、積極的に活用しています。

【お知らせ】採用もしています。STORESをよろしくお願いいたします。

jobs.st.inc

*1: SlackのUserIdはProfileからCopy member IDを選択し取得できます