🫠

Next.js + Vercelでバックグラウンド処理したくてInngest使ってみたら便利だった話

2023/03/07に公開

こんにちは。
Next.jsの記事を書いていて、Rustの記事を1年以上ドラフトにしたままだったことに気付いて、あららら、と思っているmasamikiです。

自社のポータルをNext.jsで作っており、Vercelにホスティングして運用していているのですが、SlackのSlash Commandを受けるようにしたく、APIを用意していて気が付きました。

SlackのSlash Commandのtimeoutはえぇ。

ちょい重たい処理、というかChatGPTのAPIを実行させているのですが、あぁ、これはバックグラウンドで処理させるかPubSub的な用意してやらないと厳しいなと。

とりあえず、レスポンスを先に返して非同期に処理さとけばええんでは?と、こんな感じで書いてみたら、

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  const api = new ChatGPTAPI({
    apiKey: process.env.OPENAI_API_KEY as string,
  });
  api.sendMessage(event.data.text as string).then((gptRes: ChatMessage) => {
    axios.post(event.data.response_url, {
      response_type: 'in_channel',
      text: gptRes.text,
    }).then(successCallback).catch(failedCallback);
  }).catch(failedCallback);

  res.status(200).json({
    response_type: 'in_channel',
    text: 'ちょっと待ってね。',
  });
}

ローカルでは、うまく行きました、が、、、、
Vercelにdeployしてみるとresponseを返した直後に殺されるもよう。
Functionってそんな感じよなぁと、断念。

追記
ご情報いただいたので追記。
waitUntil()で待ってくれるとのこと。
こっちのが楽でしたね、お恥ずかしい🔰
https://twitter.com/jrsyo/status/1633513329678491648?s=61&t=PEuNNtSv2M9g9V-uYMY0SA

さらに追記
この箇所でのコメントをいくつかいただいのですが、こういう理由です。
https://twitter.com/jrsyo/status/1633705553833918464?s=61&t=AVlbga4CWxtco8VzQHA1aQ

Shohei Maedaさん、おりばーさん、リプありがとうございました。

Inngest

Inngestは、Ship Background Jobs, Crons, Webhooks, and Reliable Workflows in record timeと書かれていますが、こういったServerlessな構成のためにバックグラウンドでの処理やCronを可能にしてくれるサービスだそうです。

VercelのマーケットプレイスにもVercelと連携できるよう、

というような感じで置いてあります。

準備

フロントで扱うために、まずInngestのサービスから二つの値を取得しておく必要があります。

  1. シークレットキー
    サインアップしたら、Secretsページを見に行きましょう。置いてます。

  2. イベントキー
    Sourcesページにいって、Create Event Keyボタンを押すと、イベントが作成されます。

    あとは、キーが見えてるのでコピーするだけです。(イベント名は適当に編集)。

実装

Next.js前提での実装方法です。

パッケージの追加

yarn add inngestyarn add --dev inngest-cliを実行して、パッケージを入れておきます。
inngest-cliというのが、ローカルで動かす時に、ダミーザーバーを用意してくれるやつです。

InngestのFunctionの追加

まず、バックグラウンドでやりたい処理を書きます。

公式のドキュメントではルート配下に直接inngestディレクトリを作成して、その中にファイルを置いてく感じでしたが、srcにまとめたい感があったので、僕は./src/inngestというディレクトリを作成して、そこにFunction毎にディレクトリを切って入れてくことにしました。

src/inngest/functions/chatgpt/index.ts
import { ClientOptions, Inngest } from 'inngest';
import { ChatGPTAPI } from 'chatgpt';
import axios from 'axios';

const chatgptInngestOptions: ClientOptions = {
  name: 'chatgpt',
  eventKey: process.env.INNGEST_EVENT_KEY as string,
};
const inngest = new Inngest(chatgptInngestOptions);
const chatgpt = inngest.createFunction(
  { name: 'ASK A QUESTION TO CHATGPT' }, // 好きな名前つけましょう。
  { event: 'app/chatgpt.ask' },  // 任意のイベント名をつけます。呼び出す時に使います。
  async ({ event }) => {
    const api = new ChatGPTAPI({
      apiKey: process.env.OPENAI_API_KEY as string,
    });
    const gptRes = await api.sendMessage(event.data.text as string);
    await axios.post(event.data.response_url, {
      response_type: 'in_channel',
      text: gptRes.text,
    });

    return gptRes.text;
  }
);
export { chatgptInngestOptions };
export default chatgpt;

INNGEST_EVENT_KEYが、先ほど作成したイベントキーが入るとこですね。
localで動かす時は、INNGEST_EVENT_KEY=localにしておきます。

サーバーを追加

FunctionのサーバーをAPIのディレクトリ配下に追加します。
serverの引き数に作ったFunctionを入れてあげることで提供されるようになります。

src/pages/api/inngest/index.ts
import { serve } from 'inngest/next';
import { Inngest, RegisterOptions } from 'inngest';
import chatgpt, { chatgptInngestOptions } from '../../../inngest/chatgpt';

const inngest = new Inngest(chatgptInngestOptions);
const options: RegisterOptions = {};
if (process.env.NODE_ENV !== 'development') {
  options.signingKey = process.env.INNGEST_SIGNING_KEY;
}
export default serve(inngest, [chatgpt], options);

INNGEST_SIGNING_KEYが、先ほど取ってきたシークレットキーが入るとこですね。
localでは不要なので、conditionを作ってoptionsに入れないようにしてます。

Functionを叩くAPIの追加

Cronを作るも良しですが、今回はAPIが叩かれた際に動かしたいので、そのAPIを作ります。
すでに作ったInngestの初期化のための引き数は使い回したくimport。

src/pages/api/chatgpt/index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Inngest } from 'inngest';
import { chatgptInngestOptions } from '~/inngest/chatgpt';

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  const inngest = new Inngest(chatgptInngestOptions);
  await inngest.send({
    name: 'app/chatgpt.ask', // Functionにつけたイベント名を入れます。
    data: {
      api_app_id: req.body.api_app_id,
      response_url: req.body.response_url,
      text: req.body.text,
    }, // Functionの event に入るデータです。
  });

  res.status(200).json({
    response_type: 'in_channel',
    text: 'ちょっと待ってね。',
  });
};
export default handler;

ローカルで実行

ローカルで実行するときは、inngestのダミーサーバーをinngest-cliで立ち上げておく必要があるので、

yarn inngest-cli dev

で立ち上げます。
で、

yarn dev

Next.jsを立ち上げます。(書かなくてもいいか)

あとは、APIを叩いてみてください。

Vercelで実行

VercelにNext.jsのアプリがデプロイされていることが前提です。
マーケットプレイスでInngestを探して、
https://vercel.com/integrations/inngest

自分のプロジェクトにIntegrateしましょう!

そして、シークレットキーやらイベントキーやらをVercelの環境変数に設定しましょう。

あとは、APIを叩いてみてください。

感想

Vercelをはじめて触ったときに、こんなにも簡単にアプリケーションがリリースできる時代になったのか、と思いましたが、バックグラウンドで実行したい処理やCronまでこれだけでできてしまうと、もう、、、なんだかなぁ、という気分になりました。

PMFや社内用のサービスにはこういうの使ってガンガン作っていけるなぁ。
良い時代になりましたなぁ。

Discussion