1
1

Aurora MySQL が RDS Data API を遂にサポート!AppSync で爆速 GraphQL API を作ってみた

Last updated at Posted at 2024-10-02

はじめに

本記事は 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 クラスターの作成と接続」を参考にしてください。

注意
エンジンバージョンがデフォルトではAurora MySQL 3.05.2 (compatible with MySQL 8.0.32) - メジャーバージョンのデフォルト 8.0 になっているので、Aurora MySQL 3.07.0 (compatible with MySQL 8.0.36) に変える必要がある点だけ注意してください。
screenshot 8.png

また、DB クラスター作成時に、以下のようなチェックボックスができるのでチェックを入れます。忘れてしまった場合もあとから有効化は可能です。

screenshot 7.png

認証情報は AWS Secret Manager で管理します。後ほど、認証情報は AWS AppSync からイントロスペクションを実施する際にも利用します。

screenshot 9.png

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」を選択し、次に進みます。

screenshot 4.png

次に、API 名にわかり易い名前を入力します。今回はデフォルトの「My AppSync API」としました。

screenshot 5.png

次にデータベースを選択します。

スクリーンショット 2023-12-22 16.13.17.png

「データベースを選択」をクリックすると接続したい Amazon Aurora クラスターが選択出来ます。データベースには「sample_db」と入力します。AWS Secret Manager シークレットは Step1 で作成済みのものを選択します。入力が終えたら「インポート」をクリックします。

image.png

「インポート」をクリックするとイントロスペクションが開始され、下の画像のようにデータベースのテーブルがインポートされました。タイプ名はカスタマイズすることができるので、以下のように変更します。(画像は変更前です。)

  • Conversation_participants : Participant
  • Conversations : Conversation
  • Messages : Message

問題なければ、「次へ」をクリックします。

screenshot 13.png

スキーマの設定では、クエリのみを作成するか、クエリ、ミューテーション、サブスクリプションを作成するかを選択することが出来ます。今回は、「すべてのモデルに対してクエリ、ミューテーション、サブスクリプションを作成」を選択します。これで GraphQL API の作成は完了です!

スクリーンショット 2023-12-22 13.31.31.png

Step4. 動作確認

では、実際にどんなスキーマやリゾルバーが作成されたか見てみましょう。まずはスキーマを見てみましょう。

確認 1 : GraphQL スキーマ

以下が自動で生成されたスキーマです。Aurora PostgreSQL で試したときと同様にスキーマが正しく作成できていることが確認できました。

schema.graphql
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 を爆速構築できるようになったことは嬉しいですね。

1
1
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
1
1