Python Lambda DynamoDB ServerlessFrameworkでAPI作ってみた（ローカル開発）

# はじめに
Python / Lambda / DynamoDB / ServerlessFramework でのAPI開発手順を記しておきます。

AWSの方から「Lambda × Pythonは起動が速く（コールドスタートが起きない）相性がいい」と伺っていたので、デプロイして計測するのが楽しみです！

# 環境
- macOS Ventura
- Python 3.11.4
- ServerlessFramework 3.34.0
- npm 8.19.2
- node v18.18.0

```terminal: バージョン確認コマンド
$ python3 --version
$ python3 -m pip -V
```

# Lambda作成
## 1. 事前準備
```terminal:
$ mkdir project-v1 #プロジェクト用ルートディレクトリ作成
$ npm install -g serverless #必要なプラグインのインストール
$ npm install --save serverless-iam-roles-per-function serverless-offline serverless-prune-plugin
$ sls plugin install -n serverless-python-requirements
$ sls plugin install -n serverless-dynamodb
$ sls dynamodb install

$ sls create --template aws-python3 #Serverlessプロジェクトを作成
```

※[去年使用していた serverless-dynamodb-local はもう使えないようです（npm参照）。](https://www.npmjs.com/package/serverless-dynamodb)

## 2. Serverless定義
可読性・メンテナンス性を考慮し、定義元ファイルを個別に分けています。

```serverless.yml
service: project-v1
frameworkVersion: '3'

provider:
  name: aws
  runtime: python3.11
  region: ap-northeast-1
  stage: stg

plugins:
  - serverless-dynamodb
  - serverless-offline # dynamodb より offline を後ろに記載
  - serverless-iam-roles-per-function
  - serverless-prune-plugin
  - serverless-python-requirements

custom:
  stage: ${opt:stage, self:provider.stage}
  dynamodb: ${file(./config/setting.yml):dynamodb}
  seed: ${file(./config/setting.yml):dynamodb.seed}
  resources: ${file(./config/resources.yml)}
  serverless-offline:
    stage: "local"
  prune:
    automatic: true
    number: 5

resources:
  - ${file(./config/dynamodb.yml)}

functions:
  Hello:
    handler: main.handlers.handler.lambda_handler
    name: Hello
    description: Pythonテスト
    events:
      - http:
          path: hello
          method: GET
          cors: true
    iamRoleStatements:
      - ${file(./config/iam.yml):getItemTables}
```

以下、serverless.yml内で読み込む複数ファイルを定義

```config/setting.yml
dynamodb:
  stages:
    - local
  port: 8000
  docker: false
  start:
    inMemory: true
    migrate: true
    seed: true
  seed:
    test:
      sources:
        - table: ${self:custom.resources.HogesTable.name}
          sources: [./main/resources/migrations/Hoges.json]
        - table: ${self:custom.resources.FugasTable.name}
          sources: [./main/resources/migrations/Fugas.json]

```

```config/dynamodb.yml
Resources:
  HogesTable:
    Type: AWS::DynamoDB::Table
    ////
  FugasTable:
    Type: AWS::DynamoDB::Table
    ////
```

```config/resources.yml
HogesTable:
  name: Hoges
  arn: "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/Hoges*"

FugasTable:
  name: Fugas
  arn: "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/Fugas*"
```

```config/iam.yml
getItemTables:
  Effect: Allow
  Action:
    - dynamodb:getItem
  Resource:
    - ${self:custom.resources.HogesTable.arn}
```

## 3. boto3 (AWS SDK for Python) インストール
```terminal:
$ pip3 install boto3

##Successfully installed boto3-1.28.59 botocore-1.31.59 jmespath-1.0.1 python-dateutil-2.8.2 s3transfer-0.7.0 six-1.16.0 urllib3-1.26.17
```

## 4. Handler作成
```main.handlers.handler.py
import json
from main.models.aws_dynamodb import AwsDynamoDb


def lambda_handler(event, context):
    dynamoDb = AwsDynamoDb()
    hoges = dynamoDb.get_item("table_name", "sk_name", "sk_value")

    print(f"Hoges: {hoges}")

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


## 5. AwsDynamoDbクラス作成
```main.models.aws_dynamodb.py
import boto3


class AwsDynamoDb:
    def __init__(self):
        self.dynamodb = boto3.resource(
            "dynamodb",
            region_name="ap-northeast-1",
            endpoint_url="http://localhost:8000",
        )

    # 値を一つ取得
    def get_item(self, table_name, key, value):
        try:
            table = self.dynamodb.Table(table_name)
            item = table.get_item(Key={key: value})
            return item.get("Item")
        except Exception as e:
            print(f"Error getting item from DynamoDB: {e}")
            return "エラー"
```

# 挙動確認

## 1. ローカル起動
```terminal:
$ sls offline start --stage local
$ sls dynamodb start --stage local
```

## 2. 疎通確認
```terminal:
$ curl http://localhost:3000/local/hello
```

# 参考
- [boto3ドキュメント](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html)
- [npmドキュメント](https://www.npmjs.com/package/serverless-dynamodb)
- [PEP8（コーディング規約）](https://pep8-ja.readthedocs.io/ja/latest/)

# さいごに
サクッとAPI開発するのなら、とりあえず上記で試すのはありかなと思っています。
`sls` や `npm` でのインストールは、インストール時期によって使えない場合もあるので、公式ドキュメントをご確認ください。


Java歴約2年で、Pythonを使うのは初めてでした。
本記事には数行しかPython記述はないですが、これから外部連携したり、大きくしたりで開発続けます。

ユニットテスト・規約チェック・ログ関連・バージョン管理ツールは入れる予定です。
ディレクトリ構造の最適解はまだ持ち合わせていません。

ここ間違ってるよ！もっとこうしたらどう？といったご指摘やアドバイスはいつでも歓迎です！



---
（感想）2年半前のRuby以来の「動的型付け言語」でした。

🙆‍♀️
記述量少なくて済むの感激！書きやすい読みやすい！
（データ型の宣言ないことは少し違和感、、完全にJavaに慣れてしまいました）


🙅‍♀️
エラーやバグが、コンパイルではなくプログラム実行時にしか分からない。
コンパイルに時間を要すのは退屈だったけど、そこでエラー教えてくれるのありがたかった。

結果、どちらも一長一短で楽しいです。もう少しPython続けてみます。