一人で新規Webサービスを作ろうとしたら、Amplifyにたどり着いた

こんにちは。
フリーランスエンジニアのtelumoです。

# はじめに

この記事では、

- Amplifyを使うまでの経緯
- 実際に使ってみた所感

について書きたいと思います。
具体的なAmplifyの使い方は他の記事や公式に任せたいと思います。
この記事では、私の目線でAmplifyを語っていますので、この記事を読めば体系的にAmplifyが理解できるとかは期待しないでください。。。（すみません）

Amplifyがバックエンドの選択肢の一つになる、そんなきっかけになれば幸いです。

# Amplifyを使うまでの経緯

### 前提

今年、友人の紹介である建設会社でWebサービスを開発する依頼を受けました。
そのWebサービスは求人サイトで、サービスの特徴としては、***地図から求人が探せる***というものです。
[大島てる](https://www.oshimaland.co.jp/)の求人サイト版のようなイメージです。

### 得意なフレームワークでWebアプリを構築

はじめ私はDjangoでアプリを構築していました。
（一人プロジェクトだったので技術は自由に選べました）

Djangoは、

- モデルを記述するだけでDBマイグレーションをやってくれる
- ユーザーのクラスを継承するだけで複数ロールのユーザーを作成して、それぞれに認可を与えられる
- テンプレートエンジンを利用してフロントが比較的楽にかける

といった特徴があります。
これらは、Djangoの特権でもなんでもなく、他のフルスタックフレームワークにも存在する便利機能です。

私はPythonが得意で、DjangoでのWebアプリ構築経験があったので、
「まずはDjangoで」
的なノリでアプリを構築しました。

### プロトタイプが完成

地図を使ったアプリは作ったことがなかったので、最初の段階では実際にアプリを作りながら技術検証をしていました。
インフラ（AWSを利用）は[pulumi](https://www.pulumi.com/)でさくっと実装しました。

余談ですが、pulumiの[awsx](https://github.com/pulumi/pulumi-awsx)を利用すると、AWSのベストプラクティスに則ったインフラが数行のコードで書けるのでおすすめです。

そして、最初のプロトタイプが完成しました。

DjangoでWebアプリ自体は比較的に簡単に作れたのですが、なんか遅いんです。
地図をドラッグしたあとのアイドル状態時にデータベースからデータを取得して地図に表示する仕組みなのですが、
なんか遅い。

調べてみると、

1. WebサーバーがDBサーバーからデータを取得するのに時間がかかっている
2. ブラウザが取得するデータの通信量が大きい

というダブルパンチで遅くなっていることがわかりました。

まず1に関しては、WebサーバーとDBの最適化が必要だなと感じました。
プロトタイプなので、どちらも小さなスペックのものを利用していたので遅いのは当然なのですが、新規サービスでどれだけのスペックのものを用意すればいいのかが測りづらい、、、
当然ですが、***サービスが利用されなくてもサーバー台はかかる***。。。そもそも、一人プロジェクトで***インフラに時間をかけたくないな。。。***
みたいなことを考えていました。
スペックの問題以外にも、***クエリが最適化されているか***という点も自信がありませんでした。

次に2に関してですが、APIのレスポンスの実装がおかしいことがわかりました。
テストデータが数百件のうちは、いいんですが、数万件になった時に、取得するデータがたくさんあると、（当たり前ですが）表示も遅くなります。
だからAPIのレスポンスのパラメーターを適切に調節してあげればいいだけです。
しかし、それと同時に
***もしかして、APIの修正が入ったらいちいちAPI側とブラウザ側書き直さなきゃいけない!?***
みたいなことを考えていました。

### WebサーバーをLambdaにする

これを解決するために、色々と試行錯誤しました。
後から考えると、この試行錯誤している時の私の失敗は、
***Djangoメインで考えていた***ことです。
そもそも、Webアプリケーションを作るとなったらWebサーバー + RDBという構成は一般的で、
Djangoでなくても、そういう構成で利用できるフレームワークをまず考えてしまう人は多いと（勝手に）予想しています。

試行錯誤のうちの一つは、WebサーバーをLambdaに置き換えるというものです。
「Webサーバーがサーバーだからスペックやコストを気にする必要があるんだ。だったらサーバーレスにすればいいじゃない。」
という単純な思考です。

LambdaにDjangoを乗せるには、[zappa](https://github.com/Miserlou/Zappa)を使えば簡単ですね。
ただ、ここで有名な問題にぶち当たります。
それは、***「Lambda + RDS相性良くない問題」***です。

ただ、Aurora Serverlessの[Data API](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/data-api.html)を利用すれば、その問題を解決できると思ってました。
確かにそれでLambdaからRDSは問題なく利用できます。
しかし、それだとDjangoの良さを殺すことになります。何せ、Data APIを呼び出すためのコードを別に書く必要があるのですから。

それに、「Webサーバー + RDB」構成において、RDB（RDS）が占めるコストは少なくありません。
新規サービスでリクエスト数もわからないのに常時稼働のRDBを保持するのは賢明ではないと思いました。

他にも、コンテナサービスであるFargateやk8sを考えましたが、学習コストが高いのと、RDBの問題で二の足を踏んでました。

### Amplifyを思い出す

そんなこんなしている時にふとAmplifyを思い出しました。
[AWS Dev Day 2018](https://aws.amazon.com/jp/aws-devday-tokyo-2018/)の「Dev Day Challenge」に参加した際に、チャットアプリを作りAmplifyを使ったのです。

Dav Dayのときは、開発時間もあっという間だったので詳しく理解することはできませんでしたが、
「そういえば、AWSソリューションアーキテクトの人がAmplifyをめちゃくちゃ推してたな」
という印象が強く残っていたので、調べてみることにしました。

すると、以下の点で今回の案件にぴったりなバックエンドだと感じました。

- サーバーレスでリクエスト課金なので、リクエスト数に最適なインフラを構築する必要がない（そもそもBaaSなのでインフラを意識する必要ない）
- GraphQLがデフォルトで使え、必要なデータをフロントが選択できる（APIを実装する必要がない）
- ユーザーの種類に応じて権限を細かく設定できる（Cognitoとの連携がやりやすい）

ということで、色々ありましたがAmplifyを使ってみることにしました。
Amplifyを使うということは、Djangoはもう必要がないということです。

フロントはNuxt.jsでやりました。
地図の実装が重要なアプリケーションなので、HTMLを返Djangoのようなフレームワークは合っていなかったのかなと思います。


## Amplifyを使ってみた所感

Amplifyたどり着くまでの話が長くなりましたが、ここからは実際に使ってみた感じたことを書きます。

結論を先に言うと、Amplify最高でした！
これからWebアプリケーションを作る際に、まず間違いなく有力な選択肢の一つになると思います。

そういえば、Amplify自体の説明をしていませんね。
Amplifyは、AWSのBaaSです。フロントからバックエンドを簡単に構築することができます。
詳しいことは、公式や記事等を参考にしてください。

- [Amplify Framework](https://aws-amplify.github.io/docs/)
- [スタートアップが AWS Amplify を使うべき3つの理由](https://aws.amazon.com/jp/blogs/startup/techblog-3reasons-amplify/)

では、所感を書いていきます。

### `scheme.graphql`が、データモデルのドキュメントになる

Amplifyでは、バックエンドとしてGraphQLが簡単に利用できます。
データストアはDynamoDBなのですが、GraphQLを挟むことで普通のCRUDや、チャットのようなサブスクリプションが必要なアプリが楽に構築することができます。

GraphQLを利用するために、`scheme.graphql`というファイルを書きます。
逆に言うと、データストア周りで書く必要があるのは`scheme.graphql`だけです。

これが意味するのは、***`scheme.graphql`自体が実質的にデータモデルのドキュメントになる***ということです。
これは非常に楽です。この`scheme.graphql`ファイルを見るだけで、

- どんなテーブルが存在するのか
- それぞれのテーブルにどんなカラムが存在するのか
- 各カラムの型
- テーブルへのアクセス権限まわり

が一目瞭然なのです。

これをみてください。

```graphql
type Salary @model @auth(rules: [{allow: groups, groups: ["Admin"]}]) {
  id: ID!
  wage: Int
  currency: String
}
```

上記は、[Amplifyの公式](https://aws-amplify.github.io/docs/cli-toolchain/graphql#static-group-authorization)に書かれた例です。
GraphQLを触ったことがない人でも少なくとも以下のことが読み取れるのではないかと思います。

- Salaryモデルは、`ID`型の`id`、`Int`型の`wage`、`String`型の`currency`という3つのフィールドから成り立っていること。
- `Admin`グループにのみ何かしらの権限が与えられていること。

`@model`ディレクティブで実際にDynamoDBにテーブルを作ってくれるとか、
Adminグループに与えられる権限はCRUD全ての権限であるとか、
`ID`型の`id`はAWSが自動生成してくれるとか、、、
そう言ったことはドキュメントを読んで一つ一つ理解していく必要がありますが、このようなモデルを`Scheme.graphql`に記述していくだけで、テーブルが簡単に作れます。しかも、（Cognitoと連携して）どんなユーザーにどんな権限が許可されているのかもすぐにわかります。

さらにAmplifyはこの`Scheme.graphql`を読み取って自動でquery、mutation、subscriptionのJavaScript（or TypeScript）のスクリプトを自動生成してくれます！
素晴らしい。

Djangoやその他のフレームワークでも同じようなことができるとは思います。
Djangoの場合は、モデルのクラスを実装すればいいんです。
しかしアプリごとに別々のファイルに書く必要がありますし、所詮スクリプトなので定義的ではありません。

余談ですが、宣言的なコードを見ればドキュメントを書く必要がない（少なくとも見る必要がない）と言うのはエンジニアの理想です。
関数型言語が流行ったのもそういった背景があるからだと思います。

### AmplifyはRDBユーザーでも利用しやすい

AmplifyのデータストアであるDynamoDBはいわゆる「NoSQL」です。そのため、RDBを主に利用する人にとっては理解しにくいところです。

しかし、AmplifyはRDBを利用する人が理解しやすいと感じます。
RDBでは、「1対多」や「多対多」の関係を利用してテーブルを作成していきますが、同様のことが`Scheme.graphql`にも記述できます。
（そもそも、AWSのGraphQLサービスであるAppSyncはバックエンドにRDSも利用できるので当たり前かもしれません。）

```graphql
type Post @model {
  id: ID!
  title: String!
  comments: [Comment] @connection(keyName: "byPost", fields: ["id"])
}

type Comment @model
  @key(name: "byPost", fields: ["postID", "content"]) {
  id: ID!
  postID: ID!
  content: String!
  post: Post @connection(fields: ["postID"])
}
```

上記は、`@connection`ディレクティブを利用した1対多の関係を表したテーブルのサンプルコードです。
Postテーブルが複数のComment（`[Comment]`のように配列で表している)を持っていることが一目瞭然です。
Post側にもComment側にも`＠connection`を記述することでどちらからでももう一方を取得することができます。

そして取得する時には、以下のようなGraphQLのクエリを発行すればいいのです。

```graphql

query GetCommentWithPostAndComments {
  getComment( id: "a-comment-id-1" ) {
    id
    content
    post {
      id
      title
      comments {
        items {
          id
          content
        }
      }
    }
  }
}
```

GraphQLはフロントで取得したいカラムを選択できます。
さらに***Amplifyはこのquery自体も自動で生成してくれます！***
（もちろん自分でカスタマイズしたクエリを記述して利用することも可能です。）

多対多のモデルは、間にテーブルを一つ挟めば実現できます。（RDBと同じですね。）

```graphql
type Post @model {
  id: ID!
  title: String!
  editors: [PostEditor] @connection(keyName: "byPost", fields: ["id"])
}

type PostEditor
  @model(queries: null)
  @key(name: "byPost", fields: ["postID", "editorID"])
  @key(name: "byEditor", fields: ["editorID", "postID"]) {
  id: ID!
  postID: ID!
  editorID: ID!
  post: Post! @connection(fields: ["postID"])
  editor: User! @connection(fields: ["editorID"])
}

type User @model {
  id: ID!
  username: String!
  posts: [PostEditor] @connection(keyName: "byEditor", fields: ["id"])
}
```

### CI/CDもAmplify1つで。

複数環境をAmplify CLIで構築、デプロイできることは当たり前なのですが、Amplifyコンソールを利用すれば、GitHubなどのGitレポジトリと連携して環境毎のバックエンドを自動で選択してくれます。

| 環境      | Gitレポジトリのブランチ | バックエンド（Amplify） |
|:---------|:------------------:|:-----------------:|
| 本番環境  | master             |       prod        |
| 開発環境  | dev                |        dev        |

上記のように、本番環境と開発環境があって、Gitレポジトリのブランチがそれぞれmaster、devと分かれているとします。
そして、本番環境はバックエンドのprod環境を、開発環境はdev環境を利用したい。
よくありますよね。

それ、Amplifyコンソールで環境変数を設定しておけば、Amplifyが自動でやってくれます。
つまり、masterブランチに変更（プルリクからのマージ等が行われるなど）があった場合は、prodのバックエンドが利用され、
devブランチに変更があった場合は、devバックエンドが利用されます！
（もちろんバックエンドの環境は、本番、開発だけでなく自由に追加することができます。）

これは便利ですよね。
フロント側はバックエンドの指定等一切することがなく、Amplifyがやってくれるのです。

また、gitのブランチの変更を感知してデプロイされるタイミングでテストも自動でやってくれます。
現在はE2Eのテストフレームワークである[Cypress](https://www.cypress.io/)のみ利用できます。


## おわりに

ざっくりとAmplifyを利用するに至った経緯と所感を私の視点ではありますが、書いてみました。

書き忘れましたが、最初の問題であった「レスポンスの遅さ」はAmplifyを利用することで解決していました。
もちろんサーバーを使っても、解決していたと思います。
しかし、課金コストの問題や一人でインフラを管理する時間的コストを考えると、Amplifyにして正解だと思います。

余談ですが、BaaSといえばFirebaseも有名ですが、

- GraphQLが楽に使える
- テーブルへのアクセス権限が同じファイルに楽にかけて管理しやすい
- 環境を自由に分けられる

そんなAmplifyの方が私は好きです。