
Amplify Data(Gen2)からMySQL接続を試してみた
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
NTT東日本の中村です。
Amplify Gen2では、プレビュー版では行えなかったAmplify DataでのMySQL接続ができるようになり、調査を行ってみました。
Amplify Data(Gen2)のMySQL接続の概要
以前のAmplifyは、バックエンドのデータベースと接続する場合、Amplify APIというカテゴリで、AppSyncサービスのデータソースとして既存のMySQL、PostgreSQLを選択できるようになっていました。 Amplify Gen2では、Amplify Dataという名前に変わりましたが、同じ接続ができます。プレビューでは既存データベースに接続できなかったのですが、正式にサポートされました。
Gen1(V6)のドキュメント:
Gen2のドキュメント:
実際に試してみた
MySQLとの接続を試してみました。
MySQLデータベースの作成
この機能は、「既存のMySQLから構成をインポートする機能」なので、先にMySQLデータベースとテーブルを用意します。
今回はカスタムリソースでRDSを作成しています。 検証のため、リスクのあるPublic SubnetにRDSを設置しています。AWSサービスに拘らず、MySQLサーバであれば問題無く接続できます。
import { defineBackend } from "@aws-amplify/backend";
import { auth } from "./auth/resource";
import { data } from "./data/resource";
import { storage } from "./storage/resource";
import {
Credentials,
DatabaseInstance,
DatabaseInstanceEngine,
MysqlEngineVersion,
} from "aws-cdk-lib/aws-rds";
import {
InstanceClass,
InstanceSize,
InstanceType,
Peer,
Port,
SecurityGroup,
SubnetType,
Vpc,
} from "aws-cdk-lib/aws-ec2";
const backend = defineBackend({
storage,
auth,
data,
});
const vpc = new Vpc("SubStack", "ampxVPC", {
maxAzs: 2,
subnetConfiguration: [
{
cidrMask: 24,
name: "public",
subnetType: SubnetType.PUBLIC,
},
],
});
const securityGroup = new SecurityGroup("SubStack", "RDSSecurityGroup", {
vpc,
allowAllOutbound: true,
});
securityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(3306));
securityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(443));
const rdsInstance = new DatabaseInstance("SubStack", "RDSInstance", {
engine: DatabaseInstanceEngine.mysql({
version: MysqlEngineVersion.VER_8_0_36,
}),
credentials: Credentials.fromGeneratedSecret("admin"),
instanceType: InstanceType.of(InstanceClass.BURSTABLE3, InstanceSize.SMALL),
vpc,
vpcSubnets: {
subnetType: SubnetType.PUBLIC,
},
securityGroups: [securityGroup],
databaseName: "ampxdb",
publiclyAccessible: true,
});
backend.addOutput({
custom: {
value: rdsInstance.dbInstanceEndpointAddress,
},
});
Amplify DataがMySQLと接続する場合、内部ではAppSyncが動いており、リゾルバからLambdaを経由してMySQLに接続する仕組みです。 加えて、今回のMySQLはAWS VPC上のRDSに接続するため、LambdaはVPC内に作成されます(いわゆるVPC Lambda)。
既存データベースの接続で生成されるLambdaについては、下記に説明があります。
更に、作成されたLambdaは、Secret Managerに格納されたデータベース接続情報を使用してデータベースにアクセスするので、配置されたセキュリティグループは443ポートの許可が必要です。 CDKでも443ポートを許可しています。
カスタムリソースを作成して、サンドボックスを起動します。
npx ampx sandbox
エンドポイントが出力されました。
データベースの接続情報はSecret Managerに出力されるので、控えておきます。
MySQLクライアントから、データベースに接続できることを確認しました。
テーブルとレコードの作成
テーブルとレコードを作成します。
CREATE TABLE `Team` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`createdAt` datetime DEFAULT NULL,
`updatedAt` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
)
sql> desc team;
+-----------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------+-------------+------+-----+---------+-------+
| id | int | NO | PRI | null | |
| name | varchar(64) | YES | | null | |
| updatedAt | datetime | YES | | null | |
| createdAt | datetime | YES | | null | |
+-----------+-------------+------+-----+---------+-------+
sql> INSERT INTO `ampxdb`.`Team` (`id`, `name`, `createdAt`, `updatedAt`) VALUES ('1', 'teamA', ' 2024-04-01 00:00:00', ' 2024-04-01 00:00:00');
レコードの作成が完了しました。
スキーマの生成
スキーマを生成する為に、接続情報をSandboxのSecretとして設定します。 SQL_CONNECTION_STRINGは、DBユーザ名、ホスト、接続先データベースを繋いで文字列にしたものを使用します。
% npx ampx sandbox secret set SQL_CONNECTION_STRING
? Enter secret value
mysql://admin:hogepass@hogehost.ap-northeast-1.rds.amazonaws.com:3306/ampxdb
接続情報を元に、ampx generate schema-from-databaseを実行しますが、エラーが出て生成に失敗しました。SecretManagerのアクセスに失敗しているようです。
% npx ampx generate schema-from-database --connection-uri-secret SQL_CONNECTION_STRING --out amplify/data/schema.sql.ts --stack amplify-nextamplifygen2-38ampx-sandbox-72d9a45c5b --debug
TypeError [ERR_INVALID_ARG_TYPE]: The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received undefined
[DEBUG] 2024-05-21T05:40:23.753Z: TypeError [ERR_INVALID_ARG_TYPE]: The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received undefined
at new NodeError (node:internal/errors:405:5)
at Hash.update (node:internal/crypto/hash:107:11)
at getHash (/Users/cnw/pj/next-amplify-gen2/node_modules/@aws-amplify/platform-core/lib/backend_identifier_conversions.js:80:10)
at BackendIdentifierConversions.toStackName (/Users/cnw/pj/next-amplify-gen2/node_modules/@aws-amplify/platform-core/lib/backend_identifier_conversions.js:53:22)
at getBackendIdentifierPathPart (/Users/cnw/pj/next-amplify-gen2/node_modules/@aws-amplify/platform-core/lib/parameter_path_conversions.js:52:176)
at getBackendParameterPrefix (/Users/cnw/pj/next-amplify-gen2/node_modules/@aws-amplify/platform-core/lib/parameter_path_conversions.js:45:24)
at getBackendParameterFullPath (/Users/cnw/pj/next-amplify-gen2/node_modules/@aws-amplify/platform-core/lib/parameter_path_conversions.js:65:15)
at ParameterPathConversions.toParameterFullPath (/Users/cnw/pj/next-amplify-gen2/node_modules/@aws-amplify/platform-core/lib/parameter_path_conversions.js:30:20)
at SSMSecretClient.getSecret (file:///Users/cnw/pj/next-amplify-gen2/node_modules/@aws-amplify/backend-secret/lib/ssm_secret.js:19:47)
at Object.handler (file:///Users/cnw/pj/next-amplify-gen2/node_modules/@aws-amplify/backend-cli/lib/commands/generate/schema-from-database/generate_schema_command.js:40:61)
一度だけ生成を行えれば良いので、今回はnode_modulesを書き換えてしまいました。 getSecretを呼ばず、直接SQL_CONNECTION_STRINGの値を割り当てています。 (ここは原因が分かり次第、更新します)
31: handler = async (args) => {
32: const backendIdentifier = await this.backendIdentifierResolver.resolve(args);
33: if (!backendIdentifier) {
34: throw new AmplifyFault('BackendIdentifierFault', {
35: message: 'Could not resolve the backend identifier',
36: });
37: }
38: const outputFile = args.out;
39: await this.schemaGenerator.generate({
40: connectionUri: {
41: secretName:"SQL_CONNECTION_STRING",
42: value: "mysql://admin:hogepass@hogehost.ap-northeast-1.rds.amazonaws.com:3306/ampxdb"
43: },
44: out: outputFile,
45: });
46: };
スキーマの生成に成功しました。
% npx ampx generate schema-from-database --connection-uri-secret SQL_CONNECTION_STRING --out amplify/data/schema.sql.ts --stack amplify-nextamplifygen2-38ampx-sandbox-72d9a45c5b --debug
✔ Successfully fetched the database schema.
The host you provided is for an RDS instance. Consider using an RDS Proxy as your data source instead.
See the documentation for a discussion of how an RDS proxy can help you scale your application more effectively.
schema.sql.tsを見ると、RDSに接続するためのSecretと、VPCの設定、Mysqlから生成したスキーマがあります。
/* eslint-disable */
/* THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. */
import { a } from "@aws-amplify/data-schema";
import { configure } from "@aws-amplify/data-schema/internals";
import { secret } from "@aws-amplify/backend";
export const schema = configure({
database: {
identifier: "IDMTBXNvNs0ACioBPbtDpBBg",
engine: "mysql",
connectionUri: secret("SQL_CONNECTION_STRING"),
vpcConfig: {
vpcId: "vpc-0864eb9f9a79c9117",
securityGroupIds: [
"sg-02b414f3c026fb280"
],
subnetAvailabilityZones: [
{
subnetId: "subnet-0071bd35ddef18ce1",
availabilityZone: "ap-northeast-1c"
},
{
subnetId: "subnet-068e4b4221ed25cfe",
availabilityZone: "ap-northeast-1a"
}
]
}
}
}).schema({
"Team": a.model({
id: a.integer().required(),
name: a.string(),
createdAt: a.datetime(),
updatedAt: a.datetime()
}).identifier([
"id"
])
});
既存のスキーマと合わせて、defineDataを設定します。 combinedSchemaの名前の通り、AppSyncにDynamoDBとMySQLの2つのデータソースが設定されています。
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";
import { schema as baseSqlSchema } from "./schema.sql";
const schema = a.schema({
Todo: a
.model({
tag: a.string().required(),
content: a.string(),
done: a.boolean(),
createDate: a.string().required(),
priority: a.enum(["low", "medium", "high"]),
})
.identifier(["tag", "createDate"])
.authorization((allow) => [allow.owner()]),
});
const sqlSchema = baseSqlSchema.authorization((allow) => allow.authenticated());
const combinedSchema = a.combine([sqlSchema, schema]);
export type Schema = ClientSchema<typeof combinedSchema>;
export const data = defineData({
schema: combinedSchema,
authorizationModes: {
defaultAuthorizationMode: "userPool",
},
});
フロント部分です。use clientで作成しています。
"use client";
import { generateClient } from 'aws-amplify/data';
import { type Schema } from '@/amplify/data/resource';
import "@aws-amplify/ui-react/styles.css";
import { useEffect, useState } from "react";
const client = generateClient<Schema>();
type Team = Schema['Team']['type'];
function App({
searchParams,
}: {
searchParams?: { [key: string]: string };
}) {
const [teamList, setTeamList] = useState<Team[]>([]);
async function addTeam(data: FormData) {
const dataStr = new Date().toISOString()
const response = await client.models.Team.create({
id: "2",
name: data.get("title") as string,
createdAt: dataStr,
updatedAt: dataStr,
});
setTeamList([...teamList, response.data!])
}
useEffect(() => {
(async () => {
const response: any = await client.models.Team.list();
setTeamList(response.data!);
})();
}, []);
console.log(teamList)
return (
<>
<h1>Hello, Amplify 👋</h1>
<form action={addTeam}>
<input
type="text"
name="title"
/>
<button type="submit">Add Team</button>
</form>
<ul>
{teamList.map((team) => (
<li key={team!.name}>
<a>{team!.name}</a>
</li>
))}
</ul>
</>
);
}
export default App;
レコードの表示
MySQLに保存されたデータを表示することができました。
レコードの格納
レコードの格納ですが、DynamoDBがデータソースの時と同様、model.createを使用して値を格納します。 今回のTeamsテーブルは、idはInt型であり、Auto Incrementが付与されたプライマリーキーなので、idを省略したデータ登録が可能か試しましたが、エラーが出てしまいました。明示的に値を指定する必要があります。当然PKなので一意の値を指定するべきですが、今回は1レコードだけ登録を行いたく、2を指定しています。 かつ、idの入力の型としてはstringを求められるので、数字の2ではなく、文字列の2を入れることになりました。将来的に型の統一がされると思われます。
また、createdAtや、updatedAtも省略できなかったため、明示的に投入する必要がありました。 パイプラインリゾルバを使えば自動化できるようにも思えますが・・・
const response = await client.models.Team.create({
id: "2",
name: data.get("title") as string,
createdAt: dataStr,
updatedAt: dataStr,
});
MySQLのデータの登録も確認できました。
まとめ
Amplify Data(Gen2)では、AppSyncのデータソースに、DynamoDBだけではなく、既存のMySQLやPostgreSQLも接続できるようになりました。 型指定やデータ登録周りなど、やや荒削りな印象ですが、RDBMSで構築を行いたい場合の良い選択肢になると思いました。