mcpField
I
inngest-local

inngest-local

github.com/joelhooksクラウド・DevOps6541 回インストール2 日前に更新
リポジトリ

inngest-local について

inngest-localは、macOS上でInngestをセルフホスト可能な永続ワークフローエンジンとして運用するためのセットアップスキルです。DockerやKubernetesでのデプロイ、ワーカー設定、再試行、状態保持、運用上の注意点を扱います。

主な機能

  1. 01DockerとKubernetesのセットアップ案
  2. 02ステップ単位の永続的な再試行
  3. 03ワーカーのひな形作成と登録
  4. 04イベント連鎖とCron関数
  5. 05状態保持と再起動後の復旧

活用シーン

  1. 01ローカルでのInngest試用
  2. 02永続的なバックグラウンドワークフローの運用
  3. 03KubernetesでのInngestデプロイ
  4. 04ワーカー中断後のジョブ復旧

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/joelhooks/joelclaw --skill inngest-local

インストール後、Claude Code で/inngest-localを入力して読み込まれたか確認します。

同梱ファイルagents/openai.yamlassets/small-logo.svg

詳細

macOS で Inngest をセルフホストする

このスキルでは、Mac 上で Inngest をセルフホスト型の耐久性のあるワークフローエンジンとしてセットアップします。Inngest では、ステップごとに独立して再試行するイベント駆動型の関数を実行できます。たとえば、5 つのステップのうちステップ 3 が失敗した場合、再試行されるのはステップ 3 のみです。

はじめに

必須:

  • Docker を搭載した macOS(Docker Desktop、OrbStack、または Colima)
  • ワーカープロセス用の Bun または Node.js

任意:

  • 永続的なデプロイ用の k8s クラスター(Colima 上の Talos など)
  • 関数間の状態共有とゲートウェイ統合用の Redis

意図の確認

スコープを判断するため、ユーザーに次の質問をします。

質問 1:何を構築しますか?
  1. 手早い実験 — Inngest を試して、関数を実行し、ダッシュボードを見たい
  2. 永続的なセットアップ — 常時稼働し、再起動後も動作する、実際のワークフローを実行したい
  3. フルインフラストラクチャ — 永続ストレージを備え、エージェントゲートウェイと統合された、k8s にデプロイする Inngest が必要
質問 2:ワーカーに使用するランタイムは何ですか?
  1. Bun — 高速で、TypeScript のサポートが優れており、joelclaw が使用しているもの
  2. Node.js — 標準的で、最も幅広い互換性がある
  3. 既存のフレームワーク — すでに Next.js/Express/Hono アプリがある
質問 3:どのような処理をしますか?
  1. AI エージェントのタスク — コーディングループ、コンテンツ処理、文字起こしのパイプライン
  2. 一般的なバックグラウンドジョブ — スケジュールされたタスク、Webhook、データ処理
  3. 両方 — 混在するワークロード

セットアップの段階

署名キー(必須)

2026 年 2 月時点で、inngest/inngest:latest には署名キーが必要です。キーがない場合、コンテナーは Error: signing-key is required というエラーでクラッシュを繰り返します。

# Generate once, reuse across tiers
INNGEST_SIGNING_KEY="signkey-dev-$(openssl rand -hex 16)"
INNGEST_EVENT_KEY="evtkey-dev-$(openssl rand -hex 16)"
echo "INNGEST_SIGNING_KEY=$INNGEST_SIGNING_KEY" >> .env.inngest
echo "INNGEST_EVENT_KEY=$INNGEST_EVENT_KEY" >> .env.inngest
ティア 1:Docker のワンライナー(実験用)

30 秒で Inngest を起動します。

docker run -d --name inngest \
  -p 8288:8288 \
  -e INNGEST_SIGNING_KEY="$INNGEST_SIGNING_KEY" \
  -e INNGEST_EVENT_KEY="$INNGEST_EVENT_KEY" \
  inngest/inngest:latest \
  inngest start --host 0.0.0.0

http://localhost:8288 を開くと、Inngest ダッシュボードが表示されます。

制限事項: 状態は永続化されません。コンテナーを再起動すると履歴が失われます。実験には問題ありません。

ティア 2:永続 Docker(日常利用向け)

SQLite の状態を保存するボリュームを追加します。

docker run -d --name inngest \
  -p 8288:8288 \
  -v inngest-data:/var/lib/inngest \
  -e INNGEST_SIGNING_KEY="$INNGEST_SIGNING_KEY" \
  -e INNGEST_EVENT_KEY="$INNGEST_EVENT_KEY" \
  --restart unless-stopped \
  inngest/inngest:latest \
  inngest start --host 0.0.0.0

これで Inngest の状態はコンテナーを再起動しても保持されます。--restart unless-stopped を指定すると、Docker の再起動後に再び起動します。

ティア 3:Kubernetes(本番環境向け)

適切なヘルスチェックを備えた、完全な永続化を実現します。k8s クラスター(Colima 上の Talos など)が必要です。

# inngest.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: inngest
  namespace: default
spec:
  serviceName: inngest-svc  # NOT "inngest" — avoids env var collision
  replicas: 1
  selector:
    matchLabels:
      app: inngest
  template:
    metadata:
      labels:
        app: inngest
    spec:
      containers:
      - name: inngest
        image: inngest/inngest:latest
        command: ["inngest", "start", "--host", "0.0.0.0"]
        ports:
        - containerPort: 8288
        volumeMounts:
        - name: data
          mountPath: /var/lib/inngest
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 5Gi
---
apiVersion: v1
kind: Service
metadata:
  name: inngest-svc  # CRITICAL: not "inngest" — k8s creates INNGEST_PORT env var that conflicts
  namespace: default
spec:
  type: NodePort
  selector:
    app: inngest
  ports:
  - port: 8288
    targetPort: 8288
    nodePort: 8288

適用します。

kubectl apply -f inngest.yaml

⚠️ 注意: k8s Service に、実行するバイナリと同じ名前を付けないでください。inngest という名前の Service を作成すると、INNGEST_PORT=tcp://10.43.x.x:8288 が設定されます。Inngest バイナリは INNGEST_PORT に整数を想定しています。Service の名前には inngest-svc を使用してください。

ワーカーを構築する

ステップ 1:初期化する
mkdir my-worker && cd my-worker
bun init -y
bun add inngest @inngest/ai hono
ステップ 2:Inngest クライアントを作成する
// src/inngest.ts
import { Inngest } from "inngest";

// Type your events for full type safety
type Events = {
  "task/process": { data: { url: string; outputPath: string } };
  "task/completed": { data: { url: string; result: string } };
};

export const inngest = new Inngest({
  id: "my-worker",
  schemas: new EventSchemas().fromRecord<Events>(),
});
ステップ 3:最初の関数を書く
// src/functions/process-task.ts
import { inngest } from "../inngest";

export const processTask = inngest.createFunction(
  {
    id: "process-task",
    concurrency: { limit: 1 },  // one at a time
    retries: 3,
  },
  { event: "task/process" },
  async ({ event, step }) => {
    // Step 1: Download — retries independently on failure
    const localPath = await step.run("download", async () => {
      const response = await fetch(event.data.url);
      const buffer = await response.arrayBuffer();
      const path = `/tmp/downloads/${crypto.randomUUID()}.bin`;
      await Bun.write(path, buffer);
      return path;  // Only the path is stored in step state (claim-check pattern)
    });

    // Step 2: Process — if this fails, download doesn't re-run
    const result = await step.run("process", async () => {
      const data = await Bun.file(localPath).text();
      // ... your processing logic
      return { processed: true, size: data.length };
    });

    // Step 3: Emit completion event — chains to other functions
    await step.sendEvent("notify-complete", {
      name: "task/completed",
      data: { url: event.data.url, result: JSON.stringify(result) },
    });

    return { status: "done", result };
  }
);
ステップ 4:関数を提供する
// src/serve.ts
import { Hono } from "hono";
import { serve as inngestServe } from "inngest/hono";
import { inngest } from "./inngest";
import { processTask } from "./functions/process-task";

const app = new Hono();

// Health check
app.get("/", (c) => c.json({ status: "running", functions: 1 }));

// Inngest endpoint — registers functions with the server
app.on(
  ["GET", "POST", "PUT"],
  "/api/inngest",
  inngestServe({ client: inngest, functions: [processTask] })
);

export default {
  port: 3111,
  fetch: app.fetch,
};
ステップ 5:実行する
INNGEST_DEV=1 bun run src/serve.ts

ワーカーが起動して localhost:8288 の Inngest に登録され、関数がダッシュボードに表示されます。

ステップ 6:テストする

ダッシュボード(Events → Send Event)からイベントを送信するか、curl を使います。

curl -X POST http://localhost:8288/e/key \
  -H "Content-Type: application/json" \
  -d '{"name": "task/process", "data": {"url": "https://example.com/file.txt", "outputPath": "/tmp/out"}}'

ダッシュボードでステップごとの実行を確認します。

パターン

イベントチェーン

関数 A がイベントを発行し、それをトリガーとして関数 B を実行します。

// In function A:
await step.sendEvent("chain", { name: "pipeline/step-two", data: { result } });

// Function B triggers on that event:
export const stepTwo = inngest.createFunction(
  { id: "step-two" },
  { event: "pipeline/step-two" },
  async ({ event, step }) => { /* ... */ }
);
並行処理キー

プロジェクトごとにインスタンスを 1 つ実行し、プロジェクト間では並列実行を許可します。

concurrency: {
  key: "event.data.project",
  limit: 1,
}
Cron 関数
export const heartbeat = inngest.createFunction(
  { id: "heartbeat" },
  [{ cron: "*/15 * * * *" }],
  async ({ step }) => {
    await step.run("check-health", async () => {
      // ... system health checks
    });
  }
);
クレームチェックパターン

ステップ間で大きなデータを渡す場合は、ファイルに書き込み、そのパスを渡します。

// ❌ DON'T: return large data from a step
const transcript = await step.run("transcribe", async () => {
  return { text: hugeString }; // Step output has size limits!
});

// ✅ DO: write to file, return path
const transcriptPath = await step.run("transcribe", async () => {
  const result = await transcribe(audioPath);
  await Bun.write("/tmp/transcript.json", JSON.stringify(result));
  return "/tmp/transcript.json";
});

再起動後も稼働させる

launchd 経由でワーカーを起動する

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>com.you.inngest-worker</string>
  <key>ProgramArguments</key>
  <array>
    <string>/Users/you/.bun/bin/bun</string>
    <string>run</string>
    <string>/path/to/your/worker/src/serve.ts</string>
  </array>
  <key>EnvironmentVariables</key>
  <dict>
    <key>INNGEST_DEV</key><string>1</string>
    <key>HOME</key><string>/Users/you</string>
    <key>PATH</key><string>/usr/local/bin:/usr/bin:/bin:/Users/you/.bun/bin</string>
  </dict>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>StandardOutPath</key><string>/tmp/inngest-worker.log</string>
  <key>StandardErrorPath</key><string>/tmp/inngest-worker.log</string>
  <key>WorkingDirectory</key><string>/path/to/your/worker</string>
</dict>
</plist>

読み込みます。

launchctl load ~/Library/LaunchAgents/com.you.inngest-worker.plist
再起動時の動作
  1. Docker が起動 → 永続化された状態(SQLite)を使って Inngest サーバーが起動
  2. launchd が起動 → ワーカープロセスが関数を登録
  3. 未完了の関数実行は、最後に完了したステップから再開

注意点

  1. @inngest/ai は必須のピア依存関係です。 bun add inngest だけでは不十分です。SDK は起動時に @inngest/ai をインポートします。Cannot find module '@inngest/ai' が発生してワーカーがクラッシュします。必ず両方をインストールしてください。

  2. Docker からホストへのネットワーク接続。 Inngest が Docker で実行され、ワーカーがホスト上で実行されている場合、サーバーは localhost:3111 に接続できません。docker run コマンドに --sdk-url http://host.docker.internal:3111/api/inngest を指定してください。これは Docker Desktop/OrbStack 固有の設定です。Linux Docker では --add-host=host.docker.internal:host-gateway が必要です。

  3. k8s でのサービス名: Service にバイナリと同じ名前を付けないでください。INNGEST_PORT 環境変数の衝突により、コンテナがクラッシュします。

  4. ステップ出力のサイズ: ステップの戻り値は小さく保ってください。大きなデータにはクレームチェックパターンを使用します。

  5. ワーカーの再登録: Inngest サーバーの再起動後、ワーカーは再登録が必要です。ワーカーを再起動するか、登録エンドポイントにアクセスしてください。

  6. トリガーのずれ: 関数は起動時にトリガーを登録します。コードでトリガーを変更してもサーバーの状態が古いままだと、古いトリガーが有効なままになります。監査ツールを作成するか、サーバーとワーカーの両方を再起動してください。

  7. INNGEST_DEV=1: ローカル開発に必須です。指定しないと、ワーカーは Inngest Cloud への登録を試みます。

  8. GPU 処理では同時実行数を 1 に設定: 文字起こし、推論など、GPU を飽和させる処理には concurrency: { limit: 1 } が必要です。

検証

  • Inngest ダッシュボードに http://localhost:8288 からアクセスできる
  • ダッシュボードの Functions タブで、ワーカーが登録済みとして表示される
  • テストイベントを送信し、ダッシュボードで関数が実行される
  • 関数の実行中にワーカーを停止し、ワーカーを再起動すると、関数が最後のステップから再開する
  • (Tier 2 以上)Docker を再起動しても、Inngest の状態が保持される
  • (launchd)Mac を再起動すると、ワーカーと Inngest の両方が自動的に復帰する

セットアップスクリプト(curl 優先)

セットアップを自動化するには、次を実行します:

curl -sL joelclaw.com/scripts/inngest-setup.sh | bash

または、特定のティアを指定します:

curl -sL joelclaw.com/scripts/inngest-setup.sh | bash -s -- 2

このスクリプトは冪等性があり、既存の状態を検出して、型付きイベントを備えたワーカーのひな型を作成します。

意思決定の連鎖(簡略版 ADR)

このスキルのアーキテクチャは、一連の Architecture Decision Records に基づいています。トレードオフの背景が必要な場合は、適宜参照してください。

ADR-0010 → ADR-0029 → 現在の状態

決定事項選択肢主なトレードオフリンク
ワークフローエンジンInngest(セルフホスト)ステップ単位の耐障害性と複雑さのトレードオフです。Cron とスクリプトでは、ステップ単位の再試行ができません。ADR-0010
コンテナランタイムColima(VZ framework)Docker Desktop を置き換えます。無料で、ヘッドレスで、必要な RAM も少なくなります。ADR-0029
3 つのコンテナに k8s を使用はい(Colima 上の Talos)調整ループと将来のマルチノード化のために 380MB のオーバーヘッドが発生します。Docker Compose では自己修復できません。joel-deploys-k8s
サービス名inngest ではなく inngest-svck8s が INNGEST_PORT 環境変数を注入します。バイナリは整数を想定していますが、URL が渡されます。苦労して得たデバッグ知見
ワーカーのランタイムBun + HonoNode よりコールドスタートが高速です。Hono は最小限の HTTP 機能を提供します。永続化には launchd KeepAlive を使用します。実用上の選択
ステップデータのパターンクレームチェック(ファイルパス)ステップ出力にはサイズ制限があります。大きなデータはディスクに書き込み、ステップ間ではパスを渡します。Inngest のドキュメント
トリガーの監査ハートビート Cron 監査ツールトリガーのずれに気付かず、promote 関数が数日間動作しませんでした。現在は 15 分ごとに監査しています。ADR-0037

クレジット

コメント