最近何かと話題になってきた GraphQL ですが、AWS では AppSync という GraphQL サービスがあります。2018年リリースされて以来、サーバーレスコミュニティの中でも年々関心が高まっており、実例も増えてきています。GraphQL は、主流となった REST と比べるとまだ新しい技術と言えますが、この度 Costless の開発で実際に採用してみて、どのような利点と大変さがあったか、整理しておきたいと思います。
おさらい
本題に入る前に、冒頭で触れている REST, GrapgQL, また AWS の GraphQL サービスである AppSync について簡単に触れておきます。
REST API
Web API を定義するためのアーキテクチャスタイルで、データの送受信はステートレスになること、対象のリソースを URI として一位に識別できるように表現すること、HTTP メソッドを利用してそのリソースに対する操作を定義することが原則となります。これにより、API定義の一貫性が保たれ、クライアントとのステートレスなデータの送受信、統合的なインタフェースを作りやすくなるといったメリットがあります。
GraphQL
従来の Web API より効率的かつフレキシブルなアプローチができることで知られているクエリー言語です。すべての操作が一つのエンドポイントを通して実現でき、予め送受信するデータのスキーマを定義しておく必要があります。これにより、クライアントサイドから柔軟にクエリができるようになります。また、一般的なデータの取得と更新に加えて、リアルタイムに通知のやり取りができる仕様 Subscription が含まれています。
AppSync
2018年にリリースされた、AWS のサーバーレスな GraphQL サービスです。 GraphQL スキーマを定義し、Lambda や DynamoDB といった AWS サービスをデータソースとするシームレスな連携ができたり、AWS Amplify と組み合わせてオンライン/オフラインのデータ同期ができるといった特徴があります。
Costless で AppSync / GraphQL を選定した背景
弊社ではサーバーレスに特化したコスト管理サービス「Costless」をリリースしました。この Costless を開発するにあたって、Costless 自体も AWS サーバーレスアーキテクチャで構成されていますが、弊社開発チームではそれに加え以下のような理由でバックエンドに AppSync を全面的に採用することにしました。
- 顧客の AWS アカウントと Costless を連携する際に CloudFormation Custom Resources を使う必要があり、CloudFormation の展開が完了したことをリアルタイムでフロントエンドへ通知する必要があった
- 通知を受けたいのは自分がセットしたものだけで、複数ユーザー間や双方向のやり取りは不要(API Gateway の Websocket ではオーバースペック)
- AppSync/GraphQL が話題になることが増えていることも鑑み、新しい技術を使うチャンスと捉える
AppSync を使ってみて、REST で作る場合と比べてみる
それでは、実際 Costless のバックエンドを AppSync/GraphQL メインで実装してみて分かったところについて、慣れ親しんだ REST と比較して良かったところ・大変だったところを紹介します。
良かったところ
① GraphQL スキーマ定義によるバリデーション
文字列や数字といった基本的なデータ型のバリデーションについては、GraphQL スキーマで定義した通りのデータ型によりチェックされるため、個別に行う必要はありません。
REST の場合は仕様ドキュメントレベルで各データ項目の型を定義し、実装に反映する必要があります。また、そういった定義を書くための Open API といった仕組みは充実してきていますが、実装とドキュメントが分離している状況に変わりはなく、大規模になっていくに連れて双方の整合性を維持・管理することがペインポイントとなることもあります。
type User {
id: ID!
name: String!
email: AWSEmail!
version: Int!
}
input UpdateUserInput {
name: String!
email: AWSEmail!
version: Int!
}
※スキーマに合っていないデータが送受信される場合はエラーになる
② Subscription を使ったリアルタイム通知の手軽さ
リアルタイム通知に関しては API Gateway の Websocket で実装することも可能ですが、そういった手間を書けることなくシンプルに実現できる手軽さがあります。
type Subscription {
onCreateProject(id: ID!): CreateProjectResult @aws_subscribe(mutations: ["createProject"])
}
type Mutation {
createProject(input: CreateProjectInput!): CreateProjectResult!
}
type CreateProjectByCfnResult {
id: ID!
name: String!
version: Int!
}
※プロジェクト作成が完了したら呼び出し元(フロントエンド)へ通知が出される
③ AppSync からの DynamoDB CRUD操作が便利
AppSync で受けたリクエストは、Lambda を介さずとも、VTL(Apache Velocity Template Language)というテンプレート言語を使ってシームレスに DynamoDB などの他のサービスと連携することが可能です。ただ、これについては仕様が複雑になるについて最も辛くなるところでもありますので悩ましい部分もあります。
#set($userId = "user#$context.identity.sub")
{
"version": "2018-05-29",
"operation": "GetItem",
"key": {
"id": $util.dynamodb.toDynamoDBJson($userId)
}
}
大変だったところ
AppSync では、GraphQL スキーマで定義した項目の値を解決するためのリゾルバーの設定に、前述の VTL というテンプレート言語を書く必要があります。DynamoDB へのCRUD操作といった簡単なケースでは問題にならないですが、例えばデータにアクセスするための権限チェックや条件分岐が発生するようなケースでは、テンプレート言語であるがゆえに複雑でテストがしづらくなります。
## Note: this logic exists in aaa.response.vtl...
## Check all files when the logic is modified.
#set($project = $context.result)
#if($project.status == "STATUS-1")
#set($status = "STATUS-1")
#elseif($project.status == "STATUS-2")
#set($status = "STATUS-2")
#set($now = $util.time.nowEpochMilliSeconds())
#if($now < $project.date)
#set($status = "STATUS-3")
#end
#elseif($project.status == "STATUS-4")
#set($status = "STATUS-4")
#end
こうした背景から頑張って VTL を書く代わりに AWS CDK や Direct Lambda Resolver を採用するといった方法はありますが、Costless チームではなるべく AppSync を使い倒す意図である程度 VTL と向き合って開発を進めてきました。これに関しては、定番の API Gateway + Lambda + DynamoDB 構成の REST にしていたら、きちんと仕様を詰めてさえいれば特に問題とならないでしょう。