はじめに
本記事は 2024 年 10 月 2 日時点の情報に基づいて執筆しています。
内容に不備ありましたら気軽にコメントください!
こんにちは。いなりくです✋
2023 年 11 月 27 日に、AWS AppSync は RDS Data API を使用して Amazon Aurora クラスター内のデータベースに対してイントロスペクションを行うことで、検出したテーブルに適合した GraphQL API のインターフェイスを簡単に作成することが可能になりました。
以下の記事では、このアップデートを Amazon Aurora Serverless v2 (PostgreSQL) に試したものです。しかし、前回記事の執筆時点では Aurora MySQL では、Data API は Aurora Serverless v1 データベースでのみサポートだったため、あと一歩の検証となりました。
そんな記事を書いたことすらさっぱりと忘れてたのですが、先日 X を眺めていると、こんなアップデートがありました。「遂に来た!」と思い、Aurora MySQL でも早速試してみることとします。
RDS Data API の対応状況
検証を始める前に、自分の理解のためにも RDS Data API の今までの対応状況について調べてみました。
エンジン | クラスタータイプ | 2023 | 2024 | |
---|---|---|---|---|
- 12/20 | 12/21 - | 9/26 - | ||
PostgreSQL | Serverless v2 / Provisioned | × | ○ | ○ |
Serverless v1 | ○ | ○ | ○ | |
MySQL | Serverless v2 / Provisioned | × | × | ○ |
Serverless v1 | ○ | ○ | ○ |
2023 年 12 月 21 日に以下のアップデートが来ました。
2024 年 9 月 26 日に以下のアップデートが来ました。
余談ですが、公式ドキュメントにもあるように、MySQL 8.0 または PostgreSQL 13 上でアプリケーションを実行できる場合は、Aurora Serverless v2 を使用することをお勧めします。
Important (公式ドキュメントから引用)
Aurora has two generations of serverless technology, Aurora Serverless v2 and Aurora Serverless v1. If your application can run on MySQL 8.0 or PostgreSQL 13, we recommend that you use Aurora Serverless v2. Aurora Serverless v2 scales more quickly and in a more granular way. Aurora Serverless v2 also has more compatibility with other Aurora features such as reader DB instances. Thus, if you're already familiar with Aurora, you don't have to learn as many new procedures or limitations to use Aurora Serverless v2 as with Aurora Serverless v1.
やってみた
では、やっていきます。基本的な手順に関しては公式ドキュメントのリンクを載せているのでそちらを参照ください。
Step1. Amazon Aurora Serverless v2 (MySQL) の作成
まず、Aurora Serverless v2 (MySQL) の DB クラスタを作成します。手順については「Aurora MySQL DB クラスターの作成と接続」を参考にしてください。
また、DB クラスター作成時に、以下のようなチェックボックスができるのでチェックを入れます。忘れてしまった場合もあとから有効化は可能です。
認証情報は AWS Secret Manager で管理します。後ほど、認証情報は AWS AppSync からイントロスペクションを実施する際にも利用します。
DB クラスターの作成が完了したら、データベースの作成を行います。今回は「sample_db」とします。
CREATE DATABASE sample_db;
次に、テーブルの作成を行います。以下のようなテーブル定義にします。
CREATE TABLE conversations (
id INT NOT NULL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE messages (
id VARCHAR(36) PRIMARY KEY,
conversation_id INT NOT NULL,
sub VARCHAR(36) NOT NULL,
body TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (conversation_id) REFERENCES conversations(id)
);
CREATE TABLE conversation_participants (
conversation_id INT NOT NULL,
sub varchar(36) NOT NULL,
last_read_at DATETIME,
PRIMARY KEY (conversation_id, sub),
FOREIGN KEY (conversation_id) REFERENCES conversations(id)
);
テーバルが正常に作成されたか確認します。
MySQL [sample_db]> show tables;
+---------------------------+
| Tables_in_sample_db |
+---------------------------+
| conversation_participants |
| conversations |
| messages |
+---------------------------
3 rows in set (0.002 sec)
Step2. GraphQL API の作成
次に、GraphQL API を作成しましょう。GraphQL API データソースで「Amazon Aurora クラスターから始める - New」を選択し、次に進みます。
次に、API 名にわかり易い名前を入力します。今回はデフォルトの「My AppSync API」としました。
次にデータベースを選択します。
「データベースを選択」をクリックすると接続したい Amazon Aurora クラスターが選択出来ます。データベースには「sample_db」と入力します。AWS Secret Manager シークレットは Step1 で作成済みのものを選択します。入力が終えたら「インポート」をクリックします。
「インポート」をクリックするとイントロスペクションが開始され、下の画像のようにデータベースのテーブルがインポートされました。タイプ名はカスタマイズすることができるので、以下のように変更します。(画像は変更前です。)
-
Conversation_participants
:Participant
-
Conversations
:Conversation
-
Messages
:Message
問題なければ、「次へ」をクリックします。
スキーマの設定では、クエリのみを作成するか、クエリ、ミューテーション、サブスクリプションを作成するかを選択することが出来ます。今回は、「すべてのモデルに対してクエリ、ミューテーション、サブスクリプションを作成」を選択します。これで GraphQL API の作成は完了です!
Step4. 動作確認
では、実際にどんなスキーマやリゾルバーが作成されたか見てみましょう。まずはスキーマを見てみましょう。
確認 1 : GraphQL スキーマ
以下が自動で生成されたスキーマです。Aurora PostgreSQL で試したときと同様にスキーマが正しく作成できていることが確認できました。
schema.graphql
type Conversation {
id: Int!
name: String!
created_at: String
}
type ConversationConnection {
items: [Conversation]
nextToken: String
}
input CreateConversationInput {
id: Int!
name: String!
created_at: String
}
input CreateMessageInput {
id: String!
conversation_id: Int!
sub: String!
body: String!
created_at: String
}
input CreateParticipantInput {
conversation_id: Int!
sub: String!
last_read_at: String
}
input DeleteConversationInput {
id: Int!
}
input DeleteMessageInput {
id: String!
}
input DeleteParticipantInput {
conversation_id: Int!
sub: String!
}
type Message {
id: String!
conversation_id: Int!
sub: String!
body: String!
created_at: String
}
type MessageConnection {
items: [Message]
nextToken: String
}
input ModelSizeInput {
ne: Int
eq: Int
le: Int
lt: Int
ge: Int
gt: Int
between: [Int]
}
enum ModelSortDirection {
ASC
DESC
}
input OrderByConversationInput {
id: ModelSortDirection
name: ModelSortDirection
created_at: ModelSortDirection
}
input OrderByMessageInput {
id: ModelSortDirection
conversation_id: ModelSortDirection
sub: ModelSortDirection
body: ModelSortDirection
created_at: ModelSortDirection
}
input OrderByParticipantInput {
conversation_id: ModelSortDirection
sub: ModelSortDirection
last_read_at: ModelSortDirection
}
type Participant {
conversation_id: Int!
sub: String!
last_read_at: String
}
type ParticipantConnection {
items: [Participant]
nextToken: String
}
input TableConversationConditionInput {
name: TableStringFilterInput
created_at: TableStringFilterInput
and: [TableConversationConditionInput]
or: [TableConversationConditionInput]
not: [TableConversationConditionInput]
}
input TableConversationFilterInput {
id: TableIntFilterInput
name: TableStringFilterInput
created_at: TableStringFilterInput
and: [TableConversationFilterInput]
or: [TableConversationFilterInput]
not: [TableConversationFilterInput]
}
input TableIntFilterInput {
ne: Int
eq: Int
le: Int
lt: Int
ge: Int
gt: Int
between: [Int]
attributeExists: Boolean
}
input TableMessageConditionInput {
conversation_id: TableIntFilterInput
sub: TableStringFilterInput
body: TableStringFilterInput
created_at: TableStringFilterInput
and: [TableMessageConditionInput]
or: [TableMessageConditionInput]
not: [TableMessageConditionInput]
}
input TableMessageFilterInput {
id: TableStringFilterInput
conversation_id: TableIntFilterInput
sub: TableStringFilterInput
body: TableStringFilterInput
created_at: TableStringFilterInput
and: [TableMessageFilterInput]
or: [TableMessageFilterInput]
not: [TableMessageFilterInput]
}
input TableParticipantConditionInput {
last_read_at: TableStringFilterInput
and: [TableParticipantConditionInput]
or: [TableParticipantConditionInput]
not: [TableParticipantConditionInput]
}
input TableParticipantFilterInput {
conversation_id: TableIntFilterInput
sub: TableStringFilterInput
last_read_at: TableStringFilterInput
and: [TableParticipantFilterInput]
or: [TableParticipantFilterInput]
not: [TableParticipantFilterInput]
}
input TableStringFilterInput {
ne: String
eq: String
le: String
lt: String
ge: String
gt: String
contains: String
notContains: String
between: [String]
beginsWith: String
attributeExists: Boolean
size: ModelSizeInput
}
input UpdateConversationInput {
id: Int!
name: String
created_at: String
}
input UpdateMessageInput {
id: String!
conversation_id: Int
sub: String
body: String
created_at: String
}
input UpdateParticipantInput {
conversation_id: Int!
sub: String!
last_read_at: String
}
type Mutation {
createParticipant(input: CreateParticipantInput!): Participant
updateParticipant(input: UpdateParticipantInput!, condition: TableParticipantConditionInput): Participant
deleteParticipant(input: DeleteParticipantInput!, condition: TableParticipantConditionInput): Participant
createConversation(input: CreateConversationInput!): Conversation
updateConversation(input: UpdateConversationInput!, condition: TableConversationConditionInput): Conversation
deleteConversation(input: DeleteConversationInput!, condition: TableConversationConditionInput): Conversation
createMessage(input: CreateMessageInput!): Message
updateMessage(input: UpdateMessageInput!, condition: TableMessageConditionInput): Message
deleteMessage(input: DeleteMessageInput!, condition: TableMessageConditionInput): Message
}
type Query {
getParticipant(conversation_id: Int!, sub: String!): Participant
listParticipants(
filter: TableParticipantFilterInput,
limit: Int,
orderBy: [OrderByParticipantInput],
nextToken: String
): ParticipantConnection
getConversation(id: Int!): Conversation
listConversations(
filter: TableConversationFilterInput,
limit: Int,
orderBy: [OrderByConversationInput],
nextToken: String
): ConversationConnection
getMessage(id: String!): Message
listMessages(
filter: TableMessageFilterInput,
limit: Int,
orderBy: [OrderByMessageInput],
nextToken: String
): MessageConnection
}
type Subscription {
onCreateParticipant(conversation_id: Int, sub: String, last_read_at: String): Participant
@aws_subscribe(mutations: ["createParticipant"])
onUpdateParticipant(conversation_id: Int, sub: String, last_read_at: String): Participant
@aws_subscribe(mutations: ["updateParticipant"])
onDeleteParticipant(conversation_id: Int, sub: String, last_read_at: String): Participant
@aws_subscribe(mutations: ["deleteParticipant"])
onCreateConversation(id: Int, name: String, created_at: String): Conversation
@aws_subscribe(mutations: ["createConversation"])
onUpdateConversation(id: Int, name: String, created_at: String): Conversation
@aws_subscribe(mutations: ["updateConversation"])
onDeleteConversation(id: Int, name: String, created_at: String): Conversation
@aws_subscribe(mutations: ["deleteConversation"])
onCreateMessage(
id: String,
conversation_id: Int,
sub: String,
body: String,
created_at: String
): Message
@aws_subscribe(mutations: ["createMessage"])
onUpdateMessage(
id: String,
conversation_id: Int,
sub: String,
body: String,
created_at: String
): Message
@aws_subscribe(mutations: ["updateMessage"])
onDeleteMessage(
id: String,
conversation_id: Int,
sub: String,
body: String,
created_at: String
): Message
@aws_subscribe(mutations: ["deleteMessage"])
}
確認 2 : リゾルバー
次に、リゾルバーを見てみます。以下は createMessage
のリゾルバーコードです。
JavaScript で書かれているので読みやすいですね。
import { util } from '@aws-appsync/utils';
import { insert, createPgStatement, toJsonObject } from '@aws-appsync/utils/rds';
/**
* Puts an item into the messages table using the supplied input.
* @param {import('@aws-appsync/utils').Context} ctx the context
* @returns {*} the request
*/
export function request(ctx) {
const { input } = ctx.args;
const insertStatement = insert({
table: 'messages',
values: input,
returning: '*',
});
return createPgStatement(insertStatement)
}
/**
* Returns the result or throws an error if the operation failed.
* @param {import('@aws-appsync/utils').Context} ctx the context
* @returns {*} the result
*/
export function response(ctx) {
const { error, result } = ctx;
if (error) {
return util.appendError(
error.message,
error.type,
result
)
}
return toJsonObject(result)[0][0]
}
確認 3 : GraphQL スキーマの作成タイミング
最後に AWS AppSync の GraphQL スキーマ作成のタイミングを確認してみました。
conversations
テーブルに status
というカラムを追加してみました。
ALTER TABLE conversations ADD COLUMN status VARCHAR(100);
これで、 AWS AppSync コンソール側でスキーマの反映ができれば...と思いましたが、現時点 (2024 年 10 月 2 日) 時点ではスキーマの反映は GraphQL API を新規作成するタイミングだけでした。作成された schema.graphql
に手を加えている場合 (例えば、別のデータソースを接続した、など)、上書きされてしまうリスクもあるので、あまり必要ではないのかなと思いました。ここはぜひ色んな方の意見も伺いたいです。
まとめ
今回は、AWS AppSync の RDS Data API を使用して Amazon Aurora Serverless v2 (MySQL) クラスター内のデータベースに対してイントロスペクションを行うことで、検出したテーブルに適合した GraphQL API のインターフェイスを作成する機能を試してみました。遂に、Aurora の任意のエンジン、クラスタータイプで RDS Data API を使って GraphQL API を爆速構築できるようになったことは嬉しいですね。