概要
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にチェックを入れるのを忘れないでください。
インスタンスの作成が終了したら作成したものを選択してアクションからクエリを選びます
先程設定した認証情報を入力しデータベースに接続するとこんな画面が出てくるので、使用するデータベースをSQLで作成しましょう。create database qiita
接続出来たら今度はカラムを登録していきます。
今回は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が以下のようになっている筈です。
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に移動します。
クエリを選択してテスト作動させてみましょう
エラーが起きていますがデータベースとの接続は出来ていますね。
次はクエリとリゾルバを編集する作業をします。
クエリの編集
listUsersクエリをリクエストした際にUserに紐付いたPostを取得出来るようにし、ページネーションも出来るようにします。
まずはschema.graphqlのAWSDateTimeをStringに置き換えます。
デフォルトだと型の違いでエラーが起こってしまいます。
次にページネーションとリレーションを付けたクエリを実装します。
ここだけ見てもよくわからないと思うのでとりあえずコピペしてください。
後ほど書くリゾルバで全体の動きが解ると思います。
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]を探してきてアタッチをクリックします。
データソースに生成されたものを設定するとこんな画面になると思います。
見慣れないプログラミング言語が書かれていますね。
これは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にしました。
必要に応じて権限を絞ってください。
無事に作成できたら一旦Lambdaは置いておいてAppSyncに移動してください。
左側のメニューからデータソースを選択し、データソースの作成をクリックします。
色々入力欄がありますが先程作成したLambda関数を選択してください。
f
これでLambda関数をリゾルバとして設定できるようになったので、左側のメニューからスキーマを選択して先ほどと同じように右側のResolversからlistUsers
を選択してください
リクエストマッピングテンプレートの「サンプルテンプレートを選択」からinvoke and forward argument
を選択
レスポンスマッピングテンプレートの「サンプルテンプレートを選択」からReturn lambda result
を選択
「リゾルバーを保存」をクリックしたらAppsync側の設定は終了です。
データを取得するのにLambdaはRDSに接続しなければなりません。
そのためにはRDSのARNとSecretManagerのARNが必要なので確認してメモしておいてください。
RDSはデータベースの設定からARNを確認できます。
SecretManagerはAmplifyが自動生成してくれているのでSecretManagerのコンソールに行けば確認できます。
これでLambdaを書く準備が出来ましたのでLambdaのコンソールに移動して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ページの「クエリ」から実行できます。
テストデータを挿入する際クエリをまとめて実行してcreatedAtが同じレコードが出来てしまったため若干順番がずれてますが普通に使用する場合は問題ないです。
最後に
ここまで読んだらわかるように、Amplify + Auroraserverless + AppSyncの設定はなかなか大変です。
情報も少なくこんな単純な機能を実装するにもかなり苦労しました。
同じような状況で困っている方の参考になれば幸いです。