1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[AWS Amplify] Sandbox環境にテストデータを簡単に作成できるSeed機能が追加されたので使ってみる

Posted at

はじめに

2025/4/29にAWS AmplifyのSandbox環境へテストデータを作成することが出来るSeed機能が追加されましたので、試しに使ってみました。

AWS Amplifyとは

AWS Amplifyは、開発者がフルスタックアプリケーションを効率的に開発するための統合プラットフォームとなるサービスです。

特徴

迅速な開発プロセスを提供

開発者は、個々のAWSサービスを深く理解する必要なく、フロントエンドとバックエンドの両方を迅速に開発できます。

幅広い機能を提供

先ほど統合プラットフォームと記載した通り、AWS Amplifyはアプリケーションの構築や開発からホスティングまで、包括的な機能を提供してくれます。

つまり...

AWS Amplifyを使えばサクッとAWS環境にWebサービスをバックエンドの深い知識が無くとも構築できちゃうぜ。というサービスです。

Snadbox環境とは

Sandbox環境は、Amplifyが提供する開発者の単位で隔離された検証環境です。
以下の通り、1つのAWSアカウントの中に開発者毎の環境を共存させることができます。

image.png
参考:https://docs.amplify.aws/react/deploy-and-host/sandbox-environments/setup/

もう一つのメリットとして、Amplifyのコードを書き替えて保存すると、随時、Sandbox環境に反映されます。(ホットリロードのようなものです)

以下のコマンド実行だけでSnadbox環境が構築できます。

npx ampx sandbox

利用したら忘れずにSnadboxを削除します。
SandboxといえどAWS環境にサービスを構築するので、ほっておくと料金が発生します。

npx ampx sandbox delete

Sandbox Seedとは

Sandbox Seedは、TypeScriptで記述したスクリプトを使用して、Sandbox環境にテストデータを簡単に作成できる機能です。Amplifyの記法をそのまま利用できるため、開発者はAmplify実装の延長線でテストデータも作れるようになったと感じています。

使用する際は@aws-amplify/seedをインストールして使用します。

npm install @aws-amplify/seed

出来ること

下記、AWS Amplifyで構築できるAWSサービスへのテストデータ追加が出来ます。

  • Amazon Cognito
  • Amazon DynamoDB
  • Amazon S3

利用できるAPI一覧

@aws-amplify/seedから利用できるAPIは以下になります。

No 名前 説明
1 createAndSignUpUser 新しいユーザーを作成し、サインアップします。
2 addToUserGroup ユーザーを指定されたグループに追加します。
3 signInUser 指定したユーザーでサインインします。
4 getSecret 指定されたシークレット名のシークレットを取得します。
5 setSecret 指定されたシークレット名とシークレット値を設定します。

やってみる

例えば、下記のようなTODOアプリをAWS mplifyで開発していると仮定して、TODOのリストに表示するためのデータを用意します。

image.png

準備:TODOアプリの用意

詳細はこちら

フロントエンドのプロジェクトを作成する

フロントエンド側の環境として、Viteを使用し、 React と TypeScript のプロジェクトを作成します。

npm create vite@latest
√ Project name: ... amplify_sandbox_seed_app
√ Select a framework: » React
√ Select a variant: » TypeScript

プロジェクトディレクトリに移動し、必要な依存パッケージをインストールします。

cd <プロジェクト名>
npm install

AWS Amplifyの環境を作成する

AWS Amplifyの環境を下記のコマンドで作成します。

npm create amplify@latest

AWS Amplifyでログイン機能のUIを組み込むため、以下のライブラリを追加します。

@aws-amplify/ui-react

AWS AmplifyのSandbox環境を立ち上げます。

npx ampx sandbox

バックエンドの構築

バックエンドの構築のため下記のファイルを修正します。

Cognitoように下記の修正を行います。

./amplify/auth/resource.ts
import { defineAuth } from '@aws-amplify/backend';

export const auth = defineAuth({
  loginWith: {
    email: true,
  },
  userAttributes: {
    locale: {
      required: false,
      mutable: true,
    },
  },
  groups: ["admin", "user"],
});

DynamoDBとAppSync用に下記の修正を行います。

./amplify/data/resource.ts
import { type ClientSchema, a, defineData } from '@aws-amplify/backend';

const schema = a.schema({
  Todo: a
    .model({
      title: a.string(),
      description: a.string(),
      status: a.string(),
    })
    .authorization((allow) => allow.owner()),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'userPool',
  },
});

フロントエンドの構築

UIに必要なライブラリ達をインストールします。

npm install @mui/icons-material @mui/material @emotion/styled @emotion/react

コードを修正していきます。
※今回、TODOアプリは本題ではないのでGitHub Copilot 作ってもらいました。

バックエンドのリソースにフロントエンドからアクセスするため、Amplifyの設定を行います。

./src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { Amplify } from "aws-amplify";
import outputs from "../amplify_outputs.json";
import './index.css';
import App from './App.tsx';

Amplify.configure(outputs);

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

TODOリストを表示するメインのコンポーネントです。

./src/App.tsx
import { useState, useEffect } from 'react';
import { DataGrid, GridColDef } from '@mui/x-data-grid';
import { Button, Box } from '@mui/material';
import { generateClient } from 'aws-amplify/data';
import { Authenticator } from "@aws-amplify/ui-react";
import type { Schema } from "../amplify/data/resource";
import EditIcon from '@mui/icons-material/Edit';
import AddDialog from './components/AddDialog';
import EditDialog from './components/EditDialog';

import "@aws-amplify/ui-react/styles.css";

const client = generateClient<Schema>();

function Table() {
  const [rows, setRows] = useState<any[]>([]);
  const [newTodo, setNewTodo] = useState({ title: '', description: '', status: '' });
  const [selectedIds, setSelectedIds] = useState<string[]>([]);
  const [openDialog, setOpenDialog] = useState(false);
  const [editingRow, setEditingRow] = useState<any>(null);
  const [openAddDialog, setOpenAddDialog] = useState(false);

  useEffect(() => {
    fetchTodo();
  }, []);

  const fetchTodo = async () => {
    try {
      const { data: items } = await client.models.Todo.list();
      setRows(items.map((item, idx) => ({
        id: item.id,
        no: idx + 1,
        title: item.title || '',
        description: item.description || '',
        status: item.status || ''
      })));
    } catch (error) {
      console.error('Error fetching todos:', error);
    }
  };

  const handleEditClick = (row: any) => {
    setEditingRow(row);
    setOpenDialog(true);
  };

  const handleAddClick = () => {
    setOpenAddDialog(true);
  };

  const handleAddDialogClose = () => {
    setOpenAddDialog(false);
    setNewTodo({ title: '', description: '', status: '' });
  };

  const handleAddDialogSave = async () => {
    try {
      await client.models.Todo.create({
        title: newTodo.title || '',
        description: newTodo.description || '',
        status: newTodo.status || ''
      });
      setRows([...rows, { id: rows.length + 1, no: rows.length + 1, ...newTodo }]);
      setNewTodo({ title: '', description: '', status: '' });
      setOpenAddDialog(false);
      alert('TODOの追加に成功しました!');
    } catch (error) {
      console.error('Error adding todo:', error);
    }
  };

  const handleAddDialogChange = (field: string, value: string) => {
    setNewTodo(prev => ({ ...prev, [field]: value }));
  };

  const deleteSelectedTodos = async () => {
    try {
      await Promise.all(selectedIds.map(id => client.models.Todo.delete({ id })));
      setRows(rows.filter(row => !selectedIds.includes(row.id)));
      setSelectedIds([]);
      alert('TODOの削除に成功しました!');
    } catch (error) {
      console.error('Error deleting selected todos:', error);
    }
  };

  const handleDialogClose = () => {
    setOpenDialog(false);
    setEditingRow(null);
  };

  const handleDialogSave = () => {
    try {
      if (editingRow) {
        client.models.Todo.update({
          id: editingRow.id,
          title: editingRow.title,
          description: editingRow.description,
          status: editingRow.status
        });
        setRows(prevRows => prevRows.map(row => row.id === editingRow.id ? { ...row, ...editingRow } : row));
      }
      setOpenDialog(false);
      setEditingRow(null);
      alert('TODOの更新に成功しました!');
    } catch (error) {
      console.error('Error updating todo:', error);
    }
  };

  const handleEditDialogChange = (field: string, value: string) => {
    setEditingRow((prev: any) => ({ ...prev, [field]: value }));
  };

  const columns: GridColDef[] = [
    { field: 'no', headerName: 'No', width: 90 },
    { field: 'title', headerName: 'タイトル', width: 150 },
    { field: 'status', headerName: 'ステータス', width: 150 },
    { field: 'description', headerName: '説明', width: 300 },
    {
      field: 'edit',
      headerName: '',
      width: 100,
      renderCell: (params) => (
        <Button onClick={() => handleEditClick(params.row)}>
          <EditIcon />
        </Button>
      )
    }
  ];

  return (
    <div>
      <DataGrid
        rows={rows}
        columns={columns}
        checkboxSelection
        onRowSelectionModelChange={(ids) => {
          const selected = Array.from(ids.ids).map(id => id.toString());
          setSelectedIds(selected);
        }}
        initialState={{
          pagination: {
            paginationModel: {
              pageSize: 5,
            },
          },
        }}
        pageSizeOptions={[5]}
        disableRowSelectionOnClick
      />
      <div style={{ marginTop: '16px', display: 'flex', justifyContent: 'flex-end' }}>
        <Button
          variant="contained"
          color="secondary"
          onClick={deleteSelectedTodos}
          style={{ marginRight: '8px' }}
        >
          削除
        </Button>
        <Button
          variant="contained"
          color="primary"
          onClick={handleAddClick}
        >
          追加
        </Button>
      </div>
      <AddDialog
        open={openAddDialog}
        newTodo={newTodo}
        onClose={handleAddDialogClose}
        onSave={handleAddDialogSave}
        onChange={handleAddDialogChange}
      />
      <EditDialog
        open={openDialog}
        editingRow={editingRow}
        onClose={handleDialogClose}
        onSave={handleDialogSave}
        onChange={handleEditDialogChange}
      />
    </div>
  );
}

function App() {
  return (
    <Authenticator>
      <Box sx={{ height: 400, width: '100%' }}>
        <Table/>
      </Box>
    </Authenticator>
  );
}

export default App;

TODO追加用のダイアログコンポーネントです。

./src/components/AddDialog.tsx
import React from 'react';
import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, Button, MenuItem } from '@mui/material';

interface AddDialogProps {
  open: boolean;
  newTodo: { title: string; description: string; status: string };
  onClose: () => void;
  onSave: () => void;
  onChange: (field: string, value: string) => void;
}

const AddDialog: React.FC<AddDialogProps> = ({ open, newTodo, onClose, onSave, onChange }) => {
  return (
    <Dialog open={open} onClose={onClose}>
      <DialogTitle>TODOを追加</DialogTitle>
      <DialogContent>
        <TextField
          label="タイトル"
          value={newTodo.title}
          onChange={(e) => onChange('title', e.target.value)}
          fullWidth
          margin="dense"
        />
        <TextField
          label="説明"
          value={newTodo.description}
          onChange={(e) => onChange('description', e.target.value)}
          fullWidth
          margin="dense"
        />
        <TextField
          label="ステータス"
          value={newTodo.status}
          onChange={(e) => onChange('status', e.target.value)}
          select
          fullWidth
          margin="dense"
        >
          {['完了', '未着手', '対応中'].map((option) => (
            <MenuItem key={option} value={option}>
              {option}
            </MenuItem>
          ))}
        </TextField>
      </DialogContent>
      <DialogActions>
        <Button onClick={onClose} color="secondary">キャンセル</Button>
        <Button onClick={onSave} color="primary">保存</Button>
      </DialogActions>
    </Dialog>
  );
};

export default AddDialog;

TODO編集用のダイアログコンポーネントです。

./src/components/Edit.tsx
import React from 'react';
import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, Button, MenuItem } from '@mui/material';

interface EditDialogProps {
  open: boolean;
  editingRow: { id: number; title: string; description: string; status: string } | null;
  onClose: () => void;
  onSave: () => void;
  onChange: (field: string, value: string) => void;
}

const EditDialog: React.FC<EditDialogProps> = ({ open, editingRow, onClose, onSave, onChange }) => {
  return (
    <Dialog open={open} onClose={onClose}>
      <DialogTitle>編集</DialogTitle>
      <DialogContent>
        <TextField
          label="タイトル"
          value={editingRow?.title || ''}
          onChange={(e) => onChange('title', e.target.value)}
          fullWidth
          margin="dense"
        />
        <TextField
          label="説明"
          value={editingRow?.description || ''}
          onChange={(e) => onChange('description', e.target.value)}
          fullWidth
          margin="dense"
        />
        <TextField
          label="ステータス"
          value={editingRow?.status || ''}
          onChange={(e) => onChange('status', e.target.value)}
          select
          fullWidth
          margin="dense"
        >
          {['完了', '未着手', '対応中'].map((option) => (
            <MenuItem key={option} value={option}>
              {option}
            </MenuItem>
          ))}
        </TextField>
      </DialogContent>
      <DialogActions>
        <Button onClick={onClose} color="secondary">キャンセル</Button>
        <Button onClick={onSave} color="primary">保存</Button>
      </DialogActions>
    </Dialog>
  );
};

export default EditDialog;

スクリプトを作成する

Seedを利用するため、必要なライブラリをインストールします。

npm install @aws-amplify/seed @types/node

新規登録するユーザー情報をSnadbox環境にシークレットを追加します。

npx ampx sandbox secret set username
? Enter secret value: ###
Done!

npx ampx sandbox secret set password
? Enter secret value: ###
Done!

参考:https://docs.amplify.aws/react/deploy-and-host/fullstack-branching/secrets-and-vars/#local-environment

seedフォルダを新たに作成し、新規ユーザーとTODOデータを作成するスクリプトを作成します。
Cognito周りの記述はSeedのAPIですが、DynamoDBへのアクセスは従来のAmplifyの記法で書けるので、とっつきやすいです。

./amplify/seed/seed.ts
import { readFile } from "node:fs/promises";
import { Amplify } from "aws-amplify";
import {
  getSecret,
  createAndSignUpUser,
  signInUser,
  addToUserGroup,
} from "@aws-amplify/seed";
import { generateClient } from "aws-amplify/api";
import * as auth from "aws-amplify/auth";
import type { Schema } from "../data/resource";


const url = new URL("../../amplify_outputs.json", import.meta.url);
const outputs = JSON.parse(await readFile(url, { encoding: "utf8" }));
Amplify.configure(outputs);

const dataClient = generateClient<Schema>();


/* Step1: Cognitoへのユーザー登録 */

// シークレットからユーザー名/パスワードを取得
const username = await getSecret("username");
const password = await getSecret("password");

// 新規ユーザー登録
const user = await createAndSignUpUser({
  username: username,
  password: password,
  signInAfterCreation: false, // 自動サインインしない
  signInFlow: "Password", // パスワードによる認証フロー
  userAttributes: {
    locale: "ja",
  },
});

// サインアップしたユーザーをグループに登録
await addToUserGroup(user, "user");


/* Step2: DynamoDBのテーブルへのTODO登録 */

// サインイン
await signInUser({
  username: username,
  password: password,
  signInFlow: "Password",
});

const todos = [
  { title: "夕飯の下位だし", description: "初めてのTODO", status: "未完了" },
  { title: "買い物リスト作成", description: "週末の買い物", status: "未完了" },
  { title: "メール返信", description: "クライアントへの返信", status: "未完了" },
  { title: "会議準備", description: "資料作成", status: "未完了" },
  { title: "運動", description: "30分ランニング", status: "未完了" },
  { title: "読書", description: "技術書を1章読む", status: "未完了" },
  { title: "コードレビュー", description: "チームメンバーのPR確認", status: "未完了" },
  { title: "家計簿記入", description: "今週の支出を記録", status: "未完了" },
  { title: "掃除", description: "リビングの掃除", status: "未完了" },
  { title: "料理", description: "新しいレシピに挑戦", status: "未完了" },
];

// TODOをDynamoDBに登録
for (const todo of todos) {
  const response = await dataClient.models.Todo.create(todo, {
    authMode: "userPool",
  });
  if (response.errors && response.errors.length > 0) {
    throw response.errors;
  }
}

// サインアウト
auth.signOut();

createAndSignUpUserの入力パラメータ補足
No 入力パラメータ 必須/任意 説明
1 username 必須 ユーザー名
2 signInFlow 必須 サインインのフローの種類
"Password" 、 "MFA"のいずれか
3 password 必須 パスワード
(signInFlow="Password"の場合)
4 signInAfterCreation 必須 ユーザー作成後に自動的にサインインするか
5 userAttributes 任意 ユーザーの標準的な属性
(名前、メールアドレス、電話番号など)
6 mfaPreference 任意 MFAの優先したい設定
"EMAIL"、"SMS"、"TOTP"のいずれか
(signInFlow="MFA"の場合)
7 emailSignUpChallenge 任意 メールによるサインアップチャレンジを行う関数
(signInFlow="MFA"の場合)
8 smsSignUpChallenge 任意 SMSによるサインアップチャレンジを行う関数
(signInFlow="MFA"の場合)
9 totpSignUpChallenge 任意 TOTPによるサインアップチャレンジを行う関数
(signInFlow="MFA"の場合)
addToUserGroupの入力パラメータ補足

No 入力パラメータ 必須/任意 説明
1 user 必須 登録したいユーザー
2 group 必須 登録先のグループ

参考:https://github.com/aws-amplify/amplify-backend/blob/main/packages/seed/src/types.ts#L3

最後に下記のコマンドでスクリプトを実行します。

npx ampx sandbox seed

結果を確認する

AWSマネージドコンソールから確認する

Amazon Cognitoに新規ユーザーが登録されたか確認します。
追加されていますね。ただメールアドレスは未検証の状態です。
image.png

グループにも入っています。
image.png

Amazon DynamoDBにTODOが追加されているか確認します。
確かに10件登録されています。
image.png

TOTOアプリの方でも確認する

作成したユーザーでログインをしてみます。
image.png

メールアドレスを検証していないので、検証するよう促されました。
image.png

検証を済ませると、TODOリストに先ほど作成したテストデータが表示されました。
image.png

今度から作成したスクリプトを流せば、一発でテスト環境をいつでも再現できそうです。

まとめ

今回、AWS Amplify のSandbox環境にテストデータを作成できるSeed機能を使ってみました。

従来は、AWS Amplify以外の方法(AWS CLI、SDKなど)でスクリプトを組んで、テストデータを作成する必要がありましたが、今回からAWS Amplifyで完結できるようになりました。

開発がより迅速になる

AWS Amplifyは、サービス構築の迅速さが求められるケースで使用されることが多いと思います。
Cognitoのデータ作成時はSeedのAPIを使用する必要がありますが、S3やDynamoDBについてはAmplifyの記法をそのまま使用できるため、学習コストを抑えることができます。
なので、Amplify内で完結することで、学習コストが下がり、より迅速な構築が可能になったと思います。

Snadbox環境をより便利に使える

Sandbox環境は使用時に料金が発生するため、都度、作り直すことが多くなります。本機能を使えば、テストデータを効率的に作成でき、Sandbox環境を容易に再現できる点も便利だなと感じました。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?