✍️

【学習】AWS Amplify Gen2とOpenAIのGPT-4oを使ったWebアプリを作る

2025/01/21に公開

AWS Amplify Gen2はフロントエンドとバックエンドをTypeScriptで素早く構築できる便利なサービスです。
具体的には、認証、API、DB、ストレージ、ホスティング、AIなどが全てAmplify内で完結します。

「フロントエンドはできるけれど、バックエンドやクラウドはわからない」という人でも、それらをほとんど意識することなくデプロイまで実行できます。
コードを更新する際もGithubにpushするだけでOKです。

私が使った感覚としては、supabase+Vercelを一体化したサービスといった印象です。

https://aws.amazon.com/jp/amplify/

今回やること

  • Amplifyの紹介
  • AWS Amplify Gen2とOpenAIのChatGPTで簡単なアプリをデプロイする
    • Bedrockを使う方が自然なのですが、諸事情で私の環境では利用できないため、代わりにOpenAIのChatGPTを利用します。

作るもの

エントリーシートを入力すると、想定される深掘り質問を考えてくれるアプリです。
実用性はさておき、こういったハンズオンのサンプルアプリがTODOアプリばかりで面白くないので、別パターンのものを作ってみました。

前提:Gen1とGen2の違い

2023年ごろからAmplify Gen2がリリースされました。操作方法が大幅に変わっています。

例えば、Auth機能を追加する場合、
Gen1では「Amplify CLI」を使ってターミナルから操作します。

https://docs.amplify.aws/nextjs/start/quickstart/nextjs-app-router-client-components/

Gen2では「AWS CDK(Cloud Development Kit)」を使って、resource.ts上に記載します。

https://docs.amplify.aws/nextjs/build-a-backend/auth/set-up-auth/

比較表はこちらが分かりやすかったので引用させていただきます。

画像引用:https://speakerdeck.com/tacck/aws-amplify-gen-2-gaji-nian-gen1tonowei-iwoque-ren?slide=6

実装

実装していきましょう!

プロジェクトの作成

こちらのクイックスタートを参考に進めます。
https://docs.amplify.aws/nextjs/start/quickstart/nextjs-app-router-client-components/

レポジトリを複製

今回はレポジトリ名をzenn-appとしました。(任意で変更してください。)

デプロイ

まずはデプロイします。

以下のページにアクセスしたら、
https://console.aws.amazon.com/amplify/create/repo-branch

アプリケーションをデプロイからGitHubを選びます。

次の画面で、デプロイしたいレポジトリを選びます。今回はzenn-appです。

フレームワークが検出されます。今回は特に設定を変更せずそのまま進みます。

保存してデプロイを押すとデプロイが開始されます。

途中でロケットの画面から、管理画面に変わりますが、デプロイが完了して実際にURLにアクセスできるようになるまでは5分〜10分程度かかります。

完了後に以下のような画面になります。チュートリアル用のTODOアプリです。

試しに「これは本番環境で作成したTODOです」と入力してみました。
後ほどSandbox環境での表示と比較します。
そちらで表示されない場合はローカル環境が本番環境に影響を与えないことがわかります。

ローカルにコードを用意する

Githubから先ほどのレポジトリをクローンします。

git clone  https://github.com/<あなたのユーザー名>/<あなたのレポジトリ名>.git

忘れずにnpm installをします。

npm install

また、Amplifyとローカルの紐付けを行う必要があります。管理画面から「デプロイされたバックエンドリソース」を選択して「amplify_output.jsonをダウンロード」を押します。
ダウンロードしたamplify_output.jsonをローカルのルートにおいてください。

Sandbox環境の設定

Sandboxを使うと、本番環境とは別にその開発PCごとに個別のバックエンドの環境を作成できます。resouce.tsの変更を検知して即座にバックエンドが作成されるため、挙動を確認するためだけのデプロイが不要です。また、チーム開発する際に、自分の開発環境でバックエンドを変更したとしても周囲に影響が出ません。


画像引用:https://aws.amazon.com/jp/blogs/news/team-workflows-amplify/

事前にaws cliで認証情報の設定が必要です。
こちらの記事を参考に設定してください。
https://zenn.dev/akkie1030/articles/aws-cli-setup-tutorial

Sandbox環境を立ち上げる際は以下のコードを実行してください。
ターミナルでCloudFormationが実行されていることがわかります。

npx ampx sandbox

このような表示が出たらSandbox環境の立ち上げが終了です。

新しいターミナルを開き、開発画面を表示してみましょう。

npm run dev

「これはローカルで作成したTODOです。」と入力してみました。
本番環境で作成したTODOは表示されず、独立した環境であることがわかります。

DBの定義

amlplify/data/resource.tsにあるschemaを変更してみましょう。

今回はあるテキストに対して添削を行いたいので、試しにText01というテーブルを作ってみます。
(本当はもっと命名を考えた方がいいのですが、今回はテキトーに作ります。)
項目はcontentだけです。

ファイルを保存すると、自動でその変更を検知して、すぐにSandbox環境に反映されます。
たったこれだけでDynamoDBに新しいテーブルを追加できました。
手動でマイグレーションする必要はありません。

フロントエンドの変更

TODOアプリから、以下のような入力欄を持つアプリに変更します。

Amplifyでは特にCRUD用のAPIを作成せずともClientを使って直接DBの操作ができます!!

app/page.tsx

const client = generateClient<Schema>();

〜省略〜

        await client.models.Text01.create({
          content: inputValue,
        });

page.tsxとapp.cssを書き換えてください。
page.tsxは一旦保存機能のみを作成して、後ほどChatGPTの呼び出し機能を追加します。

app/page.tsx
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() {
  // DB から取得したデータを保持するためのステート
  const [textData, setTextData] = useState<Schema["Text01"]["type"] | null>(
    null
  );

  // 入力欄の値を保持するステート
  const [inputValue, setInputValue] = useState("");

  // マウント時に、Text01 テーブルの内容を取得 (最初の1件を想定)
  useEffect(() => {
    const subscription = client.models.Text01.observeQuery().subscribe({
      next: (snapshot) => {
        const items = snapshot.items;
        // 1件しか使わない想定で、最初の要素をセット
        if (items.length > 0) {
          setTextData(items[0]);
          setInputValue(items[0].content || "");
        }
      },
    });

    // コンポーネントがアンマウントしたらサブスクリプション解除
    return () => subscription.unsubscribe();
  }, []);

  // 「保存」ボタンを押したときの処理
  async function handleSave() {
    try {
      if (textData) {
        // 既存レコードがあれば更新
        await client.models.Text01.update({
          id: textData.id, // 既存レコードの ID
          content: inputValue, // 入力値
        });
      } else {
        // レコードが無い場合は新規作成
        await client.models.Text01.create({
          content: inputValue,
        });
      }
      alert("保存しました!");
    } catch (e) {
      console.error(e);
      alert("保存に失敗しました…");
    }
  }

  return (
    <main>
      <h1>エントリーシート</h1>
      <p>Q1:志望動機を教えてください。</p>
      <textarea
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        // styleで大きさ調整
        style={{
          width: "50vw",
          height: "300px",
          boxSizing: "border-box",
          fontSize: "16px",
        }}
        placeholder="私が御社を志望したのは・・・"
      />
      <br />
      <button onClick={handleSave}>保存</button>
    </main>
  );
}

表示したい内容

ついでに背景が紫だと見にくいので、白に変えておきます。

app/app.css
app/app.css
body {
  margin: 0;
  /* background: linear-gradient(180deg, rgb(117, 81, 194), rgb(255, 255, 255)); */
  background: rgb(255, 255, 255);
  display: flex;
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  height: 100vh;
  width: 100vw;
  justify-content: center;
  align-items: center;
}

保存ボタンを押した後、リロードしてみましょう。
再度ページを読み込んでもデータが消えることなく保存できています。

ChatGPTのAPIの作成

ChatGPTに深掘り質問を考えてもらうために、APIを作成します。
今回はNext.js側のRoute Handlerで普通に作ります。

Amplifyを使ってLambdaとAPI Gatewayで実装することもできます。
https://docs.amplify.aws/react/build-a-backend/add-aws-services/rest-api/set-up-rest-api/

先にOPENAI_API_KEYを.envファイルを作成して記載しておきましょう。

.env
OPENAI_API_KEY=sk-proj-hogehogehoge

page.tsx内でaxiosを使うので追加しておきましょう。

npm install axios

route.tsを作成します。

app/api/chatgpt/route.ts
app/api/chatgpt/route.ts
// app/api/chat/route.ts
import { NextRequest, NextResponse } from "next/server";
import axios from "axios";

export async function POST(req: NextRequest) {
  try {
    // リクエスト JSON ボディを取得
    const body = await req.json();
    const { text } = body;

    if (!text) {
      return NextResponse.json({ error: "text is required" }, { status: 400 });
    }

    // ChatGPT に送るプロンプトを組み立て
    const prompt = `
あなたは就活支援のコンサルタントです。以下の入力内容をもとに、面接で聞かれそうな深掘り質問を1つ考えてください。
ただし、質問以外の内容は出力しないでください。
${text}
    `;

    // OpenAI ChatGPT を呼び出し (axios を使用)
    const response = await axios.post(
      "https://api.openai.com/v1/chat/completions",
      {
        model: "gpt-4o-mini",
        messages: [{ role: "user", content: prompt }],
      },
      {
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
        },
      }
    );

    // レスポンスから返答テキストを取り出す
    const result = response.data?.choices?.[0]?.message?.content;

    if (!result) {
      throw new Error("No response from ChatGPT");
    }

    console.log("ChatGPT response:", result);
    return NextResponse.json({ content: result }, { status: 200 });
  } catch (error: any) {
    console.error("Error calling OpenAI API:", error.message);
    return NextResponse.json(
      { error: "Failed to process the request." },
      { status: 500 }
    );
  }
}

ChatGPTの呼び出し

質問を保存するとChatGPTを呼び出して、サイドバーに表示するようにします。
本来はloading表示などをつけるべきですが本筋ではないので割愛します。

以下のコードでapp/page.tsxを更新してください。

app/page.tsx
app/page.tsx
import { NextRequest, NextResponse } from "next/server";
import axios from "axios";

export async function POST(req: NextRequest) {
  try {
    // リクエスト JSON ボディを取得
    const body = await req.json();
    const { text } = body;

    if (!text) {
      return NextResponse.json({ error: "text is required" }, { status: 400 });
    }

    // ChatGPT に送るプロンプトを組み立て
    const prompt = `
あなたは就活支援のコンサルタントです。以下の入力内容をもとに、面接で聞かれそうな深掘り質問を1つ考えてください。
ただし、質問以外の内容は出力しないでください。
${text}
    `;

    // OpenAI ChatGPT を呼び出し (axios を使用)
    const response = await axios.post(
      "https://api.openai.com/v1/chat/completions",
      {
        model: "gpt-4o-mini",
        messages: [{ role: "user", content: prompt }],
      },
      {
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
        },
      }
    );

    // レスポンスから返答テキストを取り出す
    const result = response.data?.choices?.[0]?.message?.content;

    if (!result) {
      throw new Error("No response from ChatGPT");
    }

    console.log("ChatGPT response:", result);
    return NextResponse.json({ content: result }, { status: 200 });
  } catch (error: any) {
    console.error("Error calling OpenAI API:", error.message);
    return NextResponse.json(
      { error: "Failed to process the request." },
      { status: 500 }
    );
  }
}

変更を保存して開発環境で動作確認をしてみましょう。
テキストを入力した後、保存を押すと少し待ってからAIの質問が表示されます。

環境変数の設定

これらの変更を本番環境にデプロイしていきます。
その前にChatGPTのOPENAI_API_KEYを本番環境でも読み込める用に設定する必要があります。

管理画面から登録しましょう。

再度デプロイ

ついに本番環境へのデプロイです。
方法は非常に簡単でgithub上に変更をpushするだけです。

githubの変更を検知して自動で再デプロイしてくれます。
10分程度はかかるので気長に待ちましょう。

もしデプロイに失敗した場合は際は、ログを読んだりコピペしてChatGPTに質問したりすると解決できる可能性があります。

axiosのインストールを忘れてビルドに失敗した例
2025-01-21T11:56:08.066Z [WARNING]: Failed to compile.
136
2025-01-21T11:56:08.070Z [WARNING]: ./app/api/chatgpt/route.ts
137
Module not found: Can't resolve 'axios'

動作確認

デプロイが終わったら、実際に動作するか確認してみましょう。
保存ボタンを押すと無事にAIからの質問が返ってきました。

ちなみにAmplifyの管理画面上でも保存されたDBの中身を確認することができます。
prisma studioのようなイメージです。
(今回はAIからのレスポンスはDBに保存していないため、志望動機の文章のみが保存されています。)

アプリの削除

最後に、消し忘れで無駄に課金されないように削除しましょう。
全般設定からアプリの削除を押すと、削除できます。

最後に

今回はホスティングとDB、Sandbox環境、環境変数について主に触れました。他にもよく使うものでは認証やストレージなどの機能も使えます。
少し最初の学習コストはありますが、バックエンドやクラウドを意識せずともすぐに使えるのは非常に便利です。
興味を持った方は自身でも試してみてください!

Discussion