📚

DockerでNext.js+DynamoDBのローカル環境を作る

2022/03/07に公開

概要

Docker を用いて、Next.js と DynamoDB を組み合わせたローカル環境を作ります。
docker-compose を持ちいて、合計 3 つのコンテナを立ち上げて実現します。

  1. Next.js 用コンテナ
  2. DynamoDB(ローカル)用コンテナ
  3. AWS CLI(ローカル)用コンテナ

構成

最終的に作成する構成は以下の通りとします。

.
├ src :Next.jsのプロジェクトソースを格納する
├ bin :AWS CLI用のコンテナとのファイル受け渡し用
├ Dockerfile :Next.js用のコンテナ定義のDockerfile
├ Dockerfile-awscli :AWS CLI用のコンテナ定義のDockerfile
└ docker-compose.yml :各コンテナの起動設定

以下のファイルを作成していきます。

Dockerfile
FROM node:17-alpine
WORKDIR /usr/app
Dockerfile-awscli
FROM amazon/aws-cli

ENV AWS_ACCESS_KEY_ID=fake_access_key\
    AWS_SECRET_ACCESS_KEY=fake_secret_access_key\
    DYNAMODB_REGION=ap-northeast-1
WORKDIR /usr/app
docker-compose.yml
version: '3'
services:
    app:
        build: ./
        volumes:
        - ./src:/usr/app
        command: sh -c "yarn dev"
        ports:
        - "3000:3000"
    dynamodb:
        image: amazon/dynamodb-local
        command: -jar DynamoDBLocal.jar -sharedDb -dbPath . -optimizeDbBeforeStartup
        volumes:
            - dynamodb:/home/dynamodblocal
        ports:
            - 8001:8000
    awscli:
        build:
            context: .
            dockerfile: Dockerfile-awscli
        entrypoint: [""]
        tty: true
        command:
            - /bin/sh
        volumes:
            - ./bin:/usr/app
volumes:
    dynamodb:
        driver: local
    bin:
        driver: local

※DynamoDB のデータは、毎回消えないように volumes 指定で永続化しています。(Docker の管理領域に保管されます)
※DynamoDB の command では、DynamoDB がデフォルトだとメモリ内にデータを保管しており、コンテナを停止するとデータが消えてしまうため、保持するように設定をしています。
※ローカルの bin フォルダを AWS CLI の作業ディレクトリにマウントしています。
※DynamoDB のポートは他の処理と衝突しないよう 8000 番ポート →8001 番ポートにマッピングしています。環境に合わせて設定してください。(8000 番ポートでも問題ないです)

コンテナを起動する

shell
docker-compose up -d

AWS CLI 用コンテナ名を確認する

shell
docker ps -a

AWS CLI 用コンテナに入る

shell
docker exec -it [AWS CLI用コンテナ名] /bin/bash

AWS CLI コマンドを実行する

shell
aws dynamodb list-tables --region ap-northeast-1 --endpoint-url http://dynamodb:8000

※AWS CLI 仕様上、--regionと、--endpoint-urlの指定は毎回必要になります。
※Docker ネットワーク内では、ポート番号 8000 番で通信します。

実行結果
次のような結果が返ってくれば OK です。

shell
{
    "TableNames": []
}

テーブル作成

shell
aws dynamodb \
  --region ap-northeast-1 \
  --endpoint-url http://dynamodb:8000 \
    create-table \
  --table-name User \
  --attribute-definitions \
    AttributeName=UserId,AttributeType=S \
    AttributeName=SortKey,AttributeType=S \
  --key-schema \
    AttributeName=UserId,KeyType=HASH AttributeName=SortKey,KeyType=RANGE \
  --billing-mode PAY_PER_REQUEST

※結果が表示されたら下までスクロールしてqで抜けます。

あとは必要に応じて、コマンドを記載したファイルをbinフォルダに格納しておき、aws dynamodb コマンドを実行しつつ作業を進めていきます。

DynamoDB Client を Next.js プロジェクトに導入する

次のコマンドを実行し、DynamoDB Client を Next.js のプロジェクトにインストール(追加)します。

shell
yarn add @aws-sdk/client-dynamodb

DynamoDB Client をプログラム内から呼び出す

API 処理などで、次のような記載を行い、DynamoDBClient を呼び出せるようにします。

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

DynamoDB Client に接続情報を渡し初期化する

次のような記述を行うことで、ローカルの DynamoDB に接続できる Client を作成できます。
accessKey や secretAccessKey は、ローカル接続の場合はダミー値で構いません。

const dbClient = new DynamoDBClient({
  credentials: {
    accessKeyId: "dummy",
    secretAccessKey: "dummy",
  },
  endpoint: "http://dynamodb:8000",
  region: "ap-northeast-1",
});

あとは必要な処理を実装していけば OK です。
PUT と GET を行った例を記載しておきます。(DynamoDB の PUT と GET を動かしてみる以外特に整えていないので、実際には各種ハンドリングを実装ください)

PUT 処理の例

import type { NextApiRequest, NextApiResponse } from "next";
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";

const dbClient = new DynamoDBClient({
  credentials: {
    accessKeyId: "dummy",
    secretAccessKey: "dummy",
  },
  endpoint: "http://dynamodb:8000",
  region: "ap-northeast-1",
});

type Data = {
  result: string;
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  try {
    const name = req.query.name;
    if (typeof name !== "string") {
      throw new Error();
    }
    const putCommand = new PutItemCommand({
      TableName: "User",
      Item: {
        UserId: { S: "test1" },
        SortKey: { S: "001" },
        Name: { S: name },
      },
    });
    const putResultData = await dbClient.send(putCommand);
    console.log(putResultData);

    res.status(200).json({
      result: "SUCCESS",
    });
  } catch (err) {
    res.status(500).json({ result: "failed to put data" });
  }
}

GET 処理の例

import type { NextApiRequest, NextApiResponse } from "next";
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";

const dbClient = new DynamoDBClient({
  credentials: {
    accessKeyId: "dummy",
    secretAccessKey: "dummy",
  },
  endpoint: "http://dynamodb:8000",
  region: "ap-northeast-1",
});

type Data = {
  result: string;
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  try {
    const getCommand = new GetItemCommand({
      TableName: "User",
      Key: {
        UserId: { S: "test1" },
        SortKey: { S: "001" },
      },
    });
    const getResultData = await dbClient.send(getCommand);
    console.log(getResultData.Item);

    if (!getResultData.Item?.Name.S) {
      res.status(500).json({ result: "failed to get data(No Item)" });
    } else {
      res.status(200).json({
        result: getResultData.Item.Name.S,
      });
    }
  } catch (err) {
    res.status(500).json({ result: "failed to get data(Error)" });
  }
}

Discussion