Serverless Framework で APIGateway + Lambda + DynamoDB を爆速で始める

この記事は[Monstar Lab, Inc. Advent Calendar 2016](http://qiita.com/advent-calendar/2016/monstar-lab) 22日目の記事です。

3ヶ月ほど前になりますが [ServerlessConf Tokyo](http://tokyo.serverlessconf.io/) に行って、Serverlessというバズワードに取り組みたい気持ちが高まりました。サーバレスは銀の弾丸ではなく使える場所も限定的という意見もありましたが、[CloudGuru](https://acloud.guru/) の発表では多くの部分を積極的にサーバレスで構築している例が出るなど、サーバレスの可能性も感じました。
また先日行った[IVS CTO Night 2016 Winter](http://qiita.com/DaisukeHirata/items/d4e68dd7e70600fd1e91)で **事例にこだわりすぎる。最新技術に事例があるはずがない。まずはやってみよう、少なくとも試そう。** という話に感銘を受けました。

AWSのサーバレススタックによくある典型例ですが、APIGateway + Lambda + DynamoDB という構成をServerlessのフレームワークを使ってやってみました。 Serverless フレームワークは以下のようにさまざま出てきていますが、今回はServerless Frameworkを使いました。

- [Serverless Framework](https://serverless.com/)
- [Apex](http://apex.run/)
- [Chalice](https://github.com/awslabs/chalice)
- [Zappa](https://www.zappa.io/)
- [Lamvery](https://github.com/marcy-terui/lamvery)

## Serverless Framework とは
Serverless Frameworkを使うと、YAMLに設定を書いてCLIのコマンドを実行するだけで、AWS設定やデプロイ、コードの呼び出しが簡単に行えます。手作業の多くを自動化してくれるし、GitHubでの管理やCI連携も楽になります。今回は Serverless Frameworkのexampleである[Serverless REST API](https://github.com/serverless/examples/tree/master/aws-node-rest-api-with-dynamodb)をベースに変更を加えます。

## アーキテクチャ
Serverless Frameworkも使いながら作ったプロトタイプです。

![](https://i.imgur.com/PIaFarh.png)

## スクリーンキャプチャ S3にアップロードされた画像を一覧取得し表示する
プロトタイプのキャプチャです。アーキテクチャの6, 7, 8の部分になります。
スワイプするとAPIで取得した画像一覧の画像を時系列で表示します。ジミヘンに深い理由はなく別のプロトタイプで使った画像が手元にあったからです。

<img src="https://i.imgur.com/MUdPiOr.gif" width=250px>

## GitHubリポジトリ

[serverless-rest-api-with-dynamodb](https://github.com/DaisukeHirata/serverless-rest-api-with-dynamodb)

## デプロイ

詳細はリファレンスを参照するとして、以下のコマンドでAWSにデプロイするとYAMLで定義したAPIエンドポイントができます。
```
$ sls deploy -v 
```

```
endpoints:
  POST - https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/shareImages
  GET - https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/shareImages
  GET - https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/shareImages/{id}
  PUT - https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/shareImages/{id}
  DELETE - https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/shareImages/{id}
```

## DynamoDBのテーブル設計について
DynamoDBのテーブル設計は奥が深いです。パーティションキーやソートキー、ローカルセカンダリインデックスやグローバルセカンダリインデックスをどのように設計するかがポイントになります。この動画やStackoverflowを見ると理解が進みました。

- [Amazon DynamoDB テーブル設計と実践 Tips](https://www.youtube.com/watch?v=1qMBI9_pnO4)
- [How to query DynamoDB by date (range key), with no obvious hash key?](http://stackoverflow.com/questions/35963243/how-to-query-dynamodb-by-date-range-key-with-no-obvious-hash-key)
- [Querying DynamoDB by date](http://stackoverflow.com/questions/14836600/querying-dynamodb-by-date)

ところが、DynamoDBテーブルのServerless.ymlでの記述方法がネットには少ない状況です。思ったようなテーブル設計をYAMLに記述するのには苦労するかもしれません。以下はグローバルセカンダリインデックスを貼った例で、この記事の一番の価値かもかもしれません。

```
resources:
  Resources:
    ShareImagesDynamoDbTable:
      Type: 'AWS::DynamoDB::Table'
      DeletionPolicy: Retain
      Properties:
        AttributeDefinitions:
          -
            AttributeName: id
            AttributeType: S
          -
            AttributeName: createdAt
            AttributeType: N
          -
            AttributeName: createdMonth
            AttributeType: S
        KeySchema:
          -
            AttributeName: id
            KeyType: HASH
          -
            AttributeName: createdAt
            KeyType: RANGE
        GlobalSecondaryIndexes:
          -
            IndexName: globalIndex1
            KeySchema:
              -
                AttributeName: createdMonth
                KeyType: HASH
              -
                AttributeName: createdAt
                KeyType: RANGE
            Projection:
                ProjectionType: ALL
            ProvisionedThroughput:
                ReadCapacityUnits: 1
                WriteCapacityUnits: 1
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: 'shareImages'
```

簡単に説明すると、

- アップロードした画像id  ハッシュのパーティションキー
- 作成日時 createdAt レンジのソートキー
- プライマリキー以外の属性は、定義しないでも良い。たとえばtitleやimageUrlなど
- アイテムの更新や削除はidを指定してRESTfulに行う
- ただし、このプライマリキーではquery関数でタイムラインのような時系列での画像一覧の取得ができない
- よって、作成年月(例：201612) createdMonth をグローバルセカンダリインデックスとして作成する。日付や時間ベースでのパーティション分けもありです。
- DynamoDBの性質にパーティションキーでのデータの分散配置があるので、できるだけ異なるハッシュ値で分散させた方がパフォーマンス上、良いはずです。

## iOSクライアントについて
iOSクライアントについては、主旨がズレるので詳細を割愛しますが、以下のようなものです。

- Facebook SDK によるログイン
- AWS SDK によるCognito認証、S3への画像アップロード
- 画像ダウンロード kingfisher
- APIクライアントは自作APIKit風インタフェースの Alamofire + ObjectMapper + RxSwift。 APIKitを使わず自作するのは、自作の方が細かい部分のコントロールが効きやすいからです。また作ってもそれほど多くのコード量になりません。

## Serverless アンチパターン
サーバレスにはアンチパターンもあります。

- 全てをサーバレスにする必要はない。サーバレスにすることが目的ではない。
- 単純にサーバの代わりとは考えない。従来の箱としてのサーバとは別物。同じように扱おうとする失敗する。
- コストへの過剰な期待はやめる。コスト効率がいいのは事実だが、絶対的に安いというわけではない。コストだけに着目するべきではない。
- API Gateway + Lambdaに囚われすぎる。 KinesisではなくAPI Gatewayが必要か？
- Microservicesと混同しすぎる。あるいは夢見すぎ。自然とMicroservicesに近づきやすいが、Serverless != Microservice

## 次にやってみること

- APIGatewayをCognitoでアクセスコントロールする。APIClientは自前のものを使いたい。
- 開発スピードをあげるためにDynamoDBをローカルで動かしたい。[このプラグイン](https://github.com/99xt/serverless-dynamodb-local)で出来るかな？
- AWS Step Functions で Lambda を繋いでみる。

とても手軽にAPIが出来たので、気分も盛り上がりました。 **まずはやってみよう、少なくとも試そう。** の精神でサーバレスの動向を追っていきたいです。

次は23日目の @omatsu さんです。楽しみですね。