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 の保存先

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49525/495bede9-8fe8-31af-54cf-381689054432.png)

# コード抜粋

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

- aws_appsync_graphql_api
- aws_appsync_api_key
- aws_appsync_datasource
- aws_appsync_resolver

```tf: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
}
```

# 実行

## 環境構築

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49525/c72201c6-08bc-812c-81bb-bb3225f6e06f.png)

## API テスト

### 下準備

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

```bash
# 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 でユーザーを作成

```bash: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
```

```json:response
{"data":{"createUser":{"id":"2e591d0b-c4cd-41bd-b66a-ad637e506f5f","name":"yoshida"}}}
```

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

### user でユーザー情報を取得

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

```bash: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
```

```json: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 のメリットをしっかり享受できるため、利用を検討してもいいかもしれない。