5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-07-17

概要

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

image.png

image.png

image.pngimage.png

インスタンスの作成が終了したら作成したものを選択してアクションからクエリを選びます
image.png

先程設定した認証情報を入力しデータベースに接続するとこんな画面が出てくるので、使用するデータベースをSQLで作成しましょう。create database qiita
image.png

データベースの作成に成功したら再度接続し直します
image.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

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

クエリの編集

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

データソースに生成されたものを設定するとこんな画面になると思います。
image.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 Userposts: [Post]select * from Post where userId = '$context.source.id'で得られたレコードのが反映されるようになります。
SQLに出てきた$context.sourceというのは親要素を示しています。
今回の場合posts: [Post]の親要素はType Userになるので$context.source.idというのはType Userid: String!を指すことになりますね。

Lambdaを通したリゾルバの記述

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

まずはLambda関数に設定するIAMロールを作成しましょう。
AmazonRDSDataFullAccessとAWSLambdaBasicExecutionRoleのポリシーがあれば大丈夫です。
RDSはFullAccessまではいらないかもしれませんが今回は面倒くさかったのでFullAccessにしました。
必要に応じて権限を絞ってください。
image.png

ロールが作成できたのでLambda関数を作成します。
image.png

無事に作成できたら一旦Lambdaは置いておいてAppSyncに移動してください。
左側のメニューからデータソースを選択し、データソースの作成をクリックします。
色々入力欄がありますが先程作成したLambda関数を選択してください。
image.png

f

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

image.png

データソースを先程設定したLambdaに変更します
image.png

リクエストマッピングテンプレートの「サンプルテンプレートを選択」からinvoke and forward argumentを選択
image.png

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

image.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

image.png

image.png

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

最後に

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

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?