Amplify + AuroraServerless(RDS) + AppSyncでバックエンドを構築する

# 概要
Amplify + AuroraServerless + AppSyncという構成で開発をすることになったのですが相当苦戦したので記事として残しておきます。
基本的にAmplify + AppsyncはDynamoDBの使用をメインとしており、AuroraServerless(RDS)だとコードの自動生成などもDynamoDBに比べて少ないです。
ですので、AmplifyとAppSyncを使う場合はDynamoDBの使用をおすすめします

## Amplifyプロジェクトの作成
フロントは何でもいいですがとりあえずReactにしておきます。
`npx create-react-app amplify_qiita`
作成したプロジェクト直下に移動して、Amplifyの設定をします。
設定はすべてデフォルトです。
`amplify init`
※amplifyを初めて使用する場合はamplify initの前に認証情報を設定する必要があります。
`amplify configure`で出来ますがここでは詳細を省きます。

## APIの作成

初期化が出来たらコンソールの指示通りAPIを追加します。
設定は全てデフォルトで構いません。
`amplify add api`

DynamoDBを使用する場合このまま`amplify push`をしておしまいなのですが、AuroraServerlessを使用する場合はなかなか手間がかかります。
データベースの自動作成がされないので、インスタンスを立ててSQLを使って手動でカラムを登録する必要があります。
一旦CLIを離れてAWSコンソールに移動します。
RDSのページに移動してインスタンスを作成しましょう。
ほとんどデフォルト設定ですがキャパシティータイプにサーバーレス、接続欄の追加設定でData APIにチェックを入れるのを忘れないでください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/7311a086-ea5b-7e8e-f273-355287d03009.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/f033e166-7f57-4774-bb72-58aeedc49a7b.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/f0955944-af5f-0a72-e47a-81b1127b0589.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/1d97be67-c450-ff28-3f35-127d5021e52a.png)![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/989e7037-1c24-d9a0-f038-1434f0825817.png)


インスタンスの作成が終了したら作成したものを選択してアクションからクエリを選びます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/357f95ab-e989-0acf-fc7c-82e6fef033dc.png)

先程設定した認証情報を入力しデータベースに接続するとこんな画面が出てくるので、使用するデータベースをSQLで作成しましょう。`create database qiita`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/07fc3374-a9ae-3c3e-62ec-86f5a2a3fdb8.png)

データベースの作成に成功したら再度接続し直します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/e747f1d6-5286-3346-506a-c7a6cf056cc6.png)

接続出来たら今度はカラムを登録していきます。
今回は1対多の関係を持つUser Postカラムを登録します。
idがINT型でなくVARCHAR型になっているのはAppsyncを使用してデータを登録する際AUTO INCREAMENTが効かないからです。
なのでデータを登録する際はランダムな英数字を生成して登録することになります。
アプリケーション側で処理してもいいですし、リゾルバ側に書いてもいいです。
すべてのカラムに処理を書くのは面倒くさいので私はアプリケーション側で処理します。
この辺もDynamoDBなら気にしなくて良いことなので面倒ですね。
↓上記のissue
https://github.com/aws-amplify/amplify-cli/issues/1371

    create table User (
      id varchar(255) PRIMARY KEY,
      name varchar(255) NOT NULL,
      createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    );
    create table Post (
      id varchar(255) PRIMARY KEY,
      message varchar(255) NOT NULL,
      userId varchar(255) NOT NULL,
      createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    );

ついでにデータも挿入しておきましょう

```
insert into User (id,name) values("1","firstUser"); 
insert into User (id,name) values("2","secondUser"); 
insert into User (id,name) values("3","thirdUser"); 
insert into User (id,name) values("4","fourthUser"); 
insert into User (id,name) values("5","fifthUser"); 
insert into User (id,name) values("6","sixthUser"); 
insert into User (id,name) values("7","seventhUser"); 
insert into User (id,name) values("8","eighthUser"); 
insert into User (id,name) values("9","ninthUser"); 
insert into User (id,name) values("10","tenthUser"); 
insert into Post (id,message,userId) values("1","firstUser firstPost","1"); 
insert into Post (id,message,userId) values("2","firstUser secondPost","1"); 
```

無事に作成できたらまたCLIに戻りましょう。
先程作ったデータベースをAmplifyのAPIに紐付けます。
`amplify api add-graphql-datasource`
対話式で接続先を聞かれるので適切な選択肢を選びます

    ? Provide the region in which your cluster is located: ap-northeast-1
    ? Select the Aurora Serverless cluster that will be used as the data source for your API: amplify-qiita
    ✔ Fetched Aurora Serverless cluster.
    ? Select the database to use as the datasource: qiita

無事に紐付けに成功していれば、amplify/backend/api/amplifyqiita/schema.graphqlが以下のようになっている筈です。

```schema.graphql
input CreatePostInput {
  id: String!
  message: String!
  userId: String!
  createdAt: AWSDateTime!
}

type Post {
  id: String!
  message: String!
  userId: String!
  createdAt: AWSDateTime!
}

input UpdatePostInput {
  id: String!
  message: String
  userId: String
  createdAt: AWSDateTime
}

input CreateUserInput {
  id: String!
  name: String!
  createdAt: AWSDateTime!
}

type User {
  id: String!
  name: String!
  createdAt: AWSDateTime!
}

input UpdateUserInput {
  id: String!
  name: String
  createdAt: AWSDateTime
}

type Mutation {
  deletePost(id: String!): Post
  createPost(createPostInput: CreatePostInput!): Post
  updatePost(updatePostInput: UpdatePostInput!): Post
  deleteUser(id: String!): User
  createUser(createUserInput: CreateUserInput!): User
  updateUser(updateUserInput: UpdateUserInput!): User
}

type Query {
  getPost(id: String!): Post
  listPosts: [Post]
  getUser(id: String!): User
  listUsers: [User]
}

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

schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}
```
後ほど編集しますが一旦これで`amplify push`しましょう
pushが終わったら正常に動いているか確認します。
AWSコンソールからAppsyncに移動して作成されたAPIに移動します。
クエリを選択してテスト作動させてみましょう
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/d533c395-3d12-d467-e678-2939ae2982b4.png)

エラーが起きていますがデータベースとの接続は出来ていますね。
次はクエリとリゾルバを編集する作業をします。

## クエリの編集
listUsersクエリをリクエストした際にUserに紐付いたPostを取得出来るようにし、ページネーションも出来るようにします。

まずはschema.graphqlのAWSDateTimeをStringに置き換えます。
デフォルトだと型の違いでエラーが起こってしまいます。

次にページネーションとリレーションを付けたクエリを実装します。
ここだけ見てもよくわからないと思うのでとりあえずコピペしてください。
後ほど書くリゾルバで全体の動きが解ると思います。

```schema.graphql
input CreatePostInput {
  id: String!
  message: String!
  userId: String!
  createdAt: String!
}

type Post {
  id: String!
  message: String!
  userId: String!
  createdAt: String!
}

input UpdatePostInput {
  id: String!
  message: String
  userId: String
  createdAt: String
}

input CreateUserInput {
  id: String!
  name: String!
  createdAt: String!
}

type User {
  id: String!
  name: String!
  createdAt: String!
  posts: [Post]
}

input UpdateUserInput {
  id: String!
  name: String
  createdAt: String
}

type Mutation {
  deletePost(id: String!): Post
  createPost(createPostInput: CreatePostInput!): Post
  updatePost(updatePostInput: UpdatePostInput!): Post
  deleteUser(id: String!): User
  createUser(createUserInput: CreateUserInput!): User
  updateUser(updateUserInput: UpdateUserInput!): User
}

type Query {
  getPost(id: String!): Post
  listPosts: [Post]
  getUser(id: String!): User
  listUsers(limit: Int, nextToken: String): UserConnection
}

type UserConnection {
  users: [User]
  nextToken: String
}

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

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

編集したら`amplify push`しましょう

## リゾルバの編集
先程編集したクエリはただのガワです。
実際にデータを返す処理はこのリゾルバで書いていきます。

まずはこの`posts: [Post]`をデータベース内のPostレコードと関連付けます。

```
type User {
  id: String!
  name: String!
  createdAt: String!
  posts: [Post]
}
```
AWSコンソールのAppSyncページからスキーマに移動してください。
右側のResolversの欄からUser posts: [Post]を探してきてアタッチをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/2792df3b-7952-7a91-90b0-d9415a07ad2e.png)

データソースに生成されたものを設定するとこんな画面になると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/94ffb806-4321-7e75-0eee-fe4d30181175.png)

見慣れないプログラミング言語が書かれていますね。
これはVTLという言語でAppsyncでリゾルバを書くときは基本的にこれを使うことになります。
Lambdaを通して好きな言語で書くことも出来るのですが、短いプログラムくらいはVTLで書いてしまいましょう。
VTLによるプログラミングの詳細はドキュメントを読んでください。
https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/resolver-mapping-template-reference-programming-guide.html#aws-appsync-resolver-mapping-template-reference-programming-guide

さて、早速VTLでリクエストマッピングテンプレートを上書きしましょう。

```
{
    "version": "2018-05-29",
    "statements": [
        "select * from Post where userId = '$context.source.id'"
    ]
}
```

これで`Type User`の`posts: [Post]`に`select * from Post where userId = '$context.source.id'`で得られたレコードのが反映されるようになります。
SQLに出てきた`$context.source`というのは親要素を示しています。
今回の場合`posts: [Post]`の親要素は`Type User`になるので`$context.source.id`というのは`Type User`の`id: String!`を指すことになりますね。

### Lambdaを通したリゾルバの記述
次はlistUsersクエリのリゾルバを書きます。
流石にコード量が多くてVTLで書くのはきついのでLambdaを通してPythonで書きましょう。

まずはLambda関数に設定するIAMロールを作成しましょう。
AmazonRDSDataFullAccessとAWSLambdaBasicExecutionRoleのポリシーがあれば大丈夫です。
RDSはFullAccessまではいらないかもしれませんが今回は面倒くさかったのでFullAccessにしました。
必要に応じて権限を絞ってください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/abe34717-3ecd-3531-d80e-ad55fa47352e.png)


ロールが作成できたのでLambda関数を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/279de7d2-d09f-69c5-3cc8-913abcfe3524.png)

無事に作成できたら一旦Lambdaは置いておいてAppSyncに移動してください。
左側のメニューからデータソースを選択し、データソースの作成をクリックします。
色々入力欄がありますが先程作成したLambda関数を選択してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/8f7613d6-f77b-5940-f5b1-62ed4e2f81e0.png)

f

これでLambda関数をリゾルバとして設定できるようになったので、左側のメニューからスキーマを選択して先ほどと同じように右側のResolversから`listUsers`を選択してください

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/094b8549-32cf-d639-9203-b816be3d3405.png)

データソースを先程設定したLambdaに変更します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/52506512-500c-a261-fda2-5b85fa1b3534.png)

リクエストマッピングテンプレートの「サンプルテンプレートを選択」から`invoke and forward argument`を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/47831ac4-04e2-2fd5-6986-17fed6796f63.png)

レスポンスマッピングテンプレートの「サンプルテンプレートを選択」から`Return lambda result`を選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/5aefc186-865c-34bf-73f1-1c5e753ffbaa.png)

「リゾルバーを保存」をクリックしたらAppsync側の設定は終了です。

データを取得するのにLambdaはRDSに接続しなければなりません。
そのためにはRDSのARNとSecretManagerのARNが必要なので確認してメモしておいてください。
RDSはデータベースの設定からARNを確認できます。
SecretManagerはAmplifyが自動生成してくれているのでSecretManagerのコンソールに行けば確認できます。

これでLambdaを書く準備が出来ましたのでLambdaのコンソールに移動してlambda_function.pyに好きな処理を記述できます。

```lambda_function.py
import boto3
import os

def lambda_handler(event, context):
    rdsData = boto3.client('rds-data')

    #eventからクエリの引数を受け取れます 今回はページネーションのため取得上限を定めるlimitと
    #複数回に分けてデータを取得する際の識別子として使用するnextTokenを引数として受け取ります
    nextToken = event['nextToken']
    limit = event['limit']
    
    #nextTokenがあればnextTokenの続きから、無ければ最新のレコードから降順にlimit分取得します
    if nextToken:
      sqlStatement = f'select * from User where "{nextToken}" >= createdAt  order by createdAt desc limit {limit + 1};'
    else:
      sqlStatement = f'select * from User order by createdAt desc limit {limit + 1}'
    
    response = rdsData.execute_statement(
        resourceArn=os.getenv('resourceArn'),
        secretArn=os.getenv('secretArn'),
        database=os.getenv('DB_NAME'),
        sql=sqlStatement
    )

    #レスポンスはschema.graphqlで設定したデータ型にしないとエラーになるため取得したデータを整形する
    userList = {
      "users":[],
      "nextToken":None
    }

    if len(response['records']) > limit:
      userList['nextToken'] =  response['records'][-1][2]['stringValue']
      loopTimes = len(response['records']) - 1
    else:
      loopTimes = len(response['records'])

    for i in range(loopTimes):
      id = (response['records'][i][0]['stringValue'])
      name = (response['records'][i][1]['stringValue'])
      createdAt = (response['records'][i][2]['stringValue'])
      data = {
          "id": id,
          "name": name,
          "createdAt": createdAt
      }
      userList['users'].append(data)

    #returnされるデータがそのままクエリの戻り値として戻って来る
    return userList
```

リゾルバの設定が終わったので早速クエリを叩いてみましょう
AppSyncページの「クエリ」から実行できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/3b6fbfad-8a6e-dac6-802a-7ae366b932a5.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/8c113afd-77b5-a5ff-e054-6a2323ff31ab.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662213/7bc49970-24ad-1f90-3861-c50474c7931e.png)

テストデータを挿入する際クエリをまとめて実行してcreatedAtが同じレコードが出来てしまったため若干順番がずれてますが普通に使用する場合は問題ないです。

# 最後に
ここまで読んだらわかるように、Amplify + Auroraserverless + AppSyncの設定はなかなか大変です。
情報も少なくこんな単純な機能を実装するにもかなり苦労しました。
同じような状況で困っている方の参考になれば幸いです。