【祝1.0🥟】Bunランタむム䞊で動䜜するHTTP APIをApp Runnerぞホスティングしおみた

【祝1.0🥟】Bunランタむム䞊で動䜜するHTTP APIをApp Runnerぞホスティングしおみた

2023.09.13

この蚘事は公開されおから1幎以䞊経過しおいたす。情報が叀い可胜性がありたすので、ご泚意ください。

はじめに

9月8日珟地時間にJavaScriptランタむムのBunのバヌゞョン1.0がリリヌスされたした🥳🥟 本蚘事では、Bunランタむムを生かしたHTTP APIを䜜っおApp Runnerにホスティングしおみたす。

Bunずは

JavaScriptランタむムは珟圚 Node.js Deno Bunず耇数ある状況で、それぞれ異なる芁玠技術で䜜られおいたす。詳しくはBun first impressions techfeedを参考にするのが分かりやすいです。よく挙げられる点ずしお、ネむティブ実装にZigの採甚、JITコンパむラずしおJavaScriptCoreを採甚しおいる点です。1.0の玹介動画であるBun 1.0 is hereでは速床の話がよく出おきおおり、匷く抌し出しおいる印象を受けたした。

蚭蚈ゎヌルに関しおは、bun.sh | Design Goalsが参考になりたす。

構成

構成は以䞋の通りです。Bunのバヌゞョンは1.0.1を利甚したす。この肉たんのアむコンかわいいですね。

本蚘事の䞻旚ずしおなるべくBunを掻かす構成を考えたした。結果、Bunの組み蟌み関数で特城的なbun:sqliteを掻甚しおTODOのHTTP APIを䜜成しおみたす。構成の詳现は以䞋の通りです。

芁玠 採甚 理由
ホスティング先 App Runner Lambdaのカスタムランタむムがありたすが、ネむティブサポヌトされおいないため珟時点では珟実採甚されにくいため
デヌタベヌス SQLite3 BunにはSQLite3ドラむバがネむティブ(bun:sqlite)で実装されおいるため
冗長化構成 Litestream SQLite3ずS3をレプリケヌトするため
HTTP Webフレヌムワヌク Hono Bunをサポヌトしおいるため

゜ヌスコヌド

゜ヌスコヌドはGitHubにありたす。bunコマンドで党お完結したす。(コヌドフォヌマットだけ手軜に行きたいのでdeno fmtを䜿っおいたす。。)

github.com/shuntaka9576/bun-apprunner-template

Quick Start

以䞋のコマンドで、環境構築ホスティング党お完了したす。本プロゞェクトでは、Dockerコンテナクラむアントずしおfinchの利甚を掚奚したす。

cd ./packages/aws
export CDK_DOCKER=$(which finch)
bunx cdk deploy bun-apprunner-app

凊理ずしおは、以䞋の通りです。CDKが党郚やっおくれたす。

  1. ロヌカルでコンテナビルド
  2. コンテナをECRぞpush
  3. App RunnerやS3のリ゜ヌスを䜜成
  4. コンテナのデプロむ

ロヌカルでコンテナをビルドしたす。ロヌカルのDocker環境が敎備されおいるこずを確認しおください。App Runnerは、x86である必芁があるためarmだず動䜜したせん。今回コンテナではバむナリ含め党おx86でビルドされたものを利甚しおいるので互換性はありたせん。

CDK実行結果

✹  Synthesis time: 1.65s

This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

(äž­ç•¥)

IAM Statement Changes
bun-apprunner-app.BunApprunnerHostingConstructAppRunnerUriAEE3023C = https://xxxxxxxx.ap-northeast-1.awsapprunner.com <- このURLを利甚

API利甚䟋

# GET /tasks
$ curl -v https://xxxxxxxx.ap-northeast-1.awsapprunner.com/tasks
(äž­ç•¥)
{"tasks":[]}
# POST /tasks
$ curl -v -X POST https://xxxxxxxx.ap-northeast-1.awsapprunner.com/tasks -d '{"title": "foo"}'
(äž­ç•¥)
{"taskId":"71f21d95-b4f6-4327-9134-452ed346184a","title":"foo","createAt":"2023/09/13 17:09:13"}
# GET /tasks/:taskId
$ curl -v https://xxxxxxxx.ap-northeast-1.awsapprunner.com/tasks/71f21d95-b4f6-4327-9134-452ed346184a
(äž­ç•¥)
{"taskId":"71f21d95-b4f6-4327-9134-452ed346184a","title":"foo","createAt":"2023/09/13 17:09:13"}

解説

䞀郚しか玹介しないため、気になる点がありたしたら実際の゜ヌスコヌドを参照しおください。

bunコマンド

bunコマンドは、npm yarn pnpmに圓たるコマンドです。特に埌述する䟝存のむンストヌルはかなり高速です。たたworkspaceに察応しおいたす。ただ、-wオプションは珟時点ではないようなので、特定のパスに行っおパッケヌゞ远加が必芁です。よく䜿うコマンドを玹介したす。

䟝存のむンストヌル

--froze-lockfileや--productionオプションもありたすが、コンテナ内郚だず矛盟が出おしたいうたく動䜜したせんでした。故に埌述のDockerfileではオプションを指定しおいたせん。

bun install

スクリプト実行

--hotでホットリロヌドが可胜です。HTTP API実装では重宝したす。--bunオプションは、bunランタむムに動䜜を匷制させたす。既存のNext.jsやNestJSアプリが起動するのは、このオプションを蚭定しおいないためで、裏偎でnodeプロセスが起動しおいたす。

bun run --hot --bun ./src/main.ts

bunxはnpxのようなコマンド、cdkやeslintなどのCLIを利甚する際に利甚したす。

bunx cdk deploy bun-apprunner-app

コンテナむメヌゞ

マルチステヌゞビルドで、litestreamずbunを取埗しお、アプリ偎のコンテナぞ移動しおいたす。むメヌゞサむズは玄90MBでした。

bunは*-baselineのバむナリを利甚しおいたす。baselineなしのバむナリだず、コンテナでbunコマンド起動時にクラッシュするこずがあったためです。"Illegal instruction (core dumped)" when using bun through code-serverが起因しおいそうです。MacOSでビルドしたので、Linuxだずたた結果が倉わるかもしれたせん。

FROM debian:stable-slim as get

WORKDIR /bun

RUN apt-get update
RUN apt-get install curl unzip -y
RUN curl --fail --location --progress-bar --output "/bun/bun.zip" "https://github.com/oven-sh/bun/releases/download/bun-v1.0.1/bun-linux-x64-baseline.zip"
RUN unzip -d /bun -q -o "/bun/bun.zip"
RUN mv /bun/bun-linux-x64-baseline/bun /usr/local/bin/bun
RUN chmod 777 /usr/local/bin/bun

COPY package.json bun.lockb /bun
COPY packages/app /bun/packages/app
RUN bun install

ADD https://github.com/benbjohnson/litestream/releases/download/v0.3.11/litestream-v0.3.11-linux-amd64.tar.gz /tmp/litestream.tar.gz
RUN tar -C /usr/local/bin -xzf /tmp/litestream.tar.gz

FROM debian:stable-slim

WORKDIR /work

COPY --from=get /usr/local/bin/bun /bin/bun
COPY --from=get /usr/local/bin/litestream /bin/litestream
COPY --from=get /bun/node_modules /work/node_modules
COPY --from=get /bun/packages /work/packages

RUN apt-get update && \
  apt-get install -y \
    sqlite3 \
    ca-certificates && \
  rm -rf /var/lib/apt/lists/*

RUN mv /work/packages/app/litestream.yml /etc
RUN chmod +x /work/packages/app/entrypoint.sh

WORKDIR /work/packages/app

ENTRYPOINT ["./entrypoint.sh"]

SQLite3操䜜(bun:sqlite)

bun:sqliteを䜿った読み蟌み/曞き蟌み凊理は以䞋の通りです。型の安党性はないですが、CURD凊理を曞くのに十分な機胜がある印象でした。トランザクション凊理もありたす。詳しくはSQLite – API | Bun Docsが参考になりたす。型チェックをしっかり行いたい堎合は、zodを利甚するず良いず思いたす。

import { Task } from "src/entity/task";
import { Database } from "bun:sqlite";
import { v4 as uuid } from "uuid";
import { DateTime } from "luxon";

const db = new Database("./todo.db");

const insertTask = db.prepare(
  "INSERT INTO task (task_id, title) VALUES ($taskId, $title);",
);
const queryTask = db.query(
  "SELECT task_id, title, create_at FROM task WHERE task_id = $taskId;",
);
const queryTaskAll = db.query("SELECT task_id, title, create_at FROM task;");

// 曞き蟌み
export const AddTask = async (title: string): Promise<Task> => {
  const taskId = uuid();
  await insertTask.run({
    $taskId: taskId,
    $title: title,
  });

  const record = queryTask.get({
    $taskId: taskId,
  }) as
    | {
      task_id: string;
      title: string;
      create_at: string;
    }
    | undefined;

  if (record == null) {
    throw new Error("unexpect insert exception");
  }

  return {
    taskId: record.task_id,
    title: record.title,
    createAt: DateTime.fromSQL(record.create_at, { zone: "UTC" }),
  };
};

// 取り出し(単䜓)
export const GetTask = async (taskId: string): Promise<Task | null> => {
  const record = queryTask.get({
    $taskId: taskId,
  }) as
    | {
      task_id: string;
      title: string;
      create_at: string;
    }
    | undefined;

  if (record == null) {
    return null;
  }

  return {
    taskId: record.task_id,
    title: record.title,
    createAt: DateTime.fromSQL(record.create_at, { zone: "UTC" }),
  };
};

// 取り出し(耇数)
export const ListTask = async (): Promise<Task[]> => {
  const records = queryTaskAll.all() as {
    task_id: string;
    title: string;
    create_at: string;
  }[];

  return records.map((record) => ({
    taskId: record.task_id,
    title: record.title,
    createAt: DateTime.fromSQL(record.create_at, { zone: "UTC" }),
  }));
};

最埌に

本アプリ実装ではluxonやuuidずいったモゞュヌルを利甚したしたが、問題なく動䜜しおおり、特にハマるこずなく既存のnpm資産を生かし぀぀アプリが䜜れたした。たたbun bunxがキビキビ動䜜しお気持ちよく、今回のようなHTTP APIサヌバヌの実装はホットリロヌドず盞性がよく開発䜓隓がよかったです。ホットリロヌド自䜓は、Node.jsでも蚭定を入れたり、NestJSはCLI偎で察応しおいたりはしたすが、ネむティブサポヌトされおいるず小さく切り出しおトラブルシュヌトなんかも手軜でいいですね。bun installは高速でnpm互換があるため、CIのみbunを䜿う事䟋もあるそうです。今回詊せたせんでしたが、組み蟌みのテストツヌルも気になっおいたす。

珟時点で問題なく䜿えおいたすが、より他のランタむムず差別化芁玠や安定性が出おくるず流行りそうな気がしおいたす。

匕き続きりォッチしおいきたいず思いたす

この蚘事をシェアする

FacebookHatena blogX

関連蚘事