LambdaもDynamoDBもコードから実装可能?AWS Amplify Gen2を使ってみた

普段のお仕事に役立つ普遍的なプログラミングTIPSや、業界で注目度が高い最新情報をお届けする「編集部ピックアップ」。
今回は、AWSの豊富なフルスタックサービスをフロントエンド開発者が手軽に使えるようにしたサービス「AWS Amplify」の最新バージョン、「Amplify Gen2」をご紹介します。

はじめに

AWS(Amazon Web Services)は、世界最大手のクラウドベンダーであり、コンピューティング、ストレージ、データベースなど多彩な機能を提供しています。そのスケーラビリティと柔軟性は、多くの企業や開発者にとって大きな魅力となっています。

一方で、提供されているサービスの数が非常に多いため、コード実装を専門とする開発者にとっては、最適な組み合わせや使い方を見極めるには一定の経験が求められる場面もあり、クラウド活用の入り口に“壁”を感じることもあります。

こうした「壁」を取り除き、AWSの強力なサービスをフロントエンド開発者にも扱いやすくするために登場したのが「AWS Amplify(アンプリファイ)」です。

今回の記事では、最新バージョン「Amplify Gen2(ジェネレーション2)」のクイックスタートを利用して、その機能についてご紹介します。

AWS Amplify Gen2とは

AWS Amplify公式サイト

AWS Amplify Gen2は、フロントエンド開発者向けに設計された、TypeScriptでクラウドバックエンドをコード定義し、フロントエンドと統合できる、フルスタック開発を可能にするプラットフォームです。

従来のCLIやGUI操作が中心だったGen1と異なり、Gen2ではクラウドの構成やリソース(Lambda関数、DynamoDB認証(Cognito)など)をTypeScriptコードで定義・管理できます。

また、Gitブランチごとの自動デプロイやサンドボックス環境もサポートされており、開発・テスト・運用を効率化できます。これにより、AWSに不慣れなフロントエンド開発者でも、型安全かつモダンなWebアプリ開発が手軽に行えるようになります。

Amplify Gen2の主な特長

AWS Amplify Gen2を使ってみる

AWS Amplify Gen2を使うにあたって、まず、以下のアカウントおよび開発環境をご準備ください。

アカウント・開発環境

  • AWSアカウント
  • GitHubアカウント
  • VS Code(Visual Studio Code
  • Node.js ※ LTS (長期サポート) バージョンのご利用をお勧めします
  • AWS CLI(Version2)
  • Git

クイックスタートを試す

事前準備が整ったら、以下に掲載しているAWS Amplify Gen2のクイックスタート(Next.js App Router)を実際に試していきます。

クイックスタートに沿って、まずはスターターテンプレートを使って、自分のGitHubアカウントにリポジトリを作成します。今回は「amplify-gen2-app」というリポジトリ名にしました。

リポジトリの作成

リポジトリの作成が完了したら、AWSマネジメントコンソールにログインし、AWS Amplifyの管理画面を開いて[新しいアプリを作成]ボタンをクリックします。

新しいアプリを作成

つづいて、チュートリアル形式のアプリ作成画面が表示されます。最初のステップでは「ソースコードプロバイダを選択」画面が表示されるので、ここでGitHubを選択し、[次へ]ボタンをクリックします。

ソースコードプロバイダの選択

次に表示されるのは「リポジトリとブランチを追加」の画面です。ここでは、先ほど作成したGitHubリポジトリと、対象とするブランチを選択し、[次へ]ボタンをクリックします。

リポジトリとブランチを追加1
リポジトリとブランチを追加2

つづいて表示されるのは「アプリケーションの設定」画面です。ここでは、アプリケーション名、ビルド設定、サービスロールなどを確認・設定します。

ビルド設定などは、フレームワークが自動的に検出されているため、特に変更がなければこのまま[次へ]をクリックします。

アプリケーションの設定

最後に表示されるのは「確認」画面です。ここでは、これまでに設定してきた内容の一覧が表示され、必要に応じてビルドインスタンスタイプなどの詳細設定も変更できます。
今回は特に変更せず、そのまま[保存してデプロイ]ボタンをクリックして、デプロイを開始します。

確認とデプロイ

デプロイが開始されると、進行状況を表示する画面へ遷移します。デプロイには通常5〜10分程度かかるため、完了までしばらく待ちます。

デプロイ

デプロイ完了後、[デプロイされたURLにアクセス]ボタンを押すと、Todoアプリが起動します。
下の動画のように、アプリが正しく動作することを確認できます。

ここまでで、GitHubリポジトリからのデプロイまでの一連の流れを確認できました。このままクイックスタートの続きを進めて、コードを編集し、Todoリストに削除機能を実装してみます。

コードを編集するために、GitHubリポジトリをローカルにクローンします。任意のフォルダでターミナルを開き、以下のコマンドを実行してください。

git clone https://github.com/<github-user>/amplify-gen2-app.git

リポジトリをローカルにクローンしたら、VS Codeで作成されたプロジェクトフォルダを開きます。あわせて、VS Code内でターミナルも起動しておきましょう。

ターミナルで次のコマンドを実行し、アプリケーションに必要なパッケージをインストールします。

npm install 

続いて、次の動画のように、Amplifyの管理画面から「amplify_outputs.json」をダウンロードします。

このファイルは、フロントエンドアプリケーションがバックエンドと連携するために必要な情報を含んでいます。

ダウンロードした、ファイルをプロジェクトフォルダの直下に配置してください。

amplify_outputs.json

つづいて、削除処理を「app/page.tsx」に実装します。強調表示している箇所を追加および変更してください。

"use client";

import { useState, useEffect } from "react";
import { generateClient } from "aws-amplify/data";
import type { Schema } from "@/amplify/data/resource";
import "./../app/app.css";
import { Amplify } from "aws-amplify";
import outputs from "@/amplify_outputs.json";
import "@aws-amplify/ui-react/styles.css";

Amplify.configure(outputs);

const client = generateClient<Schema>();

export default function App() {
  const [todos, setTodos] = useState<Array<Schema["Todo"]["type"]>>([]);
  
  function deleteTodo(id: string) {
    client.models.Todo.delete({ id })
  }

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

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

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

  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/nextjs/start/quickstart/nextjs-app-router-client-components/">
          Review next steps of this tutorial.
        </a>
      </div>
    </main>
  );
}

これで削除処理の実装は完了です。続けて、このまま変更内容をGitでコミットし、GitHubにプッシュします。

GitHubへ変更をプッシュすると、Amplifyが自動でビルドとデプロイを開始します。管理画面上では、ビルドの進行状況をリアルタイムで確認でき、ビルドが正常に完了すれば、変更が反映されたアプリケーションがデプロイされます。

自動ビルド


デプロイ完了後は、先ほどと同様に[デプロイされたURLにアクセス]ボタンをクリックして、アプリに変更が正しく反映されているかを確認します。
次のように、Todoリストの項目をクリックして削除が正しく行われれば、実装は成功です。

サンドボックス機能を使ったローカル環境での実行

ここまでのクイックスタートを通じて、GitHubリポジトリからの自動デプロイなど、Amplify Gen2がCI/CDに適した仕組みであることがわかりました。

一方で、デプロイやビルドには一定の時間がかかるため、まずはローカル環境で動作を確認したいという方もいらっしゃるでしょう。

Amplify Gen2では「サンドボックス」機能により、バックエンドの関数やデータストアをローカル環境で動作させることができ、CI/CDに頼らず即時に変更内容をテストすることが可能です。

ここでは、実際にバックエンドにLambda関数を追加し、その動作をローカルで確認してみます。

AWS CLIでのSSO認証設定(サンドボックス環境用)

Amplify Gen2のサンドボックス環境を利用するには、AWS CLIでのシングルサインオン(SSO)認証設定が必要です。あわせて、SSOでログインする対象アカウントの事前設定も行っておく必要があります。それらの設定方法については、以下の公式ガイドを参照してください。

アカウントの設定が完了したら、VS Codeのターミナルから以下のコマンドを実行し、画面の案内に従って認証情報を設定します。

aws configure sso

認証情報の設定後、以下のコマンドにてサンドボックスを作成します。

npx ampx sandbox --profile <認証情報を設定したプロファイル 例:amplify-policy-xxxxxx>

コマンドを実行すると、次の画像のようにプロジェクト内に「.amplify」フォルダが作成されます。このフォルダには、ローカルでバックエンドを実行するための設定や環境が含まれています。
また、ローカル実行にあわせた「amplify_outputs.json」も生成され、既存のファイルが上書きされます。

サンドボックスの作成

Lambda関数の追加

ここまでで、ローカル環境の準備が整いました。つづいて、Lambda関数を追加してみましょう。
Amplify Gen2では、プロジェクト内の「amplify」フォルダにTypeScriptでコードを記述することで、バックエンドの機能を簡単に実装できます。

Lambda関数を追加する場合は、「amplify」フォルダ内に「functions」フォルダを作成し、その中に関数ごとのフォルダを用意します。各関数フォルダの中にTypeScriptファイルを配置して、関数の処理を実装していきます。

今回は、以下のように「helloamplify」という名前の関数を追加していきます。

Lambda関数の追加

フォルダの配下にまず「resource.ts」を追加し、以下のコードを実装します。この「resource.ts」は、関数をAmplify Gen2に登録するための設定ファイルです。
関数の名前やエントリーポイントとなるファイルの場所を指定するだけで、Amplifyが自動的に関数を認識し、バックエンドの一部として扱ってくれます。

import { defineFunction } from '@aws-amplify/backend';

export const helloAmplify = defineFunction({
 // オプションで関数の名前を指定できます(デフォルトはディレクトリ名です)
  name: 'hello-amplify',
  // オプションでハンドラーのパスを指定できます(デフォルトは "./handler.ts" です)
  entry: './handler.ts'
});

つづいて、関数の本体となる「handler.ts」を実装します。このファイルには、Lambda関数として実行される処理内容を記述します。

今回は、Lambda関数が呼び出された際に”Hello, Amplify Gen2!?”という文字列を返すだけのシンプルな関数を定義しています

import type { Handler } from 'aws-lambda';

export const handler: Handler = async (event, context) => {
// 関数コードはここに記述します
  return 'Hello, Amplify Gen2!?';
};

つづいて、作成した関数をAmplifyプロジェクトに登録するため、「amplify/backend.ts」に以下の強調表示された箇所を追記します。

import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource.js';
import { data } from './data/resource.js';
import { helloAmplify } from './functions/helloamplify/resource';

defineBackend({
  auth,
  data,
  helloAmplify,
});

さらに、フロントエンドから作成した関数を呼び出すには、Amplify Dataリソースを使って、関数をカスタムクエリのハンドラーとして登録します。こうすることで、関数の引数や戻り値に型を付けて、型安全なビジネスロジックを実装できます。

では、実際にコードを追加していきます。「amplify/data/resource.ts」を開き、以下の強調表示された箇所を追記してください。

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

/*== 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()]),

  helloAmplify: a
    .query()
    .returns(a.string())
    .authorization((allow) => [allow.publicApiKey()])
    .handler(a.handler.function(helloAmplify)),    
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: "apiKey",
    apiKeyAuthorizationMode: {
      expiresInDays: 30,
    },
  },
});

さいごに、作成した関数を呼び出すために、Next.js側にAPIを実装します。Next.jsプロジェクトの「app」フォルダ内に「api」フォルダを作成し、その中に関数を呼び出すためのAPIエンドポイント「lambdacall」を追加します。

Next.jsのAPIルートでは、Amplify Gen2のDataクライアントを使ってLambda関数を呼び出します。作成した「lambdacall」フォルダ内に「route.ts」ファイルを追加し、以下のコードを実装してください。

import { generateClient } from "aws-amplify/data";
import type { Schema } from "@/amplify/data/resource";
import { Amplify } from "aws-amplify";
import outputs from "@/amplify_outputs.json";
Amplify.configure(outputs)
const client = generateClient<Schema>();

export async function GET() {
  try {
    const result = await client.queries.helloAmplify();
    return Response.json({ message: result });
  } catch (error) {
    return Response.json({ error: "Error calling helloAmplify" }, { status: 500 });
  }
}

動作確認

実装が完了したら、以下のコマンドで開発サーバーを起動し、動作を確認します。

npm run dev

起動後、ブラウザでhttp://localhost:3000/にアクセスすると、先ほどクラウド環境にデプロイしたアプリが表示されます。

さらに、追加したエンドポイントhttp://localhost:3000/api/lambdacallにアクセスすると、バックエンドに追加したLambda関数の結果が正しく返ってくることが確認できます。

ローカル環境で動作確認ができたら、変更をGitにコミットし、GitHubにプッシュします。
すると、先ほどと同じようにクラウド環境へ自動でデプロイされます。

コードを修正したらすぐにクラウド環境に反映されるため、とても楽に開発を進められます。
また、クラウド環境はブランチごとに自動でデプロイされるので、機能ごとの検証やチーム開発も安心です。

CRUD処理の追加

クイックスタートをここまで進めてきて、大まかな機能確認ができましたので、あらたにテーブルを追加し、そのテーブルに対してCRUD処理を行う機能を実装したいと思います。

DynamoDBにテーブル作成

ここまでのクイックスタートでは、データベースについては特に触れてきませんでしたが、実は最初の時点で「Todo」テーブルがすでに追加されています。
これは、Amplify Gen2の機能を使って自動的に作成されたもので、DynamoDBにテーブルとして反映されています。

以下の「amplify/data/resource.ts」にある強調表示された箇所が、その定義にあたる部分です。
ここでは、データベースのスキーマやアクセス権限のルールがコードで定義されており、それに基づいてバックエンドにDynamoDBのテーブルが自動的に構築されます。

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

~~省略~~
const schema = a.schema({
  Todo: a
    .model({
      content: a.string(),
    })
    .authorization((allow) => [allow.publicApiKey()]),
~~中略~~
});

~~省略~~

実際にクラウド上を確認してみると、Amplifyのアプリ管理画面からは以下のようにテーブルが表示されており、さらにDynamoDBの管理画面を確認してみても、同じテーブルが作成されていることがわかります。

Amplifyのアプリ管理画面からテーブル確認
DynamoDB管理画面からテーブル確認

テーブルは、「amplify/data/resource.ts」のschemaにモデルを定義することで追加できることがわかりました。これと同じ手順で、以下のように別のテーブルを追加してみます。

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

~~省略~~
const schema = a.schema({
  Todo: a
    .model({
      content: a.string(),
    })
    .authorization((allow) => [allow.publicApiKey()]),

  Invoices: a
    .model({
      InvoiceID: a.id().required(),
      BillNo: a.string().required(),
      SlipNo: a.string().required(),
      CustomerID: a.string().required(),
      CustomerName: a.string().required(),
      Products: a.string().required(),
      Number: a.integer().required(),
      UnitPrice: a.float().required(),
      Date: a.string().required(),
    })
    .identifier(["InvoiceID"])
    .authorization((allow) => [allow.publicApiKey()]),

~~中略~~
});

~~省略~~

コードをGitにコミットし、GitHubにプッシュしてデプロイすると、以下のように正しくテーブルが作成されました。

Amplifyのアプリ管理画面からテーブル確認

Web APIの追加

新しくテーブルを追加したことで、それに対応したフロントエンドを実装すれば、Todoアプリのような機能を構築することも可能です。
今回は機能の確認が目的のため、フロントエンドを新たに実装せず、Next.jsを使ってWeb APIを構築し、Swagger UIを用いて動作確認を行う形で進めていきます。

先ほど作成したLambda関数呼び出し用のAPIと同様に、「app/api」配下にエンドポイントを追加していきます。

ここでは、新たに「Invoices」エンドポイントを作成し、その配下に動的ルートとして「[id]」エンドポイントも追加します。これにより、たとえば特定の請求データ(Invoice)をID指定で取得するようなAPIを実装できるようになります。

エンドポイントを追加したあとは、処理の実装に進みます。「Invoices」エンドポイントに「route.ts」ファイルを作成し、以下のコードを実装します。

import { NextRequest, NextResponse } from 'next/server';
import { generateClient } from "aws-amplify/data";
import type { Schema } from "@/amplify/data/resource";
import { Amplify } from "aws-amplify";
import outputs from "@/amplify_outputs.json";

// Amplify の設定を読み込んで初期化
Amplify.configure(outputs);

// データ用のクライアントを生成
const client = generateClient<Schema>();

// GETリクエスト: GraphQLのobserveQueryを使い、全てのInvoicesレコードを取得
export async function GET() {
  return new Promise<Response>((resolve, reject) => {
    const subscription = client.models.Invoices.observeQuery({ })
      .subscribe({
        next: (snapshot: any) => {
          subscription.unsubscribe();
          resolve(NextResponse.json(snapshot.items));
        },
        error: (error: any) => {
          reject(NextResponse.json({ error: error.message }, { status: 500 }));
        }
      });
  });
}

// POST リクエストハンドラ: GraphQLのcreate操作を使って、請求書(Invoice)データを登録
export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const invoiceData = {
      InvoiceID: body.InvoiceID,
      BillNo: body.BillNo,
      SlipNo: body.SlipNo,
      CustomerID: body.CustomerID,
      CustomerName: body.CustomerName,
      Products: body.Products,
      Number: body.Number,
      UnitPrice: body.UnitPrice,
      Date: body.Date,
    };    
    const result = await client.models.Invoices.create(invoiceData);
    return NextResponse.json({ result });
  } catch (error: any) {
    console.error("Error creating Invoice:", error);
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}

つづいて「[id]」エンドポイントにも「route.ts」ファイルを追加し、以下のコードを実装します。

import { NextResponse, NextRequest } from 'next/server';
import { generateClient } from "aws-amplify/data";
import type { Schema } from "@/amplify/data/resource";
import { Amplify } from "aws-amplify";
import outputs from "@/amplify_outputs.json";

// Amplify の設定を読み込んで初期化
Amplify.configure(outputs);

// データ用のクライアントを生成
const client = generateClient<Schema>();

// GET: 指定された Invoice ID に一致するレコードを取得
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  const id = (await params).Id;
  return new Promise<Response>((resolve, reject) => {
    const subscription = client.models.Invoices.observeQuery({ filter: { InvoiceID: { eq: id } } })
      .subscribe({
        next: (snapshot: any) => {
          subscription.unsubscribe(); 
          resolve(NextResponse.json(snapshot.items));
        },
        error: (error: any) => {
          reject(NextResponse.json({ error: error.message }, { status: 500 }));
        }
      });
  });
}

// PUT: 指定された Invoice ID のレコードを更新(update を使用)
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  try {
    const id = (await params).id;
    const body = await request.json();
    const result = await client.models.Invoices.update({ InvoiceID: id, ...body });
    return NextResponse.json({ result });
  } catch (error: any) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}

// DELETE: 指定された Invoice ID のレコードを削除(delete を使用)
export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  try {
    const id = (await params).id;
    const result = await client.models.Invoices.delete({ InvoiceID: id });
    return NextResponse.json({ result });
  } catch (error: any) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}

これらの実装はすべて、Amplifyが提供するData機能を通じて、GraphQL APIを使ってデータ操作を行っています。Amplify Gen2では、「amplify/data/resource.ts」にTypeScriptでスキーマモデルを定義することで、DynamoDBテーブルやCRUD(作成・取得・更新・削除)操作に対応したGraphQL APIが自動生成されます。

Swagger UIの追加

それでは、最後に、Swagger UIをフロントに実装していきます。
はじめにSwagger UIを利用するために「swagger-ui-react」をインストールします。型定義もあわせて、以下のコマンドを実行してください。

npm install swagger-ui-react @types/swagger-ui-react

つづいて、Swagger UIのインターフェイスが表示されるページを追加します。「app」配下に新たに「SwaggerUI」というエンドポイントを追加し、その中に「page.tsx」を追加します。実装コードは以下のとおりです。

'use client';

import dynamic from 'next/dynamic';
import styles from './page.module.css';
import 'swagger-ui-react/swagger-ui.css';

const SwaggerUI = dynamic(() => import('swagger-ui-react'), { ssr: false });

export default function SwaggerUIPage() {
  return (
    <div className={styles.swaggerContainer}>
      <h1>Invoices API Documentation</h1>
      <SwaggerUI url="/swagger.json" />
    </div>
  );
}

さらに、「SwaggerUI」配下に、「page.module.css」ファイルを追加します。

.swaggerContainer {
  position: fixed !important;
  top: 0 !important;
  left: 0 !important;
  width: 100vw !important;
  height: 100vh !important;
  background: white !important;
  margin: 0 !important;
  padding: 0 !important;
  overflow: auto !important;
  z-index: 1000 !important;
  display: block !important;
}

.swaggerContainer * {
  box-sizing: border-box;
}

最後に、APIの仕様を定義した「swagger.json」ファイルを、Next.jsプロジェクトの「public」フォルダに配置します。これにより、アプリケーション内でSwagger UIを使ってAPIドキュメントを表示できるようになります。

  • 「swagger.json」ファイルはこちらからダウンロード、もしくは以下のJSONデータをコピーしてください。
swagger.json

動作確認

実装が完了しましたので、サンドボックスでのローカル実行、あるいはGitHubへのプッシュによって自動デプロイされたアプリケーションにて、実際の動作を確認してみましょう。

GET処理(Read-全件)

GET処理(Read-指定レコード)

POST処理(Create)

PUT処理(Update)

DELETE処理(Delete)

全ての処理が、正しく動作することが確認できました。

さいごに

今回は、AWS Amplify Gen2のクイックスタートを通じて、CRUD処理を行うWeb APIを実装してみました。Amplify Gen2を活用することで、Lambda関数やDynamoDBの作成も簡単に行え、これまでAWSでの開発経験がないフロントエンド開発者でも、フロントエンドとバックエンドを一体的に構築できるWebアプリケーションを手軽に作成できます。また、すでにAWS上にLambda関数などを構築済みの方も、Amplifyから接続して既存資産を活用可能です。業務アプリケーションのクラウド化を検討している方にとって、非常に魅力的な選択肢です。

クラウドサービス上で業務アプリケーションを構築してみたいとお考えの方は、ぜひ本記事を参考に、一度お試しください。

メシウスでは業務アプリケーション開発の工数削減に役立つ高機能なJavaScriptライブラリを提供しています。

メシウスのJavaScriptライブラリ

無償のトライアル版や、ブラウザ上で手軽に試せるデモアプリケーションも公開しているので、こちらも是非ご覧ください。

\  この記事をシェアする  /