🔥

Honoをesbuild wasmでコンパイルし、CF WorkersにAPI経由でデプロイしてみた

2024/06/30に公開1

AIにHonoJSを使ったAPIのコードを書かせて遊べるHanabi.restというサービスを作っています。
ブラウザ上でテキストを入力したらREST APIが完成し、そのまま実際にリクエストを送ったりコードを編集したりできます。

そのサービス内でAPIをブラウザ上からCloudflare Workersにデプロイ出来る機能もあるのですが、実装するにあたっていくつかハマった点がありました。
実際に組み込む際のtipsなども含めてご紹介します。

大まかな流れ

API経由でデプロイするには大まかに以下の流れを踏む必要があります。
今回の事例ではD1 Databaseをバインドするのでその関連の処理が含まれています。

  1. デプロイするユーザーのCloudflare APIトークン、Account Idを取得する
  2. D1 Databaseを作成する
  3. テーブル作成などのSQLクエリを実行する
  4. Honoで書かれたコードをesbuild wasmでコンパイルし単一のjavascriptファイルにする。
  5. javascriptファイルとbindingの情報を含めてワーカーを作る
  6. ワーカーのサブドメインを有効化する
    一つ一つ見ていきましょう。

はじめに

今回Cloudflare WorkersのAPIを触っていくにあたってCloudflare SDKを使っていきます。

$ pnpm install cloudflare

これでSDKを使う準備が整いました。
一つ注意点としてこの公式 SDKCloudflare APIのドキュメントも全然正しくないことがあるのでご注意ください。

APIトークンとAccount Idを取得する

ワーカーをデプロイするには事前にデプロイするユーザーのCloudflareのAPIトークンとAccount Idを取得する必要があります。自分のアカウントにデプロイするのなら以下のページからアクセストークンを取得したり、プロジェクトのページからAccount Idをコピーすることができます。
https://dash.cloudflare.com/profile/api-tokens
しかしサービスに組み込む場合、アクセス許可の設定などいくつかわかりにくい点が出てきます。
その場合は以下のツールを使ってユーザーがAPIトークン取得ページを開く際にアクセス許可をデフォルトで設定された状態にしておくといいでしょう。
https://cfdata.lol/tools/api-token-url-generator/

APIトークンを取得出来たら

const client = new Cloudflare({
    apiToken: token
});
const result = await client.user.tokens.verify(); // トークンが期限切れではないかの確認

if (result.status) {
    const accounts = await (await client.accounts.list()).result; // ユーザーに紐づけられたアカウントのリスト
    //`{ id: string; name: string; type: string; }[]` のような配列 (ライブラリ上の型はunknown❗)
}

このようなコードを書くことでアカウントのIDを取得できます。アカウントはユーザーごとに2つ以上持っている可能性もあるためサービスに埋め込む場合は以下のようにアカウント選択のUIなどを実装する必要があるでしょう。

これで事前準備としてAPIトークンとAccount Idを取得できました。

D1 Databaseを作成する

今回はワーカーに新規に作成したD1 Databaseをバインドするのでそのために必要なAPIをたたいていきます。
まず以下のコードを実行して指定した名前でD1 Databaseを作成します。

const result = await client.d1.database.create({
    account_id: accountId,
    name: "example-db",
});

const d1Id = d1Id = result.uuid;

この際、先ほど取得したAccount Idとデーターベースの名前を指定します。なおこの際のデーターベースの名前は一意であり重複してはいけません。戻り値から取得できるuuidは後々使うため変数にでも入れておきます。

テーブル作成などのSQLクエリを実行する

次に作成したデーターベースにテーブルを追加していきます。

const result = await client.d1.database.query(accountId, d1Id, {
    sql:"CREATE TABLE ...",
});

const errorSqlResult = result.filter((res) => res.success === false);

複数のクエリを含んだsqlを送信しても、戻り値の配列内にあるsuccessでそれぞれのSQLごとに成功・失敗を取得できます。

Honoで書かれたコードをesbuild wasmでコンパイルする

SWCでもよかったのですが、外部パッケージを扱う関係上esbuildのほうがいろいろ楽でした。
hanabiはplaygroundとしてHonoのAPIを実行できる必要がある関係上、ブラウザ上でtypescriptをコンパイルしました。

詳しい内容は#honoconfでの登壇資料をご覧ください。
https://hono-conf.hanabi.rest/15

javascriptファイルとbindingの情報を含めてワーカーを作る

いよいよワーカー本体を作成していきます。
先ほどコンパイルしてできたjavascriptとバインドする情報をまとめて送信します。

await client.workers.scripts.update("example-worker", {
  account_id: accountId,
  "index.js": new File([javascript], "index.js", {
    type: "application/javascript+module",
  }),
  //@ts-ignore <- 型どおりだとうまくいかない❗
  metadata: new File(
    [
      JSON.stringify({
        bindings: {
            type: "d1",
            name: "DB",
            database_name: "example-db",
            id: d1Id,
        },
        main_module: "index.js",
        placement: { mode: "smart" },
      }),
    ],
    "metadata.json",
    {
      type: "application/json",
    },
  ),
});

この処理ではいくつも注意する点があります。
まずワーカーは新規作成の場合もupdateメソッドを使う必要があります。
またD1 Databaseと同じく名前は重複してはいけません。
さらにindex.jsとmetadataはFileとして送信する必要があります。この部分の情報が全然なく数時間沼にはまりました。

ワーカーのサブドメインを有効化する

やっとワーカーをデプロイできた...!!
と思ってURLにアクセスしてみてもThere is nothing here yetというメッセージが出るだけ。

ワーカーのダッシュボートで確認してみるとルートに「無効」の文字が...

ということでサブドメインを有効化する必要があります。

ただし無効になったルートを有効にするエンドポイントは公式 SDKを探してもCloudflare APIのドキュメントを探しても見つかりません。ドユコッチャ.

ということでここだけfetchリクエストを書く必要があります。

export const enableSubdomain = async ({ accountId, workerName, token }: { accountId: string; workerName: string; token: string }) => {
  const res = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${workerName}/subdomain`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      enabled: true,
    }),
  }).then((res) => res.json());
};

このようなリクエストを書くことでアクセスできるようになります!!ヤッター!!
最後に

const result = await client.workers.subdomains.get({
    account_id: accountId,
});
const subdomain = result.subdomain; // https://${workerName}.${subdomain}.workers.dev がワーカーの公開URLとなる

サブドメインを取得しワーカー名とドメインを連結させることで実際にアクセスできるURLを取得できました!!

おまけ

Worker APIの利用を検討する前、Cloudflare Workers PlaygroundでのDeployボタンに着目し調査していました。
その結果cf-workers-deployerというライブラリが完成し、これを使えばCloudflare APIトークンなどを扱わなくてもフロントエンドのみでワーカーをデプロイできるという優れモノでした。
https://www.npmjs.com/package/cf-workers-deployer
ただしHonoのコードをコンパイルしたjavascriptファイルをデプロイしようとすると重すぎて途中で処理が止まるという問題が起きお蔵入りになりました...

最後に

ぜひhanabi.restでAPIを生成してデプロイしてみてください!!
https://hanabi.rest

hanabi.rest

Discussion