ServelessFrameworkのflaskとdynamodbをDockerでローカル開発する

## 経緯

副業でちょいとお手伝いをしているところがありまして、フロントだけの開発〜って感じで聞いていたのですが、フロントエンドだけじゃどうにもならなくなってきてしまいまして。
本職ぺちぱーなので普通にLaravelとかで組んでもいいんですけど、先方のやってみたいことやっていいよ〜という温かい言葉に甘えて、こりゃいっちょ新しいことやってみっかな、ってなったのです。
で、やっぱ時代はサーバーレスじゃないですか。
サーバーレスでサーバーサイドは案件で`AWS SAM`使った`Node.js`のはやったのですけど、せっかくなので`Serverless Framework`で`python`使ってやってみようかなーって思い至ったわけです。

## やりたい構成

難しい事やる気はサラサラなくて、`API Gateway`から`Lambda`通って`DynamoDB`の読み書きするってやつですね。
んで、ここの`Lambda`と`DynamoDB`のところをDockerコンテナとしてそれぞれ置こう！ってしたいわけですね。
`docker-compose.yml`はこんな感じになりました。

```docker-compose.yml
version: '3'
services:
  # フロントエンド開発用
  web:
    build: .
    ports:
      - 9080:9080
    volumes:
      - .:/app
    stdin_open: true
    tty: true

  # serverless開発用
  serverless:
    build: ./serverless
    environment:
      - AWS_PROFILE=default
    volumes:
      - ~/.aws/:/root/.aws:ro
      - ./serverless:/app
    ports:
      - 50000:5000
    stdin_open: true
    tty: true
  
  dynamodb-local:
    image: 'amazon/dynamodb-local'
    container_name: dynamodb-local
    user: root
    ports:
      - 8000:8000
    volumes:
      - ./dynamodb_data:/data
    command: ["-jar", "DynamoDBLocal.jar", "-sharedDb", "-dbPath", "/data"]
    
  dynamodb-admin:
    image: aaronshaf/dynamodb-admin:latest
    container_name: dynamodb-admin
    environment:
      - DYNAMO_ENDPOINT=dynamodb-local:8000
    ports:
      - 8001:8001
    depends_on:
      - dynamodb-local
```

で、`serverless`コンテナの`Dockerfile`はこんな感じです。

```Dockerfile:Dockerfile
FROM python:3.9.13-alpine
ARG AWS_PROFILE

ENV NODE_PATH /usr/lib/node_modules/
# install nodejs
RUN apk update \
    && apk add --no-cache nodejs npm curl

# install aws-cli
RUN pip install --upgrade pip
RUN pip install awscli werkzeug boto3

# install serverless framework
RUN npm install -g serverless serverless-plugin-existing-s3

# change work directory
WORKDIR /app/flask-dynamodb-api
```

それから、`serverless.yml`はこんな感じになりました。
これは`serverless`コマンドの`flask-dynamodb-api`のテンプレートから自動生成されるやつにちょこっと書き加えたやつです。

```serverless.yml
service: ando-san-flask-dynamodb-api

frameworkVersion: '3'

custom:
  tableName: 'users-table-${self:provider.stage}'
  wsgi:
    app: app.app
  dynamodb:
    start:
      host: dynamodb-local
      port: 8000
      noStart: true
      migrate: true
    stages:
      - dev

provider:
  name: aws
  region: ap-northeast-1
  runtime: python3.9
  stage: dev
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - dynamodb:Query
            - dynamodb:Scan
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:UpdateItem
            - dynamodb:DeleteItem
          Resource:
            - Fn::GetAtt: [ UsersTable, Arn ]
  environment:
    USERS_TABLE: ${self:custom.tableName}

functions:
  api:
    handler: wsgi_handler.handler
    events:
      - httpApi: '*'

plugins:
  - serverless-wsgi
  - serverless-python-requirements
  - serverless-dynamodb-local
resources:
  Resources:
    UsersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        AttributeDefinitions:
          - AttributeName: userId
            AttributeType: S
        KeySchema:
          - AttributeName: userId
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: ${self:custom.tableName}
```

ポイントを解説していきましょう。

## 開発の起点どうすんべ

`serverless`を`python`でチャレンジすると決めたので、Qiita漁って参考になりそうなのを探してきました。

https://qiita.com/seiichi_akiba/items/d42bda576f3fc6ac5117

こちらの記事を参考にさせていただきました。

とはいえ、4年前の記事なので、pythonのバージョン新しくしたいぜ！と思いイキって`dockerhub`にあるほぼ最新の`3.10.5`にしたら無事に`serverless deploy`で死んだので一つ落として`3.9`にしました。
`aws cli`用のプロファイルは普通にホスト側にあるクレデンシャルを流用したかったので`~/.aws/`をコンテナ側にそのままマウントして、環境変数`AWS_PROFILE`を設定するように`docker-compose.yml`に記述しています。

## dynamodb-local って何？

dockerhubをウロウロしてたら`dynamodb-local`というDockerイメージが有るのを見つけました。

https://hub.docker.com/r/amazon/dynamodb-local

Serverless Flameworkではdynamodbは`serverless-dynamodb-local`というプラグインを使って、`serverless dynamodb install`とやって`serverless dynamodb start`するのが常套手段のようですが、以下の記事のとおり、別のプラグインによって道連れになって死ぬということがあるようです。
`serverless-dynamodb-local`プラグインは使わないとどうにもならないのですが、`serverless dynamodb install`を省きたいという意図です。

https://zenn.dev/merutin/articles/0462b069bdba2b

なので、この記事を参考にdynamodb-localも無事Dockerコンテナ化しました……というわけにもいかず、これだとmigrateが走らないのでテーブルが作られません。ふべん！ということで、`serverless.yml`に一工夫を入れます。

```yaml:serverless.yml抜粋
custom:
  tableName: 'users-table-${self:provider.stage}'
  wsgi:
    app: app.app
  dynamodb:
    start:
      host: dynamodb-local
      port: 8000
      noStart: true
      migrate: true
    stages:
      - dev
```

ここの`host`と`noStart`と`migrate`のところですね。
`host`は`docker-compose.yml`にある`dynamodb-local`のコンテナ名ですね。
こうすることによって`serverless dynamodb start`のコマンドを叩いたときにmigrateが動いてDockerコンテナのdynamodb-localの方にテーブルが作られます。

あ、もちろん`app.py`も書き換えておかないとDockerコンテナのdynamodb-localを見に行かないので注意が必要です。

```python:app.py抜粋
dynamodb_client = boto3.client('dynamodb')

if os.environ.get('IS_OFFLINE'):
    dynamodb_client = boto3.client(
        'dynamodb', region_name='dynamodb-local', endpoint_url='http://dynamodb-local:8000'
    )
```

## ローカルでAPIのテストをしたいだけなのに

最後、いちばん大事なポイントです。

README.mdには

> `serverless wsgi serve` ってやるとローカルで動きまっせ

みたいなこと書いてあって、鵜呑みにしてやってみるのですが、一向にコンテナの外から叩けません。
`docker-compose.yml`でちゃんとportのバインディングもしているのにねえ、と悩んでいたんですよ。

```text:不正解のログ
Running "serverless" from node_modules
Using Python specified in "runtime": python3.9
 * Running on http://localhost:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 170-470-797
```

```yaml:docker-compose.yml抜粋
  # serverless開発用
  serverless:
    build: ./serverless
    environment:
      - AWS_PROFILE=default
    volumes:
      - ~/.aws/:/root/.aws:ro
      - ./serverless:/app
    ports:
      - 50000:5000
    stdin_open: true
    tty: true
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57811/aeb2b339-4d3d-21b3-9141-df2b4deb8d35.png)
`Connection was forcibly closed by a peer`ってなんやねん。

コンテナ側で`http://localhost:5000`ならホスト側で`http://localhost:50000`にしたらつながるんと違うんかい！と思うじゃないですか。**違うんですよこれが。**

```text:正解のログ
Running "serverless" from node_modules
Using Python specified in "runtime": python3.9
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 170-470-797
```
はい！　この通り！ `http://0.0.0.0:5000`じゃないとつながらないのです！

こうするためにはこうしないといけないわけですね。

```
$ serverless wsgi server --host 0.0.0.0
```

これで無事、コンテナの外からAPIが叩けるようになって平和が訪れました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57811/ca5fa4d0-6382-aaa8-0918-989caeb6efbe.png)

## さいごに

いかがでしたでしょうか。（言ってみたかった
これを参考にDockerでたのしいServerless開発ライフを送ってみてください。