[Cloudflare Pages+Next.js] AWS SDK for JavaScript v3を使ってDynamoDBからデータ取得して表示してみる

2023.04.18

どうも!オペレーション部の西村祐二です。

最近、Cloudflare、Next.jsをさわっています。

Next.jsをCloudflare Pagesでホスティングして、DynamoDBからデータを取得するところまでいろいろハマりながら手順がわかったので、備忘録兼ねてまとめておきます。

DynamoDBからデータを取得する処理はNext.jsのAPI Routesを使ってAPIを実装し、Edgeランタイムの設定を行い、CloudflareのEdge側で動作させる方針です。

https://nextjs.org/docs/api-routes/introduction

前提条件

AWS側の初期設定やwranglerの初期設定は完了している前提で進めていきます。

環境

  • Node.js:v18.15.0
  • wrangler: 2.15.0
  • @cloudflare/next-on-pages: 0.8.0

Next.jsの雛形プロジェクト作成

$ npx create-next-app --ts test-dynamo-pages

今回、設定は下記のようにしています。
✔ Would you like to use ESLint with this project? … Yes
✔ Would you like to use Tailwind CSS with this project? … Yes
✔ Would you like to use `src/` directory with this project? … Yes
✔ Would you like to use experimental `app/` directory with this project? … No
✔ What import alias would you like configured? … @
cd test-dynamo-pages/

ライブラリインストール

$ npm install @aws-sdk/lib-dynamodb @aws-sdk/client-dynamodb swr

Next.jsのバージョンを変更

Cloudflare Pagesにデプロイする際に利用するビルドライブラリがNext.js13.2.4までに対応しており、最新の13.3.0(2023/4/18現在) のままだと'Could not resolve "node:buffer"'のエラーがでてしまうためです。

https://github.com/cloudflare/next-on-pages/blob/main/docs/supported.md

$ npm install next@13.2.4

AWS側の設定

IAMユーザの作成し、アクセスキーの取得をしておきます。今回、手順は省略します。

権限は今回AmazonDynamoDBReadOnlyAccessを設定しています。

テスト用のDynamoDBテーブル作成し、テスト用のデータを作成

下記のテーブルとテストデータを作成しておきます。

  • DynamoDBテーブル名:test-nextjs

  • テストデータ

  • id:1
  • content:test

これでAWS側の設定はおわりです。

Next.jsでDynamoDBからデータを取得する処理を実装

環境変数用のファイル作成

ローカルで動作確認するための設定として、アクセスキーを環境変数から読み込むため .envではなく、.dev.varsを作成します。

DynamoDBからデータを取得する処理はEdgeランタイムの設定を行いEdge上で動作させるためです。

https://developers.cloudflare.com/workers/platform/environment-variables/#secrets-in-development

先程、作成したAWS側の情報を設定したファイルをプロジェクト直下に作成します。

.dev.vars

AWS_ACCESS_KEY_ID=xxxxxx
AWS_SECRET_ACCESS_KEY=xxxxx
AWS_REGION=xxxxx
TABLE_NAME=xxxxx

間違えて、レポジトリにアップロードしないようにgitignoreに追記しておきましょう

.gitignore

.
.
.
# typescript
*.tsbuildinfo
next-env.d.ts

.dev.vars

DynamoDBの関連の実装

クライアントを作成する処理を実装していきます。

今回、環境変数からアクセスキーを取得し、クライアントを作成するようにしています。

src/libs/dynamodb.ts

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";

export const createDynamoDbClient = () => {
  const marshallOptions = {
    // Whether to automatically convert empty strings, blobs, and sets to `null`.
    convertEmptyValues: false,
    // Whether to remove undefined values while marshalling.
    removeUndefinedValues: true,
    // Whether to convert typeof object to map attribute.
    convertClassInstanceToMap: true,
  };
  const unmarshallOptions = {
    // Whether to return numbers as a string instead of converting them to native JavaScript numbers.
    wrapNumbers: false,
  };
  const translateConfig = { marshallOptions, unmarshallOptions };
  const client: DynamoDBClient = new DynamoDBClient({
    region: process.env.AWS_REGION,
    credentials: {
      accessKeyId: process.env.AWS_ACCESS_KEY_ID || "",
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "",
    },
  });

  return DynamoDBDocumentClient.from(client, translateConfig);
};

DynamoDBからデータを取得する処理を実装していきます。

今回、DynamoDBのテーブルに保存されているデータをすべて取得するようにScanするようにしています。

src/pages/api/dynamodb.ts

import { NextRequest, NextResponse } from "next/server";

import { createDynamoDbClient } from "@/libs/dynamodb";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";

import { ScanCommand, ScanCommandInput } from "@aws-sdk/lib-dynamodb";

export const config = {
  runtime: "edge",
};

const handleGetRequest = async (client: DynamoDBClient) => {
  const params: ScanCommandInput = {
    TableName: process.env.TABLE_NAME,
  };
  const { Items: allItems } = await client.send(new ScanCommand(params));
  return new NextResponse(JSON.stringify(allItems));
};

const handler = async (req: NextRequest) => {
  const client: DynamoDBClient = createDynamoDbClient();

  switch (req.method) {
    case "GET":
      return await handleGetRequest(client);
    default:
      return new NextResponse(JSON.stringify({ error: "error" }));
  }
};

export default handler;

8-10行目がEdge Runtimeを使用するための設定になります。

export const config = {
  runtime: "edge",
};

動作確認

ここで、一旦、API Routesが動作するか確認しておきます。

下記コマンドを実行しNext.jsのアプリケーションをビルドします。

.vercel/output/staticが生成され、wranglerで使用することができます。

$ npx @cloudflare/next-on-pages
.
.
.
▲ Build Completed in .vercel/output [10s]
⚡️ Building Completed.
⚡️ Generated '.vercel/output/static/_worker.js'.

別ターミナルをひらき下記コマンドを実行します。

開発サーバが起動するので、URLにアクセスします。

$ npx wrangler pages dev .vercel/output/static --compatibility-flag=nodejs_compat
.
.
.
[mf:inf] Worker reloaded! (342.70KiB)
[mf:inf] Listening on 0.0.0.0:8788
[mf:inf] - http://127.0.0.1:8788
[mf:inf] - http://100.64.1.17:8788
[mf:inf] Updated `Request.cf` object cache!

http://127.0.0.1:8788/api/dynamodbに指定してアクセスしてみます。

ブラウザ上にDynamoDBに保存したテストデータ表示されたらOKです。

DynamoDBのデータをテーブルで表示

クライアント側でAPI Routesで実装したAPIを叩きDynamoDBのデータを取得しテーブル表示してます。

src/pages/index.tsx

import useSWR from "swr";

type Data = {
  id: string;
  content: string;
}[];

const fetcher = (url: string) => fetch(url).then((res) => res.json());

export default function Home() {
  const { data, error } = useSWR<Data>("/api/dynamodb", fetcher);

  if (error) return <div>An error has occurred.</div>;
  if (!data) return <div>Loading...</div>;

  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <table>
        <thead>
          <tr>
            <th>
              ID
            </th>
            <th>
              CONTENT
            </th>
          </tr>
        </thead>
        <tbody>
          {data.map((data) => (
            <tr key={data.id}>
              <td>{data.id}</td>
              <td>{data.content}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </main>
  );
}

index.tsxの修正が完了したらビルドを再度行います。

$ npx @cloudflare/next-on-pages

http://127.0.0.1:8788にアクセスしDynamoDBのデータが表示されればOKです。

wranglerを使ってCloudflare Pagesにデプロイ

下記コマンドを実行してCloudflare Pagesにデプロイしていきます。

Pagesのプロジェクト名は今回、「test-dynamo-pages」としています。

$ wrangler pages publish .vercel/output/static

✔ Select an account › 
✔ Enter the name of your new project: … test-dynamo-pages
✔ Enter the production branch name: … main

.
.
.

✨ Deployment complete! Take a peek over at https://<your url>

デプロイが完了し、自動的にURLが払い出されますが、このままだと、アクセスしてもエラーとなってしまいますので、追加で、Cloudflare Pagesの設定をしていきます。

Cloudflare Pagesの設定

Pagesのプロジェクトで下記設定を行って行きます。

  • 環境変数の設定

ローカルで設定していた環境変数(.dev.varsの内容)をPagesでも設定していきます。

  • 互換性フラグの設定

設定のFunctionsに移動します。

互換性フラグの設定を行うところがあるので、そこに nodejs_compatを設定します。

設定が完了したら再度デプロイしてます。

$ wrangler pages publish .vercel/output/static

払い出されたURLにアクセスし、ローカルで確認した画面とおなじ表示がされたらOKです。

DynamoDBのテーブルにデータを追加するとPagesの画面も更新されるのがわかると思います。

さいごに

Next.jsをCloudflare Pagesでホスティングして、Next.jsのAPI Routesを使ってDynamoDBからデータ取得するAPIを実装し、CloudflareのEdge側で動作させてみました。

今回、DynamoDBのデータ取得にAWS SDK for JavaScript v3を利用しました。

Cloudflareには魅力的なDBのサービスがありますが、既存のデータはDynamoDBにあり、それを利用しないといけない場面もあるかとおもいためしてみました。

誰かの参考になれば幸いです。