開発チームがお届けするブログリレーです!既に公開されている記事もありますので、こちらから他のメンバーの投稿もぜひチェックしてみてください!

AWS Amplify とは

AWS Amplify (以下、Amplify)は、サーバーレスなWeb・モバイルアプリケーションの開発・運用を効率化するフレームワークです。
特にアプリケーションを作る上での初期構築が高速で行える点が魅力です。

Amplifyが提供している機能例

  • 認証
  • API
  • データベース
  • ホスティング
  • ストレージ
  • 関数

Amplifyを使うことで、Amazon Cognito を用いた認証機能や AWS AppSync でのAPI構築が素早く行うことができます。
Web・モバイルアプリケーションの開発を前提にされているため、アプリケーションからAPI接続や認証処理の追加がアプリケーション側でも簡単に行えます。

Amplify Gen1 と Gen2

Amplify には Gen1 と Gen2 の2つのバージョンがあります。
公式ドキュメントでも2つのバージョンが切り替えられるようになっています。

Gen1はAWSコンソール上から作成できるAmplify StudioかAmplify CLIを使用して、リソースを作成することができました。
これも手軽で便利ではあったのですが、2024年5月にGAされた Gen2 になるとTypeScriptで定義することでリソースを作成できるようになり、より開発者に便利なサービスに変化しました。

Gen2がリリースされている今でも、Amplifyのコンソール上からGen1のアプリケーションを立ち上げることが可能です。

注意:新しくAmplifyアプリケーションを作成する場合は、Gen2の利用が推奨されています。

Gen1も便利だったが、課題も多くあった

Gen1の時でも、アプリケーションの初期立ち上げの速さに感動しました。
またFigmaとのコラボレーションも、Figmaからフロントエンド側のコードを生成してくれる画期的な機能の一つでした。

プレビューという、プルリクエスト(ブランチ)単位での動作確認用の環境を作成してくれる機能もあります。
このドキュメントにある通り、マージ先のブランチのプレビューを有効にしておくと、GitHubのプルリクエストの画面からプレビューが確認できるようになります。

このようにGithubのリポジトリと紐づけておくと、自動でビルドされプレビューのURLが生成されます。

しかし便利な反面、開発者にとって様々な課題がありました。
以下、私がGen1で構築している時に感じた課題です。

1. 開発速度
Gen1の場合、例えばAppSyncのAPIを修正し、その動作確認をするためには一度デプロイする必要がありました。
これは少し調整したいだけでもデプロイをすることになり、バックエンドの開発が億劫になる原因になっていました。さらにGen1では一部しか変更していなくても全てがビルドされるため、よりデプロイに時間がかかりました。

2. Amplify CLI のエラーがわかりづらい
Gen1のデプロイはAmplify CLIで行われます。ローカルからデプロイに失敗した場合のエラーが分かりづらく、実際に何が問題でエラーが発生しているのか調査に時間がかかることがありました。

3. 少し要件が加わると、変更が辛い
Gen1では、エスケープハッチ的な部分が不足していました。
Amplifyで作成したリソースにセキュリティや認証の都合上、設定を追加したい場合にCLIから作成することができず、自分でオーバーライドする記述をJSONやTypeScriptなどを利用して記述する必要がありました。

また Gen1 では独自のファイルが生成されており、どのファイルがどの設定値になっているのかわかりにくい点がありました。
以下の画像のように、バックエンドリソースを作成するJSONファイルが複数あり、修正する際には既存の構成を壊さないように、どのように作られているのかファイルの役割を把握する必要がありました。
(中には修正しない方が良いファイルもあります)

Gen2になって嬉しいこと

バックエンドのTypeScript記述
Gen2からはAmazon DynamoDBやAWS AppSyncなどの作成をTypeScriptで記述できるようになりました。
以下にGen1とGen2の書き方を載せていますが、どちらも同じリソースが作成されます。

Gen1:schema.graphql

type Chat @model @auth(rules: [{ allow: owner }]) {
  id: ID!
  name: String
  message: [Message] @hasMany
}

type Message @model @auth(rules: [{ allow: owner }]) {
  id: ID!
  text: String
  chatId: ID! @index
  chat: Chat @belongsTo(fields: ["chatId"])
}

Gen2:data/resource.ts

const schema = a.schema({
  Chat: a.model({
    name: a.string(),
    message: a.hasMany('Message', 'chatId'),
  }),
  Message: a.model({
    text: a.string(),
    chat: a.belongsTo('Chat', 'chatId'),
    chatId: a.id().required()
  }),
}).authorization((allow) => allow.owner());

GraphQLの書き方に慣れていればGen1の記述でも問題ないですが、補完が効かず記法が間違えていたとしてもデプロイしてみないと間違いに気づけませんでした。
それに比べてGen2ではTypeScriptにより補完を使って効率的に開発することが可能になりました。

間違えている例:

// これは記法として間違い
type Chat @model @auth(rules: [ { allow: owner } ]) {
  id: ID!
  name: String
  message: [Message] @hasMany()
}

type Message @model @auth(rules: [ { allow: owner } ]) {
  id: ID!
  text: String
  chatId: ID! @index
  chat: Chat @belongsTo(fields: ["chatId"])
}

デプロイすると、このようなエラーが出ます。

# どこでNameが必要なのか分かりづらい(正解はこの書き方なら@hasManyにNameが必要)
% amplify push
✖ There was an error pulling the backend environment xxxx.
🛑 Syntax Error: Expected Name, found ")".

Learn more at: https://docs.amplify.aws/cli/project/troubleshooting/

Session Identifier: xxxxx

これが改善されたことは、Amplifyユーザーとしてとても嬉しいです。

Sandbox機能
先に記述した通り Gen1 でも、Sandboxという用語は使われていました。
しかしこの機能はAmplify CLIでバックエンドの環境を自分で追加することで、別のバックエンド環境を作れる機能でした。
そのため開発する度に明示的にバックエンド環境の切り替えを行う必要があり、やや面倒でした。

AWSコンソールから「サンドボックスを管理」に遷移しても、作成したサンドボックスは表示されていません。これを見てもGen1のサンドボックスは、あくまでもバックエンド環境の一つでしか無いことが分かります。

Gen2になると、npx ampx sandbox というコマンドを実行することで、1開発者につき1サンドボックスが作成でき、ホットリロードが行われながらバックエンド開発が行えます。
それぞれにバックエンドもフロントエンドも構築されるため、複数人での開発がより簡単に出来るようになりました。

https://docs.amplify.aws/react/how-amplify-works/concepts/

チュートリアルを通して Amplify Gen2 を知る

Gen1 と Gen2 の違いについて理解できてきたので、Amplify Gen2のチュートリアルを少しなぞることで機能理解を深めてみます。
チュートリアルはこちらに記載されています。
※本記事では一部のみ紹介しているので、気になる方は是非全て試してみてください。

1.Amplifyと接続するリポジトリを用意する

ドキュメントに沿うと、テンプレートリポジトリを元に新しいリポジトリを作成する画面に遷移します。
これはamplify-vite-react-templateを元に作成されています。
必要な記述を行い、リポジトリを作成します。

2.AWSコンソール上からデプロイ

AWSのAmplifyコンソールから「新しいアプリを作成」を選択し、GitHubを選択します。

3.リポジトリ選択

リポジトリを選択しようとするとGitHubの認証が必要になるので、完了させます。
その後、1で作成したリポジトリを選択します。
ブランチは「main」、「私のアプリケーションはモノレポです」のチェックボックスは外した状態で次へ進みます。

4.アプリケーションの設定・デプロイ

アプリケーションの設定に遷移しますが、ここは何も変更せず次へ進みます。

最後の確認画面に遷移するので、「保存してデプロイ」を押します。

デプロイが開始しますが、完了するまで数分待ちます。

5.amplify_outputs.jsonのダウンロード

デプロイが完了したら、対象のブランチを選択します。

デプロイされたバックエンドリソースからamplify_outputs.jsonをダウンロードします。
これはGen1で使われていたteam-provider-info.jsonと同じ役割を持つ、バックエンド側の設定が書かれているものです。
これをローカルのリポジトリに配置することで、そのバックエンド環境を元にローカル開発が可能になります。

作成したリポジトリをクローンします。

git clone https://github.com/<github-user>/amplify-vite-react-template.git
cd amplify-vite-react-template && npm install

ダウンロードしたamplify_outputs.jsonは、プロジェクトのルートに配置します。

6.ローカル起動

チュートリアルでは機能追加してから起動していますが、最初にどのようなアプリケーションなのかみておきます。

npm run dev

シンプルなTODOアプリケーションが表示されます。

7.削除機能追加

作りたいアプリケーションが分かったので、チュートリアル通り削除機能を実装します。
初期の状態では、作成と取得のみ実装されています。

削除用の関数とイベントハンドラーを追加します。

// App.tsx
import { useEffect, useState } from "react";
import type { Schema } from "../amplify/data/resource";
import { generateClient } from "aws-amplify/data";

const client = generateClient<Schema>();

function App() {
  const [todos, setTodos] = useState<Array<Schema["Todo"]["type"]>>([]);

  useEffect(() => {
    client.models.Todo.observeQuery().subscribe({
      next: (data) => setTodos([...data.items]),
    });
  }, []);

  function createTodo() {
    client.models.Todo.create({ content: window.prompt("Todo content") });
  }

  // 追加
  function deleteTodo(id: string) {
    client.models.Todo.delete({ id })
  }

  return (
    <main>
      <h1>My todos</h1>
      <button onClick={createTodo}>+ new</button>
      <ul>
        {todos.map((todo) => (
          // 追加
          <li onClick={() => deleteTodo(todo.id)} key={todo.id}>{todo.content}</li>
        ))}
      </ul>
      <div>
        🥳 App successfully hosted. Try creating a new todo.
        <br />
        <a href="https://docs.amplify.aws/react/start/quickstart/#make-frontend-updates">
          Review next step of this tutorial.
        </a>
      </div>
    </main>
  );
}

export default App;

これで削除が実装できましたが、注目すべきは削除の関数内です。
作成の関数でも同じ記載がされていますが、client.models.TodoでTodoモデルの操作が行えるようになっています。かなり分かりやすいです。
このTodoモデルの参照がどこから来ているか辿ると、import部分で../amplify/data/resourceにたどり着きます。

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

/*== STEP 1 ===============================================================
The section below creates a Todo database table with a "content" field. Try
adding a new "isDone" field as a boolean. The authorization rule below
specifies that any user authenticated via an API key can "create", "read",
"update", and "delete" any "Todo" records.
=========================================================================*/
const schema = a.schema({
  Todo: a
    .model({
      content: a.string(),
    })
    .authorization((allow) => [allow.publicApiKey()]),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: "apiKey",
    // API Key is used for a.allow.public() rules
    apiKeyAuthorizationMode: {
      expiresInDays: 30,
    },
  },
});

上記がバックエンドのAppSyncとDynamoDBを作成している記述です。
スキーマ定義のTodoは、バックエンドのリソース作成だけでなく、フロントエンド側からのAPI呼び出しにも使用されています。これにより一貫したアプリケーション開発が実現できるようになっています。

チュートリアル自体はもう少し長いので、気になる方は試してみてください。
ただ、ここまででもかなり開発しやすい環境になっていることがわかると思います。

まとめ

Gen1と比較しながらAmplify Gen2の魅力について記載しました。
Amplify Gen1からGen2への移行ツールはまだ開発中であり、Gen1のプロジェクトはそのまま使うことが推奨されていますが、もしGen2を使う際には参考になると嬉しいです。

参考