Serverless Framework で DynamoDB を構築する

この記事は [CodeChrysalis Advent Calendar 2019](https://qiita.com/advent-calendar/2019/codechrysalis) の記事です。

# はじめに

[Serverless Framework](https://serverless.com/) はYAMLでサーバサイドの環境構築ができる、Infrastructure As Code の真骨頂です。確かにAWS等のコンソールでもポチポチクリックしながら環境構築はできますが、

1. 変更の管理ができない
2. 画面でポチポチクリックするのが本当に面倒（Lazy...）

なのでServerless Framework の CLI でコマンド一発で全てが構築されるのはとても気持ちが良い。今回はその中でもAWSのDynamoDBをどのように構築するかを紹介します。

## まずはYAML

早速YAMLを紹介します。

```YAML
service: cc-database

custom:
  stage: ${opt:stage, self:provider.stage}

  ccGSIForStudentId: ${self:custom.stage}-dynamodbCcGSIForStudentId
  ccTableName: ${self:custom.stage}-dynamodbCc

  tableThroughputs:
    prod: 5
    default: 1
  tableThroughput: ${self:custom.tableThroughputs.${self:custom.stage}, self:custom.tableThroughputs.default}

provider:
  name: aws
  stage: dev
  profile: cc-admin
  region: ap-northeast-1

resources:
  Resources:
    CcDynamoDbTable:
      Type: AWS::DynamoDB::Table
      DeletionPolicy: Retain
      Properties:
        TableName: ${self:custom.catalogsTableName}
        AttributeDefinitions:
          - AttributeName: cohortId
            AttributeType: S
          - AttributeName: studentId
            AttributeType: S
        KeySchema:
          - AttributeName: cohortId
            KeyType: HASH
          - AttributeName: studentId
            KeyType: RANGE
        ProvisionedThroughput:
          ReadCapacityUnits: ${self:custom.tableThroughput}
          WriteCapacityUnits: ${self:custom.tableThroughput}
        GlobalSecondaryIndexes:
          - IndexName: ${self:custom.ccGSIForStudentId}
            KeySchema:
              - AttributeName: studentId
                KeyType: HASH
            Projection:
              ProjectionType: ALL
            ProvisionedThroughput:
              ReadCapacityUnits: ${self:custom.tableThroughput}
              WriteCapacityUnits: ${self:custom.tableThroughput}

  Outputs:
    CcDynamoDbTableArn:
      Value:
        Fn::GetAtt:
          - CcDynamoDbTable
          - Arn
      Export:
        Name: ${self:custom.stage}-CcDynamoDbTableArn
```

## 要素を解説

### 必ず必要な要素

`service`, `provider`, `resources` は定義が必ず必要です。

#### service

そのサービスの名前を定義します。
ここでサービスはモノリスサービスかマイクロサービスにするのかを決める必要があります。一つのYAMLにサーバサイドのすべての要素を入れてモノリスサービスにしても良いし、それぞれをサービスに分けてマイクロサービスにしても良いし、それは設計次第です。どちらが良いかはこのドキュメントのテーマではないので深追いしません。

この説明ではこのサービスはデータベースだけ（つまりマイクロサービス）として、`cc-database`としました。

#### provider

ここではどのクラウドサービスを利用するかを定義します。今回はAWSなので以下のとおり。

```YAML
  name: aws
  stage: dev
  profile: cc-admin
  region: ap-northeast-1
```

stageはデフォルトの環境を定義しておき、逆に言えば明確に指定しないとproduction環境にデプロイしないようにしています。

profileはserverless-frameworkでデプロイするときのユーザーをあらかじめIAMで作成しておき、ここで指定します。regionは比較的親しみのあるワードだと思いますが、お使いになるリージョンを指定します。

#### resources

ここで実際にデプロイしたいサービスの内容を記述します。もしAWSを利用する場合はSyntaxはCloudFormationに準じます。

```YAML
Type: AWS::DynamoDB::Table
```

これでどのプロダクトをデプロイするかを指定します。今回はDynamoDBを指定しました。

```YAML
DeletionPolicy: Retain
```

Serverless Framework からデータベースを変更したり削除したりしてもデータを残す指定をしています。開発時点では `Delete` でも良いかもしれませんが、本番サービスに入ってからデータを消してしまうと大惨事です。保持する設定をしています。

```YAML
Properties:
```

ここで実際に作成するテーブル、ローカルインデックス、グローバルセカンダリインデックスを指定します。

```YAML
TableName: ${self:custom.catalogsTableName}
```

テーブル名を定義しています。`custom`については後ほど解説します。

```YAML
AttributeDefinitions:
  - AttributeName: cohortId
    AttributeType: S
  - AttributeName: studentId
    AttributeType: S
```

キーとして使う要素を`AttributeDefinitions`ですべて洗い出す必要があります。要素の名前とそのデータ型を指定します。またテーブルだけではなくインデックスで使う要素についてもここで洗い出す必要があります。

```YAML
KeySchema:
  - AttributeName: cohortId
    KeyType: HASH
  - AttributeName: studentId
    KeyType: RANGE
```

ここではテーブルのキーを指定します。パーティーションキーを`cohortId`、ソートキーを`studentId`にしています。

```YAML
ProvisionedThroughput:
  ReadCapacityUnits: ${self:custom.tableThroughput}
  WriteCapacityUnits: ${self:custom.tableThroughput}
```

ここでスループットを指定します。読み込みと書き込みでそれぞれ指定する必要があります。オートスケールにしたくないのでこの状態にしています。

```YAML
GlobalSecondaryIndexes:
  - IndexName: ${self:custom.ccGSIForStudentId}
    KeySchema:
      - AttributeName: studentId
KeyType: HASH
    Projection:
      ProjectionType: ALL
    ProvisionedThroughput:
      ReadCapacityUnits: ${self:custom.tableThroughput}
      WriteCapacityUnits: ${self:custom.tableThroughput}
```

こちらはグローバルセカンダリインデックスの設定です。ほぼテーブルと同じですが、`Projection`だけインデックス特有で、どのカラムをインデックスに持たせるかの設定です。今は`ALL`にしているのですべてを射影しますが、一部のカラムだけで良ければ変更します。

```YAML
Outputs:
  CcDynamoDbTableArn:
    Value:
		Fn::GetAtt:
			- CcDynamoDbTable
			- Arn
		Export:
			Name: ${self:custom.stage}-CcDynamoDbTableArn
```

もし他のマイクロサービスからDynamoDBのArnを読み取りたい場合はここでOutputsを指定しておくと、`"Fn::ImportValue"`で他のマイクロサービスを指定するYAMLに書くことで読み込めます。例えばLambdaがDynamoDBにアクセスするための設定で必要です。

#### custom

ここは変数のようなもので、定義は必須ではないが、定義しておくと便利なものを記述します。

```YAML
custom:
  stage: ${opt:stage, self:provider.stage}

  ccGSIForStudentId: ${self:custom.stage}-dynamodbCcGSIForStudentId
  ccTableName: ${self:custom.stage}-dynamodbCc

  tableThroughputs:
    prod: 5
    default: 1
  tableThroughput: ${self:custom.tableThroughputs.${self:custom.stage}, self:custom.tableThroughputs.default}
```

たとえばここで

```YAML
stage: ${opt:stage, self:provider.stage}
```

はCLIを実行するときの引数にSTAGEが引き渡されたらそれを使用するし、そうでなければ`provider`に指定されているSTAGE、すなわち`dev`を使用する設定としています。

テーブル名、グローバルセカンダリインデックスの名前、スループットの値を変数化しておき、この内部のYAMLや外部のYAMLから読み込むようにしています。

補足ですが、外部のYAMLから当該YAMLを読み込むためには

```
${file(./database/serverless.yml):custom.ccTableName}
```

を使うことで読み込めます。例えばこれはテーブル名を読み込んでいます。

## さいごに

[Code Chrysalis](https://www.codechrysalis.io/)は東京を拠点にした12週間の短期集中型ソフトウェアエンジニア養成学校です。ソフトウェアエンジニアになるための12週間特別集中コース：イマーシブやプログラミング初心者向けの講座もありますので、ぜひ見てみてください。