まだAPI Gatewayで消耗してるの？AppSyncのススメ

この記事は AWS LambdaとServerless #1 Advent Calendar 2019 の20日目の投稿となります。

# AppSync使ってますか？
この記事ではAPI Gatewayと比較をしながら，AppSyncのオススメポイントやTipsを紹介したいと思います。

# AppSyncのオススメなポイント３選

- DynamoDBやElasticsearchに対するオペレーションはLambdaいらず
- 一度のリクエストで必要なレスポンスを全て得られる
- 簡単にPub/Subが行える


## DynamoDBやElasticsearchに対するオペレーションはLambdaいらず

皆さん，そもそもサーバレス好きですか？
サーバレスって，ミドルウェアのパッチ適用とかの保守や，スケーリング，リカバリなどなど，元々はユーザ側が責任を負うべきことがらをまるっとクラウドベンダーが面倒見てくれて，ユーザは自分のアプリケーション部分に注力することができるというところが大きなメリットです。
おそらくこの記事を見ていただいているあなたも，サーバレスのそういうところをうまく活かしたいと思っているはず。

API Gatewayを使った場合，基本的な考え方としてはリソース毎，メソッド毎に１つのLambda Functionと統合し，対応する処理を書いていくことが必要です。（複数リソースやメソッドをまとめて１つのFunctionで処理させることも出来ますが，基本的にはアンチパターンとなります）

AppSyncの場合，データソースとして下記が選べます（２０１９年１２月２０日時点）

- Lambda Function
- DynamoDB
- Aurora Serverless （Data API）
- Elasticsearch Service
- HTTP
- None

AppSyncはマッピングテンプレートと呼ばれているVTLで定義されたテンプレートを使い，上記のデータソースに対して，どのようにリクエストを行うか，返ってきた結果をどのように出力するかを設定できます。
つまり，Lambda Functionを書かなくても，DynamoDB内からデータをクエリして結果を返すことや，DynamoDBにデータを書き込むことは出来てしまいます。
DynamoDBだけでなく，Elasticsearchに対するクエリを行うことや，Data API for Aurora Serverlessを使用してリレーショナルデータベースに対するクエリを行うことも出来てしまいます。

皆さんが作りたいAPIではいかがでしょうか？
少なくともGETメソッドの一意にID等が指定されているリソースを取得する処理や，場合によってはListを返す処理などについては，大部分がデータソース（DynamoDB，Elasticsearch，RDSなど）から取得してJSONに変えてレスポンスしているだけだったりしませんか？

<blockquote class="twitter-tweet" data-conversation="none" data-cards="hidden" data-partner="tweetdeck"><p lang="ja" dir="ltr">そもそも自分の責任範囲をできるだけ狭めたい人がサーバレスやっていると思っているんだけど，それなのにLambdaガリガリ書くのは矛盾しているので，書かなくて出来ることは書かない方向に持っていくべきというのを強調したい</p>&mdash; k1nakayama (@k1nakayama) <a href="https://twitter.com/k1nakayama/status/1207578986580103169?ref_src=twsrc%5Etfw">December 19, 2019</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>



オススメポイント３つ挙げましたが，正直この点だけでもAppSyncを使う意義が十分あると言えます。
Lambda Functionを作って細かな処理を行っていくのも出来るので，そういうことがやりたい場合だけ，
Lambda Functionを書けば良いという使い方ができます。
ちなみに，DynamoDBについては少し前までトランザクションを使う処理はLambda Functionにしなければ出来ませんでしたが，先日これもマッピングテンプレートのみで対応できるようになりました。

[AWS AppSyncキャッシングとAmazon DynamoDBトランザクションのサポートによりGraphQL APIのパフォーマンスと一貫性が更に向上します](https://aws.amazon.com/jp/blogs/news/appsync-caching-transactions/)

## 一度のリクエストで必要なレスポンスを全て得られる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138161/a4d35d76-39bd-7a29-768a-8e4c0bed152f.png)

これは私の会社で運用している電子書籍サイト「[白泉社e-net!](https://hakusensha-e.net)」のトップページです。
このサイトも1年ほど前にサーバレスアーキテクチャでリニューアルしたものですが，この開発時点では残念ながらAppSyncが発表されたばかりで，まだ知見もなく採用には至らずユーザ側のAPIにはAPI Gatewayを使用しています。 [^1]

[^1]: このサイトのサーバレス化については[Serverless Days Fukuoka 2019でのスライド](https://www.slideshare.net/ssuser084061/1-205114869)をご参照ください

このサイトを例にSPAでページの表示をしようとした場合，どのようなAPIコールが必要になるか考えてみましょう

APIを使いたい要素は下記のとおりです。
- ユーザ情報の取得
- 注目のキーワードの取得（検索ボックスの下に表示するもの）
- トップページに表示するコーナーの取得（新着商品一覧，特集商品１，ランキングなど）
- 各コーナーに属する商品情報の取得

実際にはもっといっぱいあるわけですが，ここでは一旦この情報をもとに考えてみましょう

### API Gateway（REST API）の場合

REST APIの場合，リソース毎にAPIコールを行うことが特徴となります。
そのため今回の場合下記のようなAPIコールが考えられます

```
GET /user/{UserId}
{
  "id": "xxxxxxxx",
  "name": "username",
  "status": "active",
  "point": 1000
}
```
```
GET /search/keywords
[{
  "keyword": "無料",
  "link": "/free"
 },
 {
  "keyword": "暁のヨナ特集",
  "link": "/special/akayona"
 },
 {
  "keyword": "ペリリュー ─楽園のゲルニカ─",
  "link": "/product/59216217periryu00711"
 }]
```
```
GET /display/page/index/corners
[{
  "id": "corner_a",
  "name": "new_191220hnayume",
  "title": "【12/20新着】花とゆめ＆コミックス",
  "order": 1
 },
 {
  "id": "corner_b",
  "name": "new_191220ai15",
  "title": "【12/20新着】花ゆめAi",
  "order": 2
 },
・・・
]
```
```
GET /display/corner/corner_a/products
[{"label":"【電子版】花とゆめ",
  "cover_image":"cover/m-hanayme_02002.jpg",
  "product_id":"e5b24205c2b84a20821adff5be6a9a82",
  "publisher":"白泉社",
  "release_at":1576767600000,
  "product_order":45,
  "introduction":"★爆売れ感謝！2020も無自覚エロスで爆裂ヒット！　堤翔「フラレガール」が表紙で登場！　最新ＨＣ5巻も発売中♪　★最新ＨＣ31巻発売中！　大人気！草凪みずほ「暁のヨナ」　★最新12巻＆ケモ姫つき特装版発売！　友藤結「贄姫と獣の王」、「ケモ姫と普通の王」ＳＰショートと２本立て!!　★連載再開カラーつき　ＨＣ2巻発売！　音久無「執事・黒星は傅かない」　★3号連続新連載第2弾　カラー43Ｐ・酒井ゆかり「呪われた夜の太陽」　★よみきり　綾かおり「のけものとワルツ」　他 ※電子化に当たって都合により収録しなかった口絵・記事や作品がある他、紙版のふろくは付いておりません。また、ページ数は紙版のものをそのまま記載しておりますので、電子版のページ数とは違っている場合がございます。",
  "author":[{"author_name":"花とゆめ編集部","author_kana":"ハナトユメヘンシュウブ","author_title":"作者"}],
  "product_group_title_kana":"ハナトユメ",
  "created_at":1576633964949,
  "product_title":"【電子版】花とゆめ 2号（2020年）",
  "product_title_kana":"ハナトユメ",
  "product_name":"2123XXXXhanayme02002",
  "product_group_title":"【電子版】花とゆめ",
  "base_price":350,
  "product_search_title":"【電子版】花とゆめ",
  "product_group_name":"m-hanayme",
  "publish_at":1576594800000,
  "product_type":"manage",
  "title_name":"m-hanayme",
  "type":"comic_magazine",
  "stop_at":1585925999000,
  "taxed_sell_price":385,
  "sell_price_without_tax":350,
  "product_icon":"NEW"},
　・・・
]
```
※GET /display/corner/corner_a/productsのレスポンスはそのコーナーに属する商品数分だけあります
※GET /display/corner/corner_a/productsと同様にGET /display/page/index/cornersでレスポンスされたコーナー数分だけAPIコールを繰り返します。

いかがでしょう？
当たり前といったら当たり前ですが，裏ではこんな大量のAPIがぞろぞろとやり取りされているんですよね。
このAPIコールの仕方からも分かる通り，APIコールを同時に並行して行える処理もあれば，あるAPIからのレスポンスを使って再度別のAPIコールを行うものもあるため，その分のオーバーヘッドが大きくなっていくことも懸念されます。

### AppSync（GraphQL）の場合

GraphQLを使う場合，１度のクエリで複数の情報を取得することが可能です。

```
query getIndexPageData {
  getUser(id: "{UserId}"){
    name
    point
  }
  listSearchKeywords {
    keyword
    link
  }
  listCorners(page: "index"){
    name
    title
    order
    products {
      cover_image
      product_title
      product_order
      author {
        author_name
      }
    }
  }
}

{
  "data": {
    "getUser": {
      "name": "username",
      "point": 1000
    },
    "listSearchKeywords": [
      {
        "keyword": "無料",
        "link": "/free"
      },
      {
        "keyword": "暁のヨナ特集",
        "link": "/special/akayona"
      },
      {
        "keyword": "ペリリュー ─楽園のゲルニカ─",
        "link": "/product/59216217periryu00711"
      }
    ],
    "listCorners": [
      {
        "name": "new_191220hnayume",
        "title": "【12/20新着】花とゆめ＆コミックス",
        "order": 1,
        "products": [
          {
            "cover_image": "cover/m-hanayme_02002.jpg",
            "product_title": "【電子版】花とゆめ 2号（2020年）",
            "product_order": 45,
            "author": [
              {
                "author_name": "花とゆめ編集部"
              }
            ]
          },
          ・・・
        ]
      },
　　　 ・・・
    ]
  }
}
```

いかがでしょう？
この素晴らしさ伝わりますか？

１回のクエリでページ上に表示したい情報をまるっと全部取れてしまいました。
しかもお気づきの方も多いかと思いますが，REST APIだと，GET /display/corner/corner_a/products のAPIで商品情報を取得した際に，トップページでは使わない無駄な情報のレスポンスを受け取ることを余儀なくされます。
ですが，GraphQLの場合，必要なときに必要な分を，必要な形でレスポンスしてもらうことがリクエスター主導で行えます。
なので，無駄な情報を受け取る必要なくスマートに処理することが可能です。

また，API Gatewayの場合，各リソース＆メソッド毎に処理するLambdaを１つ指定する形になりますが，AppSyncの場合ここも非常に柔軟です。
どういうことかというと，上記の例ですと，Query.getUserはDynamoDBから直接取得し，Query.listSearchKeywordsはElasticsearchから取得し，Query.listCornersは基本的にDynamoDBから取得するけど，Query.listCorners.productsはLambdaで処理した結果を返す，といった感じでフィールド毎にデータソースを定めることが可能です。

## 簡単にPub/Subが行える
API Gatewayも今年WebSocketが対応したため，Pub/Sub的なこと出来るかと思います。（すいません。私はAppSyncで満足して試してません。。）
ただ，AppSyncの場合，WebSocketが。。とかそんなの意識することすらいりません。なんかよりサーバレスな感じしませんか？

サブスクライブの仕方は簡単です。

```
subscription updateHistory {
  onUpdateHistory(user_id:"{UserId}"){
    created_at
    message
  }
}
```

これは何らかの履歴情報のページで，履歴が裏で更新されたりすると自動的に更新された情報を受け取りたい場合のリクエストです。

```
mutation updateHistory {
  updateHistory(user_id: "{UserId}",message: "Message Text"){
    user_id
    created_at
  }
}
{
  "data": {
    "updateHistory": {
      "user_id": "{UserId}",
      "created_at": 1576828108
    },
  }
}
```

上記のようにMutation（更新）のリクエストが送られると，上記のSubscriptionを行っていたユーザーには下記のデータが届きます

```
{
  "data": {
    "onUpdateHistory": {
      "created_at": 1576828108,
      "message": "Message Text"
    }
  }
}
```

非常に簡単ですよね。
近年のアプリケーションでは，様々な場面でリアクティブな表現を求められることがあると思います。
こうした際にも，Pub/Subを簡単に実装できるAppSyncはすごく重宝します。

# FAQ

ここまでお読みいただいたあなたは，すでにAppSyncを使いたくてウズウズしているはず！
そんなあなたによくある疑問を解決するお手伝いをいたします

## バリデーションはどうするの？
API Gatewayの場合，あまり知らない方も多いかもしれませんが，OpenAPIのSwagger定義を利用してバリデーションを行う機能が実装されています。（これも実装される前にすでにLambda側でバリデーション機能を実装する習慣がついてしまっていたため，まともに使ったことがないです。。）

AppSyncには残念ながらバリデーション機能はまだありません。
ですが，GraphQL自体スキーマと呼ばれるSwaggerのような定義に従って，それぞれの型が定義されています。
ですので，AppSyncの場合，数字（Int)で入力されてくるはずのフィールドにStringが入ってきたら，その時点でエラーとなります。
その程度のバリデーションはむしろ何も設定せず行われます。
それ以上のバリデーションチェックについては，マッピングテンプレートと呼ばれるVTLで書かれたもので実装するか，Lambda内で実装するかとなります。

マッピングテンプレートでバリデーションを行う方法については，下記の記事が参考になると思います。
[AWS AppSyncで入力チェックエラーを複数返す方法](https://takumon.com/mutliple-validation-errors-in-aws-appsync)

## デバッグはどうやってやればいい？
AppSyncの設定→ログ記録→ログを有効化を行うことによりCloudWatch Logsにリクエストやレスポンス等についてのログが吐き出されます。
これを見ながらデバッグするのが一番簡単な方法かもしれません。
また，テストやローカルでのデバッグを行うという意味では，AWS Amplifyがローカル上でのAppSyncのモックを提供しています。
https://aws.amazon.com/jp/blogs/news/new-local-mocking-and-testing-with-the-amplify-cli/

こちを参考に使ってみるとよいかと思います。

## AppSyncへのリクエストってAmplify使わないとできない？
普通にHTTPのリクエストが行える方法を使い，GraphQLのエンドポイントに対してPOSTリクエストを行うことでリクエスト出来ます。
また，最近だとGraphQLのリクエストを行うライブラリは多く出ているので，そのあたりを使うともっと楽です

## フロントエンドとサーバサイド（Lambda）の両方から使いたい場合，認証はどうする？
私はこのようなケースが最近多く実装しているのですが，認証をIAMを使い，LambdaからはLambdaに割り当てたIAM Roleを使って接続し，
フロントエンドからはCognitoの認証済みRoleやログインがないようなアプリであれば非認証Roleに対して，appsync:GraphQLのアクションを割り当てることで解決出来ます。
なお，LambdaからGraphQLにリクエストを送る際は，SignatureV4のサインが必要です。

また，appsync:GraphQLのリソースには，フィールドレベルで与えることができます。
https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/list_awsappsync.html
なので，フロントエンドから操作されたくないフィールドは許可をしないなどの細かなコントロールも行えます。

## API Gatewayにできて，AppSyncで出来ないことは？

思い当たるところは下記のとおりです。

- エンドポイントにカスタムドメインを与えられない
- AWS WAFを直接連携できない
- IPアドレスでの制限を行えない
- プライベートAPIとして設置できない（VPC対応していない）
- バリデーション機能がない

上記ぐらいかと思いますがほぼほぼ代替策は検討できたりするかなぁとおもいますので，
あとはこの記事を見てくれた皆さんがAppSyncをゴリゴリ使ってくれることで，
API Gatewayのアップデートに負けずにAppSyncのアップデートが多くなることを期待したいところです。

まだまだいっぱいAppSyncの良さを伝えたいものがあるのですが，今回はここまで