はじめに
AWSやAzureのインフラ構築をメインに日々仕事しているのですが、AWS CDKやTerraformでコードを書いてインフラ構築をするようになり、そのコードが今やAIが書いてくれるようになり・・・と、AIが僕たちの仕事を手伝ってくれるようになったおかげで楽できるようにはなったのですが、効率化された分、”インフラだけのエンジニア”は淘汰されていくような気がしました。今後エンジニアの垣根というか、境界線はどんどん無くなっていくのではないかな〜と思う日々です。
今回は、そんなインフラエンジニアが「AWSモダンアプリ開発入門」という書籍を読んで勉強してみたので、その内容の備忘録を記載したいと思います。
書籍を読んで作成できるアプリ概要
Amazon Bedrock(Claude等)を使ったAIチャットアプリです。AWS Amplify Gen2をバックエンドに採用し、インフラのコード化・認証・APIをすべてTypeScriptで定義しています。
主な機能
- Cognitoによるメール認証(サインアップ/ログイン)
- AIモデルを選択してチャット送信
- 会話履歴の保存・一覧表示(DynamoDB)
- 会話ごとのタイトル自動生成
技術スタック
| レイヤー | 技術 |
|---|---|
| フロントエンド | React 19 + TypeScript + Vite |
| スタイリング | Tailwind CSS v4 |
| ルーティング | React Router v7 |
| バックエンド | AWS Amplify Gen2 (AppSync + Lambda) |
| AI | Amazon Bedrock (Converse API) |
| DB | DynamoDB (Amplify Data) |
| 認証 | Amazon Cognito |
フォルダツリーと各ファイルの役割
aws-ai-chat/
├── amplify/ # バックエンド定義(Amplify Gen2)
│ ├── backend.ts # バックエンド全体の組み立て。IAMポリシーや環境変数の設定もここ
│ ├── auth/
│ │ └── resource.ts # Cognito認証の設定(メールログインを有効化)
│ ├── data/
│ │ └── resource.ts # GraphQLスキーマ定義。Message/ConversationモデルとLambdaクエリを定義
│ └── function/
│ ├── bedrockChat/
│ │ ├── resource.ts # Lambda関数の設定(タイムアウト・ランタイム等)
│ │ └── handler.ts # メインのLambdaハンドラ。Bedrock呼び出し・DynamoDB読み書きを担当
│ └── helloworld/
│ ├── resource.ts # 動作確認用Lambda設定
│ └── handler.ts # 動作確認用ハンドラ
│
├── src/ # フロントエンド(React)
│ ├── main.tsx # エントリーポイント。Amplify初期化・Cognito認証UI・日本語化
│ ├── App.tsx # ルーターのセットアップ
│ ├── routes.tsx # URLルーティング定義(/chat/new, /chat/:id)
│ │
│ ├── api/
│ │ ├── client.ts # Amplify GraphQLクライアントの生成(型付き)
│ │ ├── bedrock.ts # BedrockChatクエリの呼び出し関数
│ │ └── chat.ts # 会話・メッセージ一覧取得のAPI関数
│ │
│ ├── components/
│ │ ├── layout/
│ │ │ └── ChatLayout.tsx # サイドバー+メインエリアの2カラムレイアウト。Contextのプロバイダも配置
│ │ └── ui/
│ │ ├── Sidebar.tsx # 会話履歴一覧・新規チャットボタン・ログアウト
│ │ ├── ChatInput.tsx # メッセージ入力欄とモデル選択UI
│ │ ├── MessageList.tsx # メッセージ一覧の表示
│ │ ├── AIMessage.tsx # AIの返答メッセージ(Markdownレンダリング)
│ │ ├── UserMessage.tsx # ユーザのメッセージ表示
│ │ └── Profile.tsx # ログインユーザ情報の表示
│ │
│ ├── context/
│ │ └── ConversationsContext.tsx # 会話一覧をグローバルに管理するContext。サイドバーと各ページで共有
│ │
│ ├── hooks/
│ │ └── useConversations.ts # 会話一覧の取得・状態管理カスタムフック
│ │
│ ├── pages/
│ │ ├── NewChat.tsx # 新規チャット画面。UUIDを生成してチャット画面へ遷移
│ │ └── ChatConversation.tsx # チャット画面。メッセージ送受信・履歴表示・ローディング制御
│ │
│ ├── types/
│ │ └── chat.ts # Message・Conversation型定義
│ │
│ └── utils.ts # チャットタイトル生成などのユーティリティ関数
│
├── amplify_outputs.json # Amplifyデプロイ後に自動生成される接続情報(フロントが参照)
├── amplify.yml # CI/CDパイプライン設定
├── package.json # 依存パッケージ・スクリプト定義
├── vite.config.ts # Viteビルド設定
├── tsconfig.app.json # フロントエンド用TypeScript設定
└── tsconfig.node.json # Amplifyバックエンド用TypeScript設定
アプリ全体のフォルダ構成を見ていきます。大きく分けると、バックエンド( amplify/) とフロントエンド(src/) の2つに分かれています。
amplify/ ― AWSのインフラ・バックエンド定義
amplify/
├── backend.ts
├── auth/
│ └── resource.ts
├── data/
│ └── resource.ts
└── function/
├── bedrockChat/
│ ├── resource.ts
│ └── handler.ts
└── helloworld/
├── resource.ts
└── handler.ts
このフォルダが、AWSのインフラをコードで定義している場所です。インフラエンジニアにとっては一番馴染みやすい部分かもしれません。
-
backend.ts:バックエンド全体の設定をまとめるエントリーポイントです。LambdaへのIAMポリシーの付与や環境変数の設定もここで行います。Terraformでいうとルートモジュールのようなイメージです。 -
auth/resource.ts:Amazon Cognitoの認証設定です。書籍ではメールアドレスとパスワードでのログインを有効化しています。 -
data/resource.ts:DynamoDBのテーブル定義とGraphQL APIのスキーマを定義しています。このファイル1つを書くだけで、DynamoDBテーブル・AppSync API・認可ルールがAmplifyによって自動生成されます。 -
function/bedrockChat/:AIチャットのメイン処理を担うLambda関数です。resource.tsでタイムアウトやランタイムを設定し、handler.tsに実際の処理(Bedrock呼び出し・DynamoDBへの読み書き)を実装しています。 -
function/helloworld/:動作確認用のLambda関数です。書籍内では「Amplifyの設定が正しく動くか」を確認するために作成しました。
src/ ― フロントエンド(React)
フロントエンドは、役割ごとにフォルダを分けて管理しています。
src/types/ ― 型定義
types/
└── chat.ts
TypeScriptでは、扱うデータの「型」をあらかじめ定義しておくことができます。例えば「メッセージはid・role・content・timestampを持つ」といった定義です。
型を定義しておくことで、コードを書く際に間違った使い方をするとエディタが即座に警告してくれます。インフラで言えば、変数の型チェックのようなイメージです。このフォルダはアプリ全体で使うデータの「設計図」を置く場所です。
src/api/ ― API呼び出し
api/
├── client.ts
├── bedrock.ts
└── chat.ts
バックエンド(AppSync)との通信処理をまとめたフォルダです。
-
client.ts:AmplifyのGraphQLクライアントを生成します。このクライアントを通じてバックエンドと通信します。 -
bedrock.ts:AIへのメッセージ送信処理です。 -
chat.ts:会話履歴の取得処理です。
画面側のコードとAPI通信のコードを分離することで、「どこでAPIを呼んでいるか」が一目でわかるようになります。
src/hooks/ ― カスタムフック
hooks/
└── useConversations.ts
Reactには「フック」という仕組みがあり、コンポーネントに状態管理やデータ取得などのロジックを持たせることができます。カスタムフックとは、そのロジックを独立したファイルに切り出したものです。
useConversations.tsは会話一覧の取得・ローディング状態の管理を担っています。ロジックをここに集約することで、画面側のコードをシンプルに保てます。
src/context/ ― グローバルな状態管理
context/
└── ConversationsContext.tsx
Reactでは、コンポーネント間でデータを共有する仕組みとして「Context」があります。
今回のアプリでは、サイドバーの会話履歴一覧をチャット画面からも更新する必要があります。このように複数の画面・コンポーネントをまたいで共有したいデータをContextで管理しています。
src/components/ ― UIコンポーネント
Reactでは、画面を構成するUI部品を「コンポーネント」という単位で分割して実装します。例えば、メッセージの入力欄・AIの返答・ユーザのメッセージ・サイドバーといった要素をそれぞれ独立したファイルとして切り出しています。
src/components/
├── layout/
│ └── ChatLayout.tsx # 画面全体の2カラムレイアウト
└── ui/
├── ChatInput.tsx # メッセージ入力欄
├── MessageList.tsx # メッセージ一覧
├── AIMessage.tsx # AIの返答表示
├── UserMessage.tsx # ユーザのメッセージ表示
├── Sidebar.tsx # 会話履歴・ナビゲーション
└── Profile.tsx # ユーザ情報表示
こうして細かく分割しておくことで、以下のようなメリットがあります。
-
再利用性:
ChatInputやAIMessageは別のページでもそのまま使い回せる - 可読性:1ファイルの責務が明確になり、コードが追いやすくなる
- 保守性:UIの修正が必要になったとき、影響範囲を該当コンポーネントだけに限定できる
インフラのコードで言えば、モジュール化してリソースを再利用する感覚に近いかもしれません。TerraformのモジュールやCDKのConstructと同じ考え方です。
src/pages/ ― ページ
pages/
├── NewChat.tsx
└── ChatConversation.tsx
ユーザが実際に見る「画面」に対応するコンポーネントを置くフォルダです。
-
NewChat.tsx:アプリを開いたときの最初の画面です。メッセージを入力して送信すると、新しい会話が始まります。 -
ChatConversation.tsx:チャットのメイン画面です。メッセージの送受信・AIの返答表示・会話履歴の読み込みを担っています。
pages/はコンポーネントを組み合わせて画面を作る場所、components/は部品を作る場所、という役割分担になっています。
まとめ
各フォルダの役割を整理するとこうなります。
| フォルダ | 役割 |
|---|---|
amplify/ |
AWSインフラ・バックエンドの定義 |
src/types/ |
データの型定義(設計図) |
src/api/ |
バックエンドとの通信処理 |
src/hooks/ |
データ取得・状態管理のロジック |
src/context/ |
複数コンポーネント間で共有するデータ |
src/components/ |
再利用可能なUI部品 |
src/pages/ |
実際の画面 |
このように役割ごとにフォルダを分けることで、「どこに何があるか」が明確になり、機能追加や修正がしやすい構成になっています。
データの流れ
アプリのデータの流れは大きく2つに分けて考えることができます。
フロントエンドとバックエンドの間のデータの流れと、フロントエンド内部のデータの流れです。
フロントエンド ↔ バックエンドのデータの流れ
チャットでメッセージを送信したときの流れを見てみます。
① ユーザがメッセージを入力・送信
↓
② ChatConversation.tsx(画面)
↓
③ api/bedrock.ts(API呼び出し)
↓
④ AppSync(GraphQL API)
↓
⑤ Lambda: handler.ts
├── DynamoDB(会話・メッセージを保存)
└── Amazon Bedrock(AIに問い合わせ)
↓
⑥ AIの返答をフロントエンドに返却
↓
⑦ 画面にAIの返答を表示
ポイントは、フロントエンドが直接DynamoDBやBedrockにアクセスするわけではないという点です。必ずAppSync(GraphQL API)を経由してLambdaが処理を行います。これにより、認証されたユーザだけがAPIを呼び出せるようにアクセス制御ができています。
また、会話履歴を表示するときのデータの流れはこうなります。
① サイドバー・チャット画面を開く
↓
② api/chat.ts(API呼び出し)
↓
③ AppSync(GraphQL API)
↓
④ DynamoDB(会話・メッセージを取得)
↓
⑤ 画面に会話履歴を表示
こちらはLambdaを経由せず、AppSyncが直接DynamoDBからデータを取得しています。Amplify Dataの仕組みにより、モデルの読み取りはLambdaなしで自動的に処理されます。
なぜLambdaを経由しないのか
ここは理解が及んでなかったので、AIに聞きながら理解を深めます。
amplify/data/resource.tsのスキーマ定義を見てみます。
Message: a
.model({ ... })
.authorization((allow) => [allow.owner()]),
Conversation: a
.model({ ... })
.authorization((allow) => [allow.owner()]),
BedrockChatの定義と比べてみると、違いがわかります。
BedrockChat: a
.query()
.handler(a.handler.function(bedrockChatFunction)), // Lambdaを明示的に指定
a.model()で定義したものは、Amplifyがデータの読み書きに必要なAPIを自動生成してくれます。具体的には以下のような操作が自動で使えるようになります。
-
client.models.Conversation.list()→ 会話一覧を取得 -
client.models.Conversation.get()→ 特定の会話を取得
これらはLambdaを介さず、AppSyncが直接DynamoDBにアクセスして処理します。開発者がLambdaのコードを書かなくても、モデルを定義するだけでCRUD操作が使えるようになる、というのがAmplify Dataの大きな特徴です。
一方、BedrockChatのようにAIへの問い合わせやDynamoDBへの複雑な書き込みが必要な処理は、.handler(a.handler.function(...)) でLambdaを明示的に指定して独自の処理を実装しています。
なるほど。まとめると、単純なデータの読み書きはAmplifyにおまかせ、複雑な処理だけLambdaで実装するという使い分けにしているということでした。
フロントエンド内部のデータの流れ
次に、フロントエンド内部でデータがどのように流れているかを見てみます。
useConversations.ts(データ取得・状態管理)
↓
ConversationsContext.tsx(グローバルな状態として保持)
↓
├── Sidebar.tsx(会話履歴一覧を表示)
└── ChatConversation.tsx(新規会話作成後にサイドバーを更新)
Reactでは、基本的に親から子へ一方向にデータが流れます。しかし今回のアプリでは、チャット画面(ChatConversation.tsx)で新しい会話を作成したとき、サイドバー(Sidebar.tsx)の会話履歴一覧も更新する必要があります。
この2つのコンポーネントは親子関係にないため、直接データを渡すことができません。そこでContextを使い、会話一覧のデータをアプリ全体で共有できるようにしています。
具体的な流れはこうなります。
① ChatConversation.tsx で新しい会話を作成
↓
② ConversationsContext の notifyConversationCreated() を呼び出す
↓
③ useConversations.ts の refreshConversations() が実行される
↓
④ api/chat.ts 経由で会話一覧を再取得
↓
⑤ Sidebar.tsx の会話履歴一覧が自動的に更新される
インフラで例えるなら、Contextはパラメータストアのようなイメージです。
複数のリソースが同じ値を参照したいときに、一箇所で管理して共有する仕組みに近いと思います。
まとめ
データの流れを整理するとこうなります。
[ユーザ操作]
↓
[pages/] 画面
↓
[api/] API呼び出し
↓
[AppSync] ─── [Lambda] ─── [Bedrock]
↓ └───── [DynamoDB]
[pages/] 画面に反映
↑
[context/] ─── [hooks/] (複数画面をまたぐデータの共有)
フロントエンドとバックエンドの間はAppSyncが窓口となり、フロントエンド内部ではContextが複数の画面をつなぐ役割を担っています。
最後に
インフラの仕事をしているとあまり関わりのないフロントエンドの内容を重点的に知りたかったので、AIと協力しながら備忘録として情報をまとめてみました。
これらの内容を頭に入れた上で、もう1周書籍を振り返ってみて、理解を深めようと思います。