Terraform で構築した AWS AppSync で GraphQL を触る


概要

  • Serverless 界隈でよく使われる GraphQL を触ってみた
  • Backend を用意するのが面倒なので AWS AppSync を採用
  • 簡単に構築できるように Terraform で書いた

サンプルコード

以下リポジトリを clone するとすぐに terraform で環境構築できます。
https://github.com/tsubasaogawa/terraform-appsync-graphql-test

Terraform v0.12.24 で動作確認

おさらい

GraphQL

  • API によく使われる言語
  • REST API と比べ
    • リクエストの回数を減らしやすい (一度に複数の機能を呼び出せる)
    • レスポンスされる項目を自由に指定できる
  • スキーマ第一主義
    • フロントエンドとバックエンドの認識齟齬を防ぐ

AppSync

  • GraphQL の API サーバーをマネージドで提供してくれるサービス
    • REST における API Gateway の GraphQL 版のようなもの
  • データソース (API のデータ読み取り先) として AWS の各サービス (Dynamo/Elasticsearch/Lambda etc.) や HTTP が選択できる
    • ノンプログラミングで DB からデータを読み書きする API が作れる

作るもの

  • AppSync (GraphQL API)
    • Query (SQL の SELECT に相当)
      • user: ユーザー情報を DB から取得
    • Mutation (SQL の UPDATE/INSERT/DELETE に相当)
      • createUser: ユーザー情報を DB に書き込み
  • DynamoDB
    • user の保存先

コード抜粋

ここでは AppSync の tf ファイルを掲載。variables は別ファイルで定義されている。
必要となる resource は以下の 4 つ。

  • aws_appsync_graphql_api
  • aws_appsync_api_key
  • aws_appsync_datasource
  • aws_appsync_resolver
modules/appsync/main.tf
# schema は別ファイルで定義しておく
data "local_file" "graphql_schema" {
  filename = "modules/appsync/resources/schema.graphql"
}

resource "aws_appsync_graphql_api" "this" {
  name                = var.name
  authentication_type = "API_KEY"
  schema              = data.local_file.graphql_schema.content
}

resource "aws_appsync_api_key" "this" {
  api_id = aws_appsync_graphql_api.this.id
}

resource "aws_appsync_datasource" "this" {
  api_id           = aws_appsync_graphql_api.this.id
  name             = var.datasource_name[
  # IAM Role も別ファイルで定義
  service_role_arn = aws_iam_role.this.arn
  type             = "AMAZON_DYNAMODB"

  dynamodb_config {
    table_name = var.dynamo_table_name
  }
}

# field の数だけ resolver を定義できるように map 型の resolvers 変数を使う
# https://github.com/tsubasaogawa/terraform-appsync-graphql-test/blob/c112fbd649ef80aeae2648a1fab21ff0bf64af6d/locals.tf#L13-L24
data "local_file" "resolver_requests" {
  for_each = var.resolvers
  filename = "modules/appsync/resources/resolver_templates/${each.key}/request.template"
}

data "local_file" "resolver_responses" {
  for_each = var.resolvers
  filename = "modules/appsync/resources/resolver_templates/${each.key}/response.template"
}

resource "aws_appsync_resolver" "this" {
  for_each    = var.resolvers
  api_id      = aws_appsync_graphql_api.this.id
  field       = lookup(each.value, "field")
  type        = lookup(each.value, "type")
  data_source = lookup(each.value, "data_source")

  request_template  = lookup(data.local_file.resolver_requests, each.key).content
  response_template = lookup(data.local_file.resolver_responses, each.key).content
}

実行

環境構築

# Setup
cd terraform-appsync-graphql-test
terraform init
terraform apply

成功すると AppSync のマネジメントコンソールで API が作成されていることが確認できる。

API テスト

下準備

ここでは作成された GraphQL API を curl で叩いてみる。
curl するために必要な API KEY とホスト名を terraform の output から取得する。

# Set environment variables from tfstate
API_KEY=$(cat terraform.tfstate | jq -r '.outputs.appsync_api_key.value')
HOST=$(cat terraform.tfstate | jq -r '.outputs.appsync_api_uris.value.GRAPHQL' | grep -oP '[^/]+\.amazonaws.com')

createUser でユーザーを作成

request
curl \
  -H 'Content-Type: application/json' \
  -H "x-api-key: $API_KEY" \
  -H "Host: $HOST" \
  -X POST -d '
    {
      "query": "mutation { createUser(name: \"yoshida\") { id name } }"
    }' \
  https://$HOST/graphql
response
{"data":{"createUser":{"id":"2e591d0b-c4cd-41bd-b66a-ad637e506f5f","name":"yoshida"}}}

yoshida というユーザーが 2e591... という ID で作成されたことがわかる。

user でユーザー情報を取得

先程作成した yoshida さんの情報を DB から取得する。ここでは name 属性のみを取得する。

request
curl \
  -H 'Content-Type: application/json' \
  -H "x-api-key: $API_KEY" \
  -H "Host: $HOST" \
  -X POST -d '
    {
      "query": "query { user(id: \"2e591d0b-c4cd-41bd-b66a-ad637e506f5f\") { name } }"
    }' \
  https://$HOST/graphql
response
{"data":{"user":{"name":"yoshida"}}}

必要な項目を API の利用者側が選択できるため、データのやり取りが軽量で済む。

備考/所感

AppSync の Resolver

AWS AppSync 特有の機能が Resolver であり、GraphQL の Field (本頁における user/createUser) のロジックを表現する。Request と Response を別々の mapping template ファイルとして定義する。
例えば user の Resolver は以下のように定義されている。

  • Request: DynamoDB に GetItem を発行する。
  • Response: 得られた結果を JSON で返す。

Mapping template のリファレンスは以下にまとめられている。
https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/resolver-mapping-template-reference.html

学習コスト

上記の通り Resolver の考え方が若干複雑なのと、GraphQL の Schema における記法がこれまた独特であることから、REST API と比べると若干の取っ付きにくさ・学習コストの高さを感じる。ちょっとした API であれば素直に API Gateway を使うのが手っ取り早い。

※ なお、API Gateway であっても Service Proxy を用いることで Lambda レスで DynamoDB などの AWS リソースを扱うことができる。
https://dev.classmethod.jp/articles/api-gateway-aws-service-proxy-dynamodb/

一方、比較的大きい (育ちそうな) API であれば GraphQL のメリットをしっかり享受できるため、利用を検討してもいいかもしれない。