AppSync & Aurora Serverless の GraphQL バックエンドを CDK で構築したぞい！（2022/05/24版）

更新
・2022/07/24 [はまったこと](https://qiita.com/r-hirakawa/items/c1e63c2d1e9d1830d0e6#%E3%81%AF%E3%81%BE%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8) の原因を記載

# はじめに

これまで、API Gateway + Lambda + RDS（or Aurora Serverless）の REST API バックエンドしか実装したことがなかったのですが、いま参加している開発ではじめて AppSync + Aurora Serverless の GraphQL バックエンドを実装することになったので、色々調べならが実装中の、現時点の CDK のコードについて軽く紹介してみたいと思います。

> 本記事は、以下の記事の後続記事となります。
> Aurora Serverless の CDK のコードについてご覧になりたい方は、こちらをどうぞ。
>
> - [Aurora Serverless V2 を CDK v2 で構築できるか試してみたぞい！（2022/05/24版）](https://qiita.com/r-hirakawa/items/c71ead3e51307f3710b8)

# 試した内容

## 利用したライブラリ

| ライブラリ名               | バージョン     |
| -------------------------- | -------------- |
| aws-cdk-lib                | 2.25.0         |
| @aws-cdk/aws-appsync-alpha | 2.25.0-alpha.0 |

- AppSync は、GraphQLスキーマとリゾルバ（VTLファイル）を分離して参照する形にしたかったので `aws-appsync-alpha` の方を試してみました。

## 実装コード例

### ディレクトリ構成

```text
. 
└ lib
│ ├ api
│ │ ├ schema.graphql
│ │ └ resolvers
│ │   └ items
│ │     ├ get-items-req.vtl
│ │     └ get-items-resp.vtl
│ ├ stack
│ │ └ xxx-stack.tsx
```

### GraphQL スキーマ

#### schemas.graphql

```graphql
type Item {
  id: ID!
  name: String
}

type Query {
  getItemss: [Item]
}

schema {
  query: Query
}
```

- はじめてのお試しなので Model と Query だけ（Mutation はまだ未定義）

### リゾルバー

#### get-items-req.vtl

```velocity
## SQLステートメントの定義
#set ($statement = "SELECT * FROM item")

## リクエストテンプレートの定義
## @see https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/tutorial-rds-resolvers.html
{
    "version": "2018-05-29",
    "statements": [
        $util.toJson(${statement})
    ]
}
```

- SQLステートメントは複数行で整形して書くこともできる模様
  [AppSync Resolver multiline string · Issue #2331 · aws-amplify/amplify-cli](https://github.com/aws-amplify/amplify-cli/issues/2331)

- `version` って何のバージョンなのか、まだ未確認

- 入力データがある場合は、セキュリティ対策のためにサニタイズが必要ということだが、現時点では条件なしの一覧検索なので未確認

#### get-items-resp.vtl

```tsx
## レスポンステンプレートの定義
## @see https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/tutorial-rds-resolvers.html
$utils.toJson($utils.rds.toJsonObject($ctx.result)[0])
```

- 検索結果をそのまま返すだけなら、お作法に従うだけのよう

### スタック

```ts
import * as rds from 'aws-cdk-lib/aws-rds';
import * as appsync from '@aws-cdk/aws-appsync-alpha';

/**
 * Aurora Serverless をバックエンドに持つ AppSync GraphQL API の定義
 * @see https://docs.aws.amazon.com/cdk/api/v2/docs/aws-appsync-alpha-readme.html
 * @param scope
 * @param cluster ★前回の記事で作成した Aurora Serverless のクラスター★
 */
const graphqlApi = (scope: Construct, cluster: rds.ServerlessCluster) => {
  const api = new appsync.GraphqlApi(scope, 'GraphqlApi', {
    name: 'GraphqlApi',
    schema: appsync.Schema.fromAsset('./lib/api/schema.graphql'),
    xrayEnabled: true,
  });

  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  const dataSource = api.addRdsDataSource('RdsDataSource', cluster, cluster.secret!, env.rdsDatabaseName());
  dataSource.createResolver({
    typeName: 'Query',
    fieldName: 'getItems',
    requestMappingTemplate: appsync.MappingTemplate.fromFile('./lib/api/resolvers/items/get-items-req.vtl'),
    responseMappingTemplate: appsync.MappingTemplate.fromFile('./lib/api/resolvers/items/get-items-resp.vtl'),
  });

  return api;
};
```

- GraphqlApi
  - `GraphqlApiProps` の `schema` 属性に、`schema.graphql` ファイルから読み込んだスキーマ定義を指定します
  - `GraphqlApiProps` の `authorizationConfig` 属性を省略すると、既定値は API キーによる認証モードが適用されます
- RdsDataSource の createResolver
  - `BaseResolverProps` の `typeName` に GraphQL の操作種別（Query or Mutation）を指定します
  - `BaseResolverProps` のと `fieldName` に、`schema.grapql` で定義したクエリの名前を指定します
  - `requestMappingTemplate`、`responseMappingTemplate` に、リクエスト・レスポンスそれぞれのリゾルバーファイルから読み込んだテンプレート定義を指定します

スタックのコードをみると、かなりスッキリと書けているかな。と思います。

> API Gateway を OpenAPI Spec ファイルから作成するのと比べると、少しだけ色々書かないといけないようですが…

## はまったこと

### 初回デプロイ時に AWSServiceRoleForAppSync の作成に失敗したように見える

```text
14:16:46 | CREATE_FAILED        | AWS::AppSync::GraphQLApi                    | GraphqlApiXXXXXXXX
Failed to assume service-linked role arn:aws:iam::xxxxxxxxxxxx:role/aws-service-role/appsync.amazonaws.com/AWSServiceRoleForAppSync, please retry. (Service: AWSAppSync; Status Code: 403; Error Code: AccessDeniedException; Request ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx; Proxy: null)

 ❌  XxxxStack failed: Error: The stack named XxxxStack failed to deploy: UPDATE_ROLLBACK_COMPLETE: Failed to assume service-linked role arn:aws:iam::xxxxxxxxxxxx:role/aws-service-role/appsync.amazonaws.com/AWSServiceRoleForAppSync, please retry. (Service: AWSAppSync; Status Code: 403; Error Code: AccessDeniedException; Request ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx; Proxy: null)
    at prepareAndExecuteChangeSet (/xxxx/node_modules/aws-cdk/lib/api/deploy-stack.ts:385:13)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)
    at CdkToolkit.deploy (/xxxx/node_modules/aws-cdk/lib/cdk-toolkit.ts:209:24)
    at initCommandLine (/xxxx/node_modules/aws-cdk/lib/cli.ts:341:12)

The stack named XxxxStack failed to deploy: UPDATE_ROLLBACK_COMPLETE: Failed to assume service-linked role arn:aws:iam::xxxxxxxxxxxx:role/aws-service-role/appsync.amazonaws.com/AWSServiceRoleForAppSync, please retry. (Service: AWSAppSync; Status Code: 403; Error Code: AccessDeniedException; Request ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx; Proxy: null)
```

[Issue #16598](https://github.com/aws/aws-cdk/issues/16598)

執筆時点では未解決の不具合のようです。
再デプロイすると正常動作するため、エラーが出たら再実行すれば動作します。

# 最後に

AppSync & Aurora Serverless の GraphQL バックエンドがちゃんと完成したら、もう少ししっかりした内容に更新しますぞ〜。

以上。