AWS Copilot で Phoenix Framework の本番環境を構築する データベース編

## はじめに

AWS Copilot で Phoenix Framework の本番環境を構築するシリーズです

- [コスト・料金](https://qiita.com/RyoWakabayashi/private/a170c2ce9dd3cd1fe9ff)
- [リリース用ビルド](https://qiita.com/RyoWakabayashi/private/21baa60f9fcf64835a49)
- [クラスタリング](https://qiita.com/RyoWakabayashi/private/b6888b510b20bb7579e8)
- データベース <- 今ここ
- [認証](https://qiita.com/RyoWakabayashi/private/9ea90df3970ec6e9d272)

実装したサンプルはこちらに格納しています

https://github.com/RyoWakabayashi/phoenix-channels-samples

## ストレージの追加

前回の時点では、クラスタリングはできていますが、
チャットした内容は保存されていません

https://github.com/RyoWakabayashi/phoenix-channels-samples/tree/with-cluster

本番運用するシステムならば、データ保存（永続化）は必要でしょう

AWS Copilot では、ストレージをサービスに追加することにより、データ保存が可能になります

https://aws.github.io/copilot-cli/ja/docs/developing/storage/

`copilot storage init` でストレージを対話形式で追加できます

```                                      
Only found one workload, defaulting to: lb-svc

  What type of storage would you like to associate with lb-svc?  [Use arrows to move, type to filter, ? for more help]
  > DynamoDB            (NoSQL)
    S3                  (Objects)
    Aurora Serverless   (SQL)
```

選択肢は `DynamoDB` か `S3` か `Aurora Serverless` です

今回はチャットなので、 DynamoDB を使ってみましょう

```
...
Storage type: DynamoDB

  What would you like to name this DynamoDB Table? [? for help] 
```

次にテーブル名を入力します

 
`messages` と入力した場合、出来上がるテーブル名は
`react-chat-test-lb-svc-messages` になります

つまり `<アプリケーション名>-<環境名>-<サービス名>-<入力したテーブル名>` です

続いてパーティションキーやソートキーを入力していきます

最終的に以下のように表示されます

```
Recommended follow-up actions:
  - Update lb-svc's code to leverage the injected environment variable MESSAGES_NAME.
    For example, in JavaScript you can write:
    ```
    const storageName = process.env.MESSAGES_NAME
    ```
  - Run `copilot deploy --name lb-svc` to deploy your storage resources.
```

そして、 `copilot/lb-svc/addons/messages.yml` が以下の内容で作成されます

```yml
Parameters:
  App:
    Type: String
    Description: Your application's name.
  Env:
    Type: String
    Description: The environment name your service, job, or workflow is being deployed to.
  Name:
    Type: String
    Description: The name of the service, job, or workflow being deployed.
Resources:
  messages:
    Metadata:
      'aws:copilot:description': 'An Amazon DynamoDB table for messages'
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub ${App}-${Env}-${Name}-messages_x
      AttributeDefinitions:
        - AttributeName: channel_name
          AttributeType: "S"
        - AttributeName: message_id
          AttributeType: "S"
      BillingMode: PAY_PER_REQUEST
      KeySchema:
        - AttributeName: channel_name
          KeyType: HASH
        - AttributeName: message_id
          KeyType: RANGE

  messagesAccessPolicy:
    Metadata:
      'aws:copilot:description': 'An IAM ManagedPolicy for your service to access the messages db'
    Type: AWS::IAM::ManagedPolicy
    Properties:
      Description: !Sub
        - Grants CRUD access to the Dynamo DB table ${Table}
        - { Table: !Ref messages }
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: DDBActions
            Effect: Allow
            Action:
              - dynamodb:BatchGet*
              - dynamodb:DescribeStream
              - dynamodb:DescribeTable
              - dynamodb:Get*
              - dynamodb:Query
              - dynamodb:Scan
              - dynamodb:BatchWrite*
              - dynamodb:Create*
              - dynamodb:Delete*
              - dynamodb:Update*
              - dynamodb:PutItem
            Resource: !Sub ${ messages.Arn}
          - Sid: DDBLSIActions
            Action:
              - dynamodb:Query
              - dynamodb:Scan
            Effect: Allow
            Resource: !Sub ${ messages.Arn}/index/*

Outputs:
  messagesName:
    Description: "The name of this DynamoDB."
    Value: !Ref messages
  messagesAccessPolicy:
    Description: "The IAM::ManagedPolicy to attach to the task role."
    Value: !Ref messagesAccessPolicy
```

addon として CloudFormation のテンプレートが作られるわけですね

この状態でまたデプロイします

```bash
copilot svc deploy \
  --name lb-svc \
  --env test
```

デプロイが完了した後、 AWS のコンソールを見ると確かに DynamoDB にテーブルが作成されています

![スクリーンショット 2022-06-03 15.03.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/e0870f57-2623-dc76-c6df-341003b86870.png)

また、 ECS のタスクに環境変数も追加されています

![スクリーンショット 2022-06-03 15.06.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/3522c792-cae0-b138-07e3-33273c959601.png)

コンテナ内からはこの環境変数を使ってテーブルを指定すれば良いわけですね

ちなみに、テーブルのキーを変更した場合、そのままデプロイすると以下のようなエラーになります

```
...
    - An Amazon DynamoDB table for messages                                            [update complete]           [0.0s]
      CloudFormation cannot update a stack when a custom-named resource requ                                       
      ires replacing. Rename react-chat-test-lb-svc-messages and update the                                        
      stack again.                                                                                                 
...
```

キーを変える場合、 `addons/messages.yml` 内のテーブル名を変更する必要があります

テーブル名を変えたくない場合、一度名前を変更してデプロイした後、
テーブル名を元に戻して再度デプロイすることになります

## Elixir でのテーブル名設定

DynamoDB へのアクセス方法紹介の記事ではないので、ポイントだけ書きます

まず、 `react_chat/config/config.exs` にテーブル名取得用の設定を追加します

```exs
config :react_chat, ReactChatWeb.RoomChannel, table_name: System.get_env("MESSAGES_NAME")
```

また、環境変数 `MESSAGES_NAME` は実行時のコンテナに設定されます

`config.exs`や `prod.exs` はビルド時の環境変数を使うため、
実行時の環境変数を使う場合は `runtime.exs` に設定を書いておく必要があります

したがって、 `react_chat/config/runtime.exs` にも設定を追加する必要があります

```exs
if config_env() == :prod do
  ...
  # 次の行を追加
  config :react_chat, ReactChatWeb.RoomChannel, table_name: System.get_env("MESSAGES_NAME")
end
```

## まとめ

AWS Copilot で簡単に DynamoDB のテーブルを追加することができました

デフォルトで作ったら DynamoDB が `BillingMode: PAY_PER_REQUEST` になっていたので、
コスト最適にするならここもいじる必要があります

RDB を使いたい場合は Aurora Serverless を選択することになると思いますが、こちらはまだ未検証です

本番利用したときどれだけの性能、コストになるのか、、、

リアルタイム処理では DynamoDB を使って、
バッチ処理結果を Aurora Serverless に入れるような感じにすればコスト最適にできそう