🙆

upstashでRate Limitを簡単導入

2023/07/31に公開

はじめに

  • upstash で Rate Limit を簡単に導入する方法を紹介します。
  • upstash のアカウント作成、Next.js への Rate Limit の導入を手順毎に解説します。
  • 以下に、今回のサンプルコードを公開しています。

https://github.com/hayato94087/nextjs-upstash-rate-limit-sample

なぜ導入するのか

  • Rate Limit を導入することで、条件を満たしている場合のみ処理を実行でき、サーバサイドで実行される利用回数を制限できます。(例えば、10 秒間に 10 回までのリクエストを許可するなど)
  • 逆に処理回数を制限させない場合、悪意を持ったユーザーによってサーバサイドの処理を大量に実行されてしまいます。その結果、サーバサイドで外部 API を呼び出している場合、利用料が高額になってしまう可能性があります。
  • Rate Limit で制限をかける方法としては、例として以下があります。
    • API ごとの Rate Limit を設ける
    • API とユーザーの IP アドレスごとに Rate Limit を設ける
    • API とユーザアカウントごとに Rate Limit を設ける

upstashとは

  • upstash は Redis をクラウド上で提供しているサービスです。
  • Region は日本リージョンもあり、日本からのアクセスにも高速に応答します。
  • Redis とは、インメモリデータベースの一種で、高速にデータを読み書きできます。
  • Redis を利用することで、Rate Limit を簡単に導入できます。

https://upstash.com/

利用料金についてはこちらを参照ください。サービス立ち上げ時期であれば、無料でも十分使えるようになっています。

https://docs.upstash.com/redis/overall/pricing

upstashでRate Limitを導入

本記事では Next.js で upstash を利用し Rate Limit を導入する方法を紹介します。

以下の機能を実装します。

  • ページにボタンを配置します。
  • ボタンをクリックするとサーバサイドで処理が実行されます。
  • サーバサイドは Rate Limit を導入し、10 秒間に 10 回までのリクエストを許可します。
  • Rate Limit は upstash を利用して実現します。

upstashのアカウントを作成

  1. upstashのサイトにアクセスします。

https://upstash.com/

  1. 「Login」をクリックします。

  1. ソーシャルログインが使えるので、好きな方法でログインします。ここでは、GitHub でログインします。

  1. GitHub から権限付与の確認が表示されるので、「Authorize UpstashBot」をクリックします。

  1. アカウント作成が完了しました。すごく簡単です。

データベースを作成

  1. ダッシュボードから「Create database」をクリックします。

  1. 必要箇所を入力します。

項目 説明
Name 任意のデータベースの名前を入力します。ここでは「nextjs-upstash-rate-limit-sample」と入力します。
Type, Region データベースが実行される場所を指定します。パフォーマンスを最適化したい場合は、「Type」は「Regional」を選択し、アプリケーションが実行される最も近い場所を「Region」から選択します。今回は、日本リージョンで実行したいので、「Type」は「Regional」を選択し、「Region」は「Japan」を選択します。
TTS(SSL)Enabled 有効化するとアクセス元とデータベースの間の通信が暗号化されます。セキュリティを高めたい場合は有効にします。今回は有効にします。
Eviction 有効化するとデータベースの容量が上限に達した場合、古いデータを自動的に削除します。upstash以外のデータベースでデータがキャッシュされていたり、データを削除しても問題ない場合に有効化します。今回は有効にしません。

https://docs.upstash.com/redis/features/security

https://docs.upstash.com/redis/features/eviction

  1. データベースが作成されました。

  1. データベースはダッシュボードからも確認できます。

それでは、以降は、実際に Next.js から upstash を利用して Rate Limit を導入していきます。

Next.jsプロジェクトの新規作成

作業するプロジェクトを新規に作成していきます。

長いので、折りたたんでおきます。

新規プロジェクト作成と初期環境構築の手順詳細
$ pnpm create next-app@latest nextjs-upstash-rate-limit-sample --typescript --eslint --import-alias "@/*" --src-dir --use-pnpm --tailwind --app
$ cd nextjs-upstash-rate-limit-sample

以下の通り不要な設定を削除し、プロジェクトの初期環境を構築します。

$ mkdir src/styles
$ mv src/app/globals.css src/styles/globals.css
src/styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
src/app/page.tsx
export default function Home() {
  return (
    <main className="text-lg">
      テストページ
    </main>
  )
}
src/app/layout.tsx
import '@/styles/globals.css'

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body className="">{children}</body>
    </html>
  );
}
tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  plugins: [],
};
tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
+   "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

コミットします。

$ pnpm build
$ git add .
$ git commit -m "新規にプロジェクトを作成し, 作業環境を構築"

React Server Components と Server Actionsで処理を実装

Server Actions を利用して、サーバサイドの処理を実装します。

Server Actions を利用するには、next.config.jsexperimental.serverActions を追加します。Server Actions を利用するには、App Router を利用する必要があります。`next.config.js`` を修正します。

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
+  experimental: {
+    serverActions: true,
+  },
};

module.exports = nextConfig;

Server Actions で利用する関数を作成します。実行されると、現在時刻をログに出力します。

$ mkdir src/server-actions
$ touch src/server-actions/action.ts
src/server-actions/action.ts
"use server";

export async function process() {
  // get time in format yyyy/mm/dd hh:mm:ss in one line
  const date = new Date();
  const time = `${date.getFullYear()}/${
    date.getMonth() + 1
  }/${date.getDate()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;

  console.log({ time: time });
}

単純なボタンコンポーネントを作成します。

  • ボタンをクリックしサーバサイドの処理を実行します。
  • Server Actions の useTransition を利用することで、サーバ側で処理中かステータスを判断できる isPending が利用できます。
  • 処理中の場合は、「送信中」と表示させます。
$ mkdir src/components
$ touch src/components/button.tsx
src/components/button.tsx
"use client";

import { FC, useTransition } from "react";
import { process } from "@/server-actions/action";

interface ButtonProps {}

const Button: FC<ButtonProps> = (props) => {
  let [isPending, startTransition] = useTransition();
  return (
    <>
      {isPending ? (
        <button
          onClick={() => startTransition(() => process())}
          className="border-2 py-2 px-3 bg-slate-400 text-white rounded-lg w-[250px]"
          disabled={true}
        >
          処理中
        </button>
      ) : (
        <button
          onClick={() => startTransition(() => process())}
          className="border-2 py-2 px-3 bg-slate-800 text-white rounded-lg w-[250px]"
        >
          送信
        </button>
      )}
    </>
  );
};

export default Button;

page.tsx にボタンを配置します。

src/app/page.tsx
import Button from "@/components/button";

export default function Home() {
  return (
    <main className="text-lg">
      <Button/>
    </main>
  )
}

コミットします。

$ pnpm build
$ git add .
$ git commit -m "ログ出力まで処理を実装"

upstashのアクセスキーを設定

.env.local に upstash のアクセスキーを設定します。アクセスキーは、管理コンソールから取得できます。

$ touch .env.local
.env.local
UPSTASH_REDIS_REST_URL="https://xxxx-xxxxx-xxxx-xxxxx.upstash.io"
UPSTASH_REDIS_REST_TOKEN="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

Rate Limit を導入

upstash で Rate Limit を導入するために、以下のパッケージを追加します。

$ pnpm add @upstash/redis
$ pnpm add @upstash/ratelimit

Rate Limit として、10 秒間に 10 回までのリクエストを許可するようにします。

src/server-actions/action.ts
"use server";

+import { Ratelimit } from "@upstash/ratelimit"; // for deno: see above
+import { Redis } from "@upstash/redis";

+// Create a new ratelimiter, that allows 10 requests per 10 seconds
+const ratelimit = new Ratelimit({
+  redis: Redis.fromEnv(),
+  limiter: Ratelimit.slidingWindow(10, "10 s"),
+  analytics: true,
+  /**
+   * Optional prefix for the keys used in redis. This is useful if you want to share a redis
+   * instance with other applications and want to avoid key collisions. The default prefix is
+   * "@upstash/ratelimit"
+   */
+  prefix: "@upstash/ratelimit",
+});

+// Use a constant string to limit all requests with a single ratelimit
+// Or use a userID, apiKey or ip address for individual limits.
+const identifier = "api";

export async function printMessage() {
  // get time in format yyyy/mm/dd hh:mm:ss in one line
  const date = new Date();
  const time = `${date.getFullYear()}/${
    date.getMonth() + 1
  }/${date.getDate()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;

-  console.log({ time: time });
+  const { success } = await ratelimit.limit(identifier);
+
+  if (!success) {
+    console.log({ time: time, status: "ratelimit exceeded" });
+  } else {
+    console.log({ time: time, status: "success" });
+  }
}

@upstash/ratelimit の詳細はこちらを確認ください。

https://github.com/upstash/ratelimit

動作確認

ローカルサーバで動作確認をします。

$ pnpm dev

ボタンをクリックすると、サーバサイドで処理が実行され、ログが出力されます。成功している場合は、status: success が表示されます。Rate Limit を超えている場合は、status: ratelimit exceeded が表示されます。

ボタンを連打した場合のログを以下に記載します。安定して、10 秒間に 10 回までのリクエストを許可するようになっています。

{ time: '2023/7/31 7:42:11', status: 'success' }
{ time: '2023/7/31 7:42:12', status: 'success' }
{ time: '2023/7/31 7:42:13', status: 'success' }
{ time: '2023/7/31 7:42:13', status: 'success' }
{ time: '2023/7/31 7:42:14', status: 'success' }
{ time: '2023/7/31 7:42:14', status: 'success' }
{ time: '2023/7/31 7:42:14', status: 'success' }
{ time: '2023/7/31 7:42:14', status: 'success' }
{ time: '2023/7/31 7:42:14', status: 'success' }
{ time: '2023/7/31 7:42:15', status: 'success' }
{ time: '2023/7/31 7:42:15', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:15', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:15', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:15', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:16', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:16', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:16', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:16', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:16', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:17', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:17', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:17', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:17', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:17', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:18', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:18', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:18', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:19', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:19', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:19', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:20', status: 'success' }
{ time: '2023/7/31 7:42:20', status: 'success' }
{ time: '2023/7/31 7:42:20', status: 'success' }
{ time: '2023/7/31 7:42:21', status: 'success' }
{ time: '2023/7/31 7:42:21', status: 'success' }
{ time: '2023/7/31 7:42:21', status: 'success' }
{ time: '2023/7/31 7:42:21', status: 'success' }
{ time: '2023/7/31 7:42:21', status: 'success' }
{ time: '2023/7/31 7:42:22', status: 'success' }
{ time: '2023/7/31 7:42:22', status: 'success' }
{ time: '2023/7/31 7:42:22', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:22', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:22', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:23', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:23', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:23', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:24', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:24', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:25', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:25', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:25', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:26', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:26', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:26', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:27', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:27', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:27', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:28', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:28', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:29', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:29', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:29', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:30', status: 'success' }
{ time: '2023/7/31 7:42:30', status: 'success' }
{ time: '2023/7/31 7:42:30', status: 'success' }
{ time: '2023/7/31 7:42:31', status: 'success' }
{ time: '2023/7/31 7:42:31', status: 'success' }
{ time: '2023/7/31 7:42:31', status: 'success' }
{ time: '2023/7/31 7:42:31', status: 'success' }
{ time: '2023/7/31 7:42:31', status: 'success' }
{ time: '2023/7/31 7:42:31', status: 'success' }
{ time: '2023/7/31 7:42:32', status: 'success' }
{ time: '2023/7/31 7:42:32', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:32', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:32', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:32', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:33', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:33', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:33', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:33', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:33', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:34', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:34', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:34', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:34', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:34', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:35', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:35', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:35', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:36', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:36', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:36', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:37', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:37', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:38', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:38', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:38', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:39', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:39', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:39', status: 'ratelimit exceeded' }
{ time: '2023/7/31 7:42:40', status: 'success' }
{ time: '2023/7/31 7:42:40', status: 'success' }

コミットします。

$ pnpm build
$ git add .
$ git commit -m "Rate Limitを導入"

管理コンソールを確認

upstash の管理コンソールで各種状態について確認できます。

格納されているデータ

Redis に格納されているデータを確認できます。現状は、成功回数(32 回)、失敗回数(70 回)が格納されています。

利用状況

利用状況について詳細が確認できます。

全体確認

ダッシュボードで、全体の利用状況について確認できます。

まとめ

  • upstash で Rate Limit を簡単に導入する方法を紹介しました。
  • upstash のアカウント作成、Next.js への Rate Limit の導入を手順毎に解説しました。
  • 以下に、今回のサンプルコードを公開しています。

https://github.com/hayato94087/nextjs-upstash-rate-limit-sample

参考

https://upstash.com/
https://docs.upstash.com/redis/features/security
https://docs.upstash.com/redis/features/eviction
https://github.com/upstash/ratelimit
https://docs.upstash.com/redis/overall/pricing

Discussion