Serverless Framework の DynamoDB + lambda でコードを変えずにローカル開発環境を構築する方法

# はじめに
serverless framework はサーバーレス構成に特化したIaCで、手軽にLambdaなどを使ったアプリケーションを構築することができます。
しかしながら、構築は比較的簡単に行なえますが実際に触ってみるとテストがやや大変です。
今回はそんな悩みを解決するために、Lambda + DynamoDB の環境をコードを変更せずにローカルで作成する方法について紹介します。

# 構成
![serverlessframework-dynamodb-local-sample.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1586001/ceafa18c-d23a-4dc3-2c5e-cd750fefcb37.png)


# DynamoDB local
DynamoDBはローカル環境で実行できるパッケージがAWS公式から提供されています。

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DynamoDBLocal.html

Serverless Framework　はこちらのパッケージを用いてローカル環境を構築します。
ドキュメントにも書かれているように、実行にはJRE(Javaの実行環境)が必要になるため、インストールしてから環境構築をしていきましょう(1敗)。

# 環境
- aws-cli/2.15.19
- serverless framework 3.38.0
- java 21.0.2 (JREがDynamoDB localの実行に必要)
- node.js v21.6.1
- python 3.11(Lambda のランタイム)

# 構築
Serverless Framework や　aws cli　の設定されていることが前提で進みます。

## ディレクトリ構成
こちらの構成で進めていきます。
各ファイルの中身を詳しく見たい方はGitHubのリンクを貼っておくのでそちらを見てください。
```
.
├── handler
│   └── hello.py      # Lambda handler
├── seeds
│   └── seed.json  # DynamoDB Local 用データセット
├── package-lock.json
├── package.json
├── requirements.txt　 # Lambdaで使用するパッケージ
├── .env
└── serverless.yml
```

## 各種インストール

### プラグイン
serverless framework で DynamoDB local を動かすためのプラグインとして、`serverless-dynamodb-local`があります。
しかし、こちらは最新の DynamoDB local に対応していないため、`serverless-dynamodb-local`をフォークして作られた`serverless-dynamodb`を使用します。
```zsh
serverless plugin install -n serverless-dynamodb
# 他の必要プラグインもインストールします
serverless plugin install -n serverless-python-requirements
serverless plugin install -n serverless-offline
```

https://github.com/99x/serverless-dynamodb-local

https://github.com/raisenational/serverless-dynamodb

### DynamoDB local のインストール
serverlessコマンドでインストールできます。
JREはインストールされないので、別途インストールしておきましょう。
```zsh
serverless dynamodb install
```
## 各ファイルの編集

### serverless.yml

```yml:serverless.yml
service: dynamodb-local-sample
# .envファイルからの環境変数読み込みを有効化
useDotenv: true

custom:
  stage: ${opt:stage, 'dev'}
  dynamodbTableName: ${env:DYNAMODB_TABLE_NAME}-${self:custom.stage}
  # DynamoDB Local
  serverless-dynamodb:
    docker: false
    start:
      migrate: true
      host: 127.0.0.1
      port: 8000
      seed: true
    seed:
      sampleDatabese:
        sources:
          - table: ${self:custom.dynamodbTableName}
            sources: [./seeds/seed.json]
    stages:
      - dev

provider:
  name: aws
  stage: ${self:custom.stage}
  region: ap-northeast-1
  runtime: python3.10
  environment:
    TZ: Asia/Tokyo

functions:
  hello:
    name: ${self:service}-handler
    handler: handler/hello.lambda_handler
    environment:
      DYNAMODB_TABLE_NAME: ${self:custom.dynamodbTableName}
    events:
      - http:
          path: /hello
          method: get

resources:
  Resources:
    myDynamoDBTable:
      Type: AWS::DynamoDB::Table
      Properties:
        AttributeDefinitions:
          - AttributeName: UserID
            AttributeType: S
        KeySchema:
          - AttributeName: UserID
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 5
          WriteCapacityUnits: 5
        TableName: ${self:custom.dynamodbTableName}

plugins:
  - serverless-python-requirements
  - serverless-offline
  - serverless-dynamodb
```

### .env
serverless framework から呼び出す環境変数を設定します。

```tex:.env
DYNAMODB_TABLE_NAME=sampleTable
```

### seed.json
ローカルで作成した DynamoDB へ手動でデータを入れるのは非常に大変です。
そこで、`serverless-dynamodb`に備えられているseed機能を使います。
これは、テーブル作成時に自動でjsonデータを挿入してくれる機能です。
パーティションキーが含まれていないデータを入れるとエラーになるので注意しましょう。

```json:seeds/seed.json
[
    {
        "UserID": "1",
        "UserName": "John",
        "UserEmail": "xxx@example.com"
    },
    {
        "UserID": "2",
        "UserName": "Doe",
        "UserEmail": ""
    }
]
```

### hello.py
```python:handler/hello.py 
import json
import boto3
import os


# 実行環境によって分岐
if os.environ.get("IS_OFFLINE", "") == "true":
    dynamodb_client = boto3.client("dynamodb", endpoint_url="http://localhost:8000")
else:
    dynamodb_client = boto3.client("dynamodb")

table_name = os.environ.get("DYNAMODB_TABLE_NAME", "")


def lambda_handler(event, context):
    # DynamoDB からデータを取得
    response = dynamodb_client.scan(TableName=table_name)
    items = response.get("Items", [])
    # ページネーション
    while "LastEvaluatedKey" in response:
        response = dynamodb_client.scan(
            TableName=table_name, ExclusiveStartKey=response["LastEvaluatedKey"]
        )
        items.extend(response["Items"])

    return {
        "statusCode": 200,
        "body": json.dumps(items),
    }

```
boto3クライアントの宣言方法が今回の記事で一番重要な部分です。
severless local で実行した場合、環境変数`IS_OFFLINE`が`true`になります。この値を見てclientで使うエンドポイントを分けることで、ローカルとaws上で同じコードを使って動かすことができます。
今回はサンプルなので、テーブルのデータをすべて返す処理としています。

## 必要パッケージのインストール
ローカルで実行するためにはLambdaで使うpythonのパッケージをインストールする必要があります。
今回はboto3が必要なので、requirements.txtに追記します。
```text:requirements.txt
boto3==1.34.54
botocore==1.34.54
jmespath==1.0.1
python-dateutil==2.9.0.post0
s3transfer==0.10.0
six==1.16.0
urllib3==2.0.7
```

```zsh
python -m venv .venv
source ./.venv/bin/activate
# powershellの場合
# ./.venv/bin/activate.ps1
pip install -r requirements.txt
```

## 実行
```zsh
serverless offline start
```
エンドポイントが表示されれば成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1586001/627aa325-09ed-45bd-252f-aba06fb9b119.png)

試しに叩いてみます。
```zsh
curl "http://localhost:3000/dev/hello"
[{"UserEmail": {"S": "xxx@example.com"}, "UserID": {"S": "1"}, "UserName": {"S": "John"}}, {"UserEmail": {"S": ""}, "UserID": {"S": "2"}, "UserName": {"S": "Doe"}
```
ちゃんとDynamoDB に入れたデータが取得できていますね。

# まとめ

今回はローカルで Lambda + DynamoDB の開発環境を作成する方法を紹介しました。ローカル開発環境があることでテストやテストデータの準備のハードルが大きく下がります。

コード全文はリポジトリに上げたので、全文読みたい方はこちらを参考にしてください。

https://github.com/Nalagami/serverless-dynamodb-local-sample

# 参考

https://www.serverless.com/plugins/serverless-python-requirements

https://www.serverless.com/plugins/serverless-dynamodb-local

https://zenn.dev/ishimura/articles/910ef8ec1bcc64