AWS AppSync入門(GraphQL、AWS CDK)

## はじめに

みなさんAppSync使ってますか？
私も最近少し触っているのですが、個人的には結構便利だなーと思ってます。ただ、やっぱり使ってみないとどこが便利で、従来のREST形式との違いが分かりにくいですよね。なので、GraphQLの基礎から従来のREST形式とどう違うのか、AppSyncがどういう立ち位置なのかについて解説していこうと思います。
分かりにくい部分あれば、コメントでご指摘ください。
ソースコード一式は下記にあります。
[appsync-practice-backend](https://github.com/papi-tokei/appsync-practice-backend/blob/master/lib/schema.graphql)

※REST形式の呼び出しについてある程度の知識があることが前提ですので、ご了承ください。

## GraphQLとは

AppSyncを理解するためにはまずGraphQLを理解することから始めましょう。
GraphQL公式ページから内容を抜粋します。
[GraphQL](https://graphql.org/)

### Ask for what you need, get exactly that(必要なものだけを得る)

例えば、ユーザ情報取得APIを実行した場合、ユーザ名やユーザID、年齢や性別などが取得できるとします。ただし、実際に使うのはユーザ名とユーザIDだけで、年齢と性別は不要だった場合、あえてその情報を取得する必要はありません。従来のREST形式ではAPIを呼び出すとすべての情報を取得することになっていましたが、GraphQLの場合は必要な情報のみを取得することができます。この情報の取捨選択はプログラマが明示的に記述する必要はなく、GraphQLサーバー(今回はAppSync)が自動的に実施してくれます。

![Snag_505e2a5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/27e072f8-96d5-24d5-5222-2963bd4aa4c0.png)

### Get many resources in a single request(複数の情報を一つのリクエストで)

例えば、ユーザ情報と記事の情報を取得する場合、従来の方法だと/userを呼び出して、/articlesを呼び出す必要があります。この場合、2回のリクエストが必要となり、時間もリソースも使います。GraphQLの場合は、1回のリクエストでユーザ情報と記事の情報の両方を取得することができます。

![Snag_506b314.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/9c320585-6259-71fa-a405-ba0ddb577282.png)

### Describe what’s possible with a type system(何ができるかは型を見ればわかる)

従来の方法だと/userを呼び出した場合、どんな情報が取れるのかは仕様書などを参照する必要があります。ただし、仕様書が最新かどうかは分からず、仕様書自体が整備されていない場合は、コードを見ないとAPIを呼び出した際にどのような情報が取れるのかが分からない、という場合もありました。GraphQLの場合、スキーマ定義(仕様書みたいなもの)が絶対に必要なので、仕様書を作らないとそもそも動かない、という仕組みになっています。なので、スキーマ定義をみれば、どんな情報を取得できるのかが一目瞭然になります。
※ただし、スキーマ定義にOpenAPIのようなリッチな情報を含めることはできません。

### Evolve your API without versions(バージョン無しでAPIを進化させる)

例えば、RESTの場合、/userの挙動を変えたい場合(戻り値の年齢を削除するなど)、できるだけ既存のシステムに影響を与えないようにするとなると、/v2/userのようにすることがあると思います。この場合、新しくエンドポイントを作成し、バックエンドのコードも作ることで、できる限り既存のシステムに影響を与えることなく/userの挙動を変えることができます。ただ、結構大変ですよね。
GraphQLの場合、できるだけv2の様なことをしなくてもいいように設計されています。GraphQLは明示的に必要なフィールド(年齢など)を指定する必要があるので、削除したいフィールド(年齢など)がどこで使われているかが分かりやすいです。また、`@deprecated`をフィールドにつけることができるので、今後削除予定のフィールドをスキーマ定義に反映することで、フロント側の修正が完了後に、`@deprecated`のフィールドを削除する修正をすることができます。

### Bring your own data and code(好きなデータ、好きな言語で使える)

GraphQLは特定の言語に制限されるような設計になっていないので、PythonでもJavaでも同じような形式で利用することができます。GraphQLのデータ送信は実際はHTTP POSTでデータを送っているだけなので、curlでもデータ通信することができます(詳細は後述)。



# AWS AppSyncとは

前置きが長くなりましたが、やっとAppSyncについてです。これまでの説明でGraphQLサーバーという言葉が出てきましたが、この部分を担当するのがAppSyncになります。具体的な機能としては以下になります。

* リクエストデータがスキーマ定義に従っているかチェック
* 認証機能を提供(APIキー認証など)
* 後続処理と紐づける(リゾルバー)
* 後続処理の結果をリクエストデータに沿ってフィルタリング(ユーザが要求したデータのみ返すように)
* Subscription(実際にはWebSocketサーバー)の機能を提供

上記で上げた機能についてはプログラマが明示的に実装する必要がない、ということでかなりメリットがあるのではないでしょうか。

AppSyncには以下のページがありますが、今回は「スキーマ」「データソース」「クエリ」「設定」についてのみ解説します。

![Snag_618ff7a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/e64a067c-6af8-b9f6-34d8-7fe17a6d3f6a.png)



### 補足：BFF(Backends For Frontends)について

ここまでの説明を聞いてBFFについて思われた方もいるかもしれません。BFFとはその言葉の通り、「フロントエンドのためのバックエンド」という意味で、フロントエンドの負担を軽減するためのシステムを指します。今回のAppSyncもBFFとして機能します。フロントエンドからの要求をよしなにさばいて、フロントエンドが望むデータのみを返す、という役割を果たします。

## スキーマについて

![Snag_62d6f3a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/c5707e4a-6e68-9e54-c731-dd7a713c813c.png)

スキーマとはフロントエンドとバックエンドがどのような形式でデータをやり取りするのかを決めたものです。

そもそも、GraphQLには下記3つのオペレーションが存在します。

* クエリ: 読み取り専用の取得
* ミューテーション: 書き込む、更新して取得
* サブスクリプション: データを受け取るための、存続期間の長い接続

これらのオペレーションについてスキーマで定義していきます。
詳細は下記リファレンスが詳しいですが、簡単に解説します。
[スキーマの設計](https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/designing-your-schema.html)


まずは、「type」について説明します。
レスポンスなどで使用します。

```graphql
type User {
    user_id: ID!
    user_name: String!
    age: Int
    gender: String
}

type Article {
    article_id: String!
    article_name: String!
    publication_date: String
}

type ArticleList {
    articleList: [Article] 
    nextToken: String
}
```

次は「input」です。クエリの引数として利用します。

```graphql
input UserInput {
    user_id: ID!
    user_name: String!
    age: Int!
    gender: String!
}

input ArticleInput {
    article_id: ID!
    article_name: String!
    publication_date: String!
}
```

最後にスキーマ定義です。
「Query 」と「Mutation」「Subscription」はtypeで定義していますが、最初に説明したtypeとは役割が異なります。この3つはトップレベルオペレーションの型として利用します。


```graphql
type Mutation {
    createUser(input: UserInput!): User
    createArticle(input: ArticleInput): Article
}

type Query {
    getUserData(user_id: ID!): User!
    getUserList: [User]
    getArticleList(nextToken: String): ArticleList
}

type Subscription {
    onCreateUser: User 
        @aws_subscribe(mutations: ["createUser"])
}

schema {
    query: Query
    mutation: Mutation
    subscription: Subscription
}
```

見方としては例えば以下の場合は、「getUserData」がフィールド名で、user_idが引数で型がIDで、返るデータがUser型というイメージです。

```graphql:(抜粋)
type Query {
    getUserData(user_id: ID!): User!
}
```

最終的なスキーマ定義は以下になります。
[schema.graphql](https://github.com/papi-tokei/appsync-practice-backend/blob/master/lib/schema.graphql)


### 補足：CDKでスキーマ定義を実装する場合のTIPS

CDKでスキーマ定義をデプロイする場合、スキーマ定義が間違っているとデプロイに失敗します。ただし、その際に出力されるエラーメッセージが分かりにくいので、スキーマ定義はマネジメントコンソール上のAppSync内で編集してエラーがないことを確認した方がいいです。AppSync内のエディタ上では詳細なエラーが表示されるので、エラーの内容がすぐにわかります。

## リゾルバーについて

```graphql:(抜粋)
type Query {
    getUserData(user_id: ID!): User!
}
```

この定義でgetUserDataのインターフェイス定義は出来ましたが、実態はどこにあるのか、と思われたかもしれません。スキーマで定義したフィールドの実態を定義するのが「リゾルバー」になります。

![Snag_6d0e02b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/bbf74aa5-bda7-b3be-acce-ac95286d0238.png)

![Snag_6d2a6d2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/cd283e41-7ec1-565d-7bcf-7e427d340b38.png)

まず、マッピングテンプレートについて簡単に説明します。
データソースからのデータ取得等はマッピングテンプレートを用いて実施します。
詳細は下記を参照ください。
[リゾルバーのマッピングテンプレートの概要](https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/resolver-mapping-template-reference-overview.html)

次にリクエストマッピングテンプレートについて説明します。
今回のデータソースはDynamoDBなので、リクエストマッピングテンプレートを使ってデータの取得や追加、更新を実施します。
詳細は下記リファレンスが詳しいので参照ください。
[DynamoDB のリゾルバーのマッピングテンプレートリファレンス](https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/resolver-mapping-template-reference-dynamodb.html)

レスポンスマッピングテンプレートは、リクエストマッピングテンプレートを使ってデータ処理を実施した結果をどのように処理するのかを定義しています。
リクエスト、レスポンスマッピングテンプレートの両方にVTL(Velocity Template Language)という言語でカスタマイズできます。
変数定義やIF分岐などをすることができるので、後続にLambdaを設置せずともある程度の実装が可能です。
詳細は下記を参照ください。
[リゾルバーのマッピングテンプレートプログラミングガイド](https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/resolver-mapping-template-reference-programming-guide.html)
[リゾルバーのマッピングテンプレートのユーティリティリファレンス](https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/resolver-util-reference.html)


### リゾルバーの動作確認について

リゾルバーのマッピングテンプレート作成時に動作確認をしたい場面はたくさんあると思います。その際のTIPSを紹介します。

「テストコンテキストを選択」から「新しいコンテキストを作成」をクリックします。

![Snag_6f70f32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/fbee9a9d-cf8a-74ef-ec74-4f96e799d3ab.png)

「コンテキスト名」を入力し、「保存」をクリックします。

![Snag_6f8777c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/f6ab649d-ae2e-c1b5-2005-a6724d0de013.png)

「テストを実行」をクリックします。

![Snag_6f9ac33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/a5751251-d290-54f6-91e7-1d432a4e16df.png)

「評価済みリクエストマッピングテンプレート」にはデータソースとやり取りする直前のテンプレートが、「評価済みレスポンスマッピングテンプレート」にはフロントエンドに返すデータが表示されます。

![Snag_6fa953c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/b4d25cac-dbe2-c0c9-0594-1cc097e98248.png)

この動作確認方法は主にVTLでテンプレートをカスタマイズする際に重宝すると思います。注意点として、**この動作確認方法時には実際にデータソースとのやり取りは実施されません**。つまり、DynamoDBからデータ取得は実施しないということです。

もしロジックを作成途中で途中経過をログ出力したい場合は、そのまま記述すると表示されます。

リクエストマッピングテンプレート

![Snag_7228e92.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/07fe8d0b-3369-997d-92c8-41bb1e0f8b53.png)

![Snag_7223ead.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/13727f7c-eb1e-5c34-e3fd-8ca7d7fd9129.png)


## データソースについて

![Snag_62e9b37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/1a2432f4-0977-2c21-716f-bf08246baeba.png)

データソースにはDynamoDBやLambdaを設定することができます。イメージとしては以下の図のようになります。

![Snag_70b2c7c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/97cffacb-bd94-3238-cba5-9d0c36fc4231.png)

データソースについてはそこまで難しいことはありませんが、1点補足しておくとリゾルバーでデータ取得する際の権限(ロール)はここで設定することになります。
このロールに必要な権限がないと、データ取得に失敗します。
※CDKの場合、デフォルト設定だとインデックスの読み取り権限がないので、インデックス経由でデータ取得する場合は、カスタムでロールを当てる必要があります。

![Snag_70e182b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/5aabdb37-89fd-393e-a051-a2b798f34800.png)

## クエリについて

実際にクエリを実行してみることができます。

![Snag_723fc6a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/53867e46-d9a8-a2d7-0d7b-3f68e9c91ff3.png)

オペレーションから必要なフィールドを選択して、実行ボタンをクリックします。
取得するデータが右に表示され、「LOGS」にチェックを入れるとCloudwatch Logが表示されます。
ちなみに、queryの後のMyQueryについては名前は何でもOKです。クエリをひとまとめにするグループとしての役割なので。

![Snag_7279cd2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/147acf4b-7918-1362-a690-10f65866c991.png)

複数のオペレーションを指定することで、複数のデータを取得することができます。
以下の結果ではユーザ情報と記事の情報を1度のリクエストで取得しています。

![Snag_73d67fc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/9abb5c75-bae1-71bb-5483-fb0067475615.png)

同じ方法でMutationも実行できます。

![Snag_7421aed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/736ceef0-4872-3f10-ce69-6f768659bd38.png)

実際にテーブルにデータが保存されていることを確認できます。

![Snag_74264e6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/ead7cd77-7fb3-7b12-b23b-3bf7388117c2.png)


最後にSubscriptionについても試してみましょう。

下記図のようにmutationを実行します。

![Snag_747ad41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/9be2bc6b-c8be-2787-7658-77a2b05e29fa.png)

サブスクライブしているクライアントには下記のようにデータを受信できます。
※AppSyncのクエリ画面でサブスクライブする場合は、サブスクライブ以外の定義が存在するとエラーになるので注意してください。

![Snag_7487ca7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/721d4481-03f3-80b2-5dd1-5ad25b743624.png)

サブスクライブで受信するデータはmutationで受信するデータと同じものです。なので、mutationでgenderを受信しないようにクエリした場合は、同様にsubscriptionでもgenderのデータを受信することはできません。

## 設定について

API URLがGraphQLのエンドポイントになります。
APIキー認証がデフォルトの場合は、API KEYも併せて必要になります。

![Snag_74c9c51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/5eea0f4d-4942-5386-4dac-60e075500648.png)

デフォルトの認証モードにはCognitoユーザプールやIAM認証などが選択できます。
複数の認証モードを選択することができます。
ただし、**スキーマ定義で明示的に追加の認証プロバイダー設定を追加すると、デフォルトの認証プロバイダーは有効ではないので注意してください**。
(詳細は省略しますが、スキーマ定義で認証モードの設定をする際にデフォルト+追加の認証設定を設定する必要があります
[追加の承認モードの使用](https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/security.html#using-additional-authorization-modes))
あと、APIキーの有効期限は最大365日です。

![Snag_7511631.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/fe1c285f-e94a-b479-9724-e3fbfc853ed5.png)


「詳細なコンテンツを含める」のチェックをつけると詳細なログを出力することができます。テンプレートの途中経過なども表示されるので、デバッグ時にはONにするとデバッグがはかどりますが、かなりたくさんのログが表示されるので本番ではオフにするのがいいかと。

![Snag_74e7ba0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/27c79d5b-c821-df9d-6a79-22eb7260b9f4.png)


## おまけ：curlでgraphQLの呼び出しをしてみる

queryとmutationは後ろ側はHTTP POSTなので、curlでも実行することができます。
(ちなみに、subscriptionはWebsocketです)

下記コマンドでデータを取得可能です。
(内容は適宜読み替えてください)

```bash
$ curl -H "x-api-key: <ここにAPIキーを入れる>" -X POST \
-d "{ \"query\": \"query MyQuery { getUserList { age gender user_id user_name }}\"}" \
<ここにgraphQLエンドポイントを入れる>
```

![Snag_76cc337.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/7c9e9d14-2507-8643-d870-c37285d1c448.png)


# その他

CDKでAppSyncを作成する場合のハイレベルコンストラクタはまたStableではありません。
トラッキングは下記issueから
[Tracking: AWS AppSync](https://github.com/aws/aws-cdk/issues/6836)

スキーマ定義をフロントエンド側と共有する必要があります。
Amplify CLIをうまく使うことで、AppSyncのスキーマからTypeScriptの型定義ファイルを作ることができますが、今回は省略します。

API Gateway + Lambdaの場合はLambdaのコールドスタート問題を気にする必要がありますが、AppSyncの場合は気にする必要がありません(恐らくですが)。

Lambda等からIAM認証でAppSyncを呼び出す場合、LambdaのロールにAppSyncの呼び出し権限を付与して、デフォルトの環境編集から認証情報を取得して呼び出すのが便利です(AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY、AWS_SESSION_TOKEN)。