令和最新版(？) React + CloudFront + APIGateway でWebアプリを作成する方法のおさらい！

# はじめに

明けましておめでとうございます！ **yu-Matsu**です！
今年も頑張ってアウトプットしていきたいと思いますので、よろしくお願いします。

今回は、**React + CloudFront + APIGateway でWebアプリを作成、ホスティングする**方法をおさらいする機会があったため、備忘録として記事にしたいと思います。

:::note warn
色々詰め込んでいるのでかなり長い記事になっています。
出来るだけ細かく章分けしているつもりですので、「もう知ってるよ」という内容がある場合は必要な情報だけ拾っていってもらえればと思います！

**2025/1/18:**
「作成してみよう」の5.4.に、**ページリロードの際に403エラーになる事象に対する対応策**を追記しました。
:::


# 今回作成するWebアプリケーションのイメージ

今回作成するもののイメージは以下のようになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/0f30119d-7e3e-032b-f2bf-dbe75d4ea99c.png" width=600>

- **CloudFront + S3**でWebアプリケーションをホスティング
- **Cognito**で認証機能を設ける
- **API Gateway + Lambda**でフロントエンドから実行するAPIを実装
    - **DynamoDB**からデータを取得するAPI
    - **Bedrock**とやり取りするAPI
    - Cognitoを**オーソライザー**として設定し、認証を通していないユーザーはAPIを実行できないようにする

フロントエンドのホスティングといえば、カジュアルに扱えるものとして**AWS Amplilfy**を利用する場合が多いと思います。しかし、プロジェクトの制約等でAmplifyがNGの場合も結構あり、そういった場合にCloudFront + S3構成が選択肢に挙がります。

この記事が、CloudFront + S3構成でホスティングが必要になった際のサンプル、マニュアルになれば幸いです！



# 作成してみよう
## 1. Reactプロジェクトの作成

最初にReactのプロジェクトを作成しましょう！

React開発ツールとして以前は「**create-react-app**」が利用されていたのですが、今では**非推奨になっています。** 今回は、代替ツールとしてよく利用されている「**Vite**」を利用します。

https://ja.vite.dev/

まずは、作業用のディレクトリを作成します。「**frontend**」というディレクトリを作成して、移動します。
```
% mkdir frontend && cd frontend
```

次に、以下のコマンドを実行し、Vite + Rreact のプロジェクト作成を開始します。

```
% npm create vite@latest
```

すると、以下のようにプロジェクト作成が始まります。

以下のメッセージに関しては「**y**」で大丈夫です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/8800cf96-c4c7-f4c1-cc85-f68514aa9592.png" width=500>

「**Project name**」は、お好きなプロジェクト名を入力します。こちらがプロジェクトのルートディレクトリ名になります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/46cc7214-a79c-375e-70ee-2ed49b3b1508.png" width=400>

フレームワークの選択は「**React**」を選択。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/5c80f4e4-d9eb-b53a-98da-b7b5dc8893b0.png" width=500>

言語に関しては、今回は簡単のために「**JavaScript**」を選択。（本来はTypeScriptを選ぶことが多いかと思います。）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/df1d86b6-8fd7-27ef-6329-aa98e6afff64.png" width=500>

一通り設定が終わると、ViteによるReactプロジェクトが作成されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/2ae5b061-4117-3e69-098c-ccc248cd6533.png" width=500>

作業ディレクトリ（今回だとfrontendディレクトリ）に以下のようなディレクトリが作成されていれば、無事完了です！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/ac290a4b-da3b-737b-ff56-881259caeef7.png" width=500>

早速動かしてみましょう。
以下のコマンド群を実行します。

```
% npm install
% npm run dev
```

すると、以下のようにローカルサーバーが起動しますので、「**Local:〜**」 に表示されているURLをブラウザで開きます。（例だとhttp\://localhost:5173）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/a1e0961c-0e08-14aa-b3fb-cdfa1b232878.png" width=500>

以下のような画面が開かれたらOKです！

![スクリーンショット 2024-12-30 20.04.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/8df88e30-300b-fb6c-b59c-532df6f1052f.png)

## 2. 一旦CloudFront + S3でホスティングしてみる

先ほどはローカルサーバーでアプリケーションが動くことを確認したので、今度は**CloudFront + S3でホスティング**してみたいと思います。

![test 2.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/00c72996-dcc9-2d27-7e9d-1d0fa67a7aa2.png)


### 2.1. S3バケットの作成

WebアプリケーションのオリジンとなるS3バケットを作成します。

S3のコンソールの「**バケットを作成**」から新規バケットを作成します。

![スクリーンショット 2025-01-05 20.29.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/da3d87b9-1601-fb04-c167-ae65f85584cf.png)

バケット名は完全に一意である必要があるため、注意が必要です。

![スクリーンショット 2024-12-30 20.13.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/c4482db8-3331-a5cd-f682-61eed0182d6a.png)

その他の設定はそのままで作成で大丈夫です。


### 2.2. CloudFrontディストリビューションの作成

次に、CloudFrontディストリビューションを作成していきます。

:::note info
以降の作業は**東京リージョン**で実施します。
:::

CloudFrontのコンソールを開き、「**ディストリビューションを作成**」から新規ディストリビューションを作成します。

![スクリーンショット 2024-12-30 20.14.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/969e51e4-7008-bf4c-a385-8debcbe1d02b.png)

作成画面で、まず「オリジン」として先ほど作成したS3バケットを選択します。オリジン名はそのままでも大丈夫です。
「オリジンアクセス」は「**Origin access control settings**」を選択し、「Create new OAC」から新規でOrigin access controlを作成します。

![スクリーンショット 2025-01-05 20.35.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/608ecc72-45ea-e290-f1bc-5f587f5d5a78.png)

OACの作成モーダルが開くので、特に何も変えずに「Create」します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/c52f8280-fd00-caee-7e82-0f7bb797c474.png" width=400>

ディストリビューション作成画面に戻るので、「Origin access control」で作成したOACを選択します。「**S3バケットポリシーを更新する必要があります**」というメッセージが出ますが、後で対応するので一旦無視します。

![スクリーンショット 2025-01-05 20.42.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/14008e6c-2c4e-fb16-e426-fd744eb56f9a.png)

「デフォルトのキャッシュビヘイビア」、「関数の関連付け」、「ウェブアプリケーションファイアウォール (WAF) 」の設定は特に変更なし、「設定」欄の「**デフォルトルートオブジェクト**」に「**index.html**」と入力して、「ディストリビューションの作成」を押下します。

![スクリーンショット 2024-12-30 20.29.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/922c403f-6a50-b71b-c1cb-d10d960e4174.png)


ディストリビューションの作成が完了したら、以下のようなメッセージが表示されます。


![スクリーンショット 2024-12-30 20.18.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/382c6ac6-93f7-9812-21a7-79f026b9ed59.png)

ここで改めてバケットポリシーの更新を促されるので、「ポリシーをコピー」からPolicy statementをコピーします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/482881f1-261a-57a6-876e-5d216066a6ad.png" width=200> 

オリジンとして設定したS3のバケットポリシーを更新します。バケット詳細画面を開き、「アクセス許可」タブの「**バケットポリシー**」から更新します。
先ほどコピーしたポリシーステートメントを貼り付けて、保存します。

![スクリーンショット 2024-12-30 20.18.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/ad8a5e7e-458d-d4f3-2183-6e4180d96452.png)

これでCloudFrontディストリビューションの作成は完了です！


### 2.3. WebアプリケーションをS3にアップロード

CloudFrontの準備は出来たので、WebアプリケーションをオリジンのS3バケットにアップロードします。

以下のコマンドを実行して、**Reactプロジェクトをビルド**しましょう

```
npm run build
```

以下のような感じになれば、ビルド成功です！

![スクリーンショット 2024-12-30 20.20.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/d56ce0f3-88b8-ce91-3864-3562802028d3.png)


プロジェクト直下に「**dist**」というディレクトリが出来ていることも確認しておきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/231ceac3-1581-e153-f224-fa03761a4c8e.png" width=400>

ビルドが完了したら、distディレクトリの中身をS3バケットにアップロードします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/df549901-2018-93fd-1d89-737e4edc2f1f.png" width=400>

これで、ホスティングの一連の流れが完了です！


### 2.4. 動作確認

では、動作確認をしてみましょう。

CloudFront詳細画面の「**ディストリビューションドメイン名**」をコピーして、ブラウザで開いてみます。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/08885b33-d620-c6ad-b5c2-78b7b72e5b84.png" width=450>

このような感じでローカルサーバーを建てた際と同じようなページが開かれたら成功です！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/aa7bbc26-4900-17b1-c1aa-ab315d37a785.png" width=600>



## 3. Cognitoでの認証機構を追加

これでWebアプリケーション（単なるウェルカムページですが...）のホスティングができました。次は認証機構を追加したいと思います。

![test 3.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/4be2ecf9-1ba8-282e-860b-4fb0a006fb56.png)


### 3.1. Cognitoユーザープールの作成

まずは、Cognitoのユーザープールを作成します。

Cognitoのコンソールのユーザープール一覧から、「ユーザープールを作成」を押下します。

![スクリーンショット 2025-01-07 0.14.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/8757e1bc-5bf1-8ad3-74dc-6811f99471c6.png)

すると、アプケーションの設定画面が開きますので、「アプリケーションを定義」で「**シングルページアプリケーション（SPA）**」を選択、「アプリケーションに名前を付ける」には適当な名前を入力します。

![スクリーンショット 2024-12-30 20.33.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/cea967b8-b465-1cb5-90dc-7cf051fa0072.png)

「オプションの設定」では、サインイン属性を選択できます。今回は「**メールアドレス**」のみ選択します。

「サインアップのための必須属性」ではサインイン属性として選んだ属性のうち、サインアップ時に必ずユーザーに求める属性を選択します。今回は「**email**」を選択します。

![スクリーンショット 2024-12-30 20.34.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/94c16bc6-1a94-e7d8-d7d0-c8110b745d48.png)

:::note info
**注目ポイント: サインイン/サインアップの属性に関する設定は慎重に！**

サインイン/サインアップの属性の設定は、ユーザープールの作成後は変更することが出来ません。もし変更が必要な場合は再作成する必要があるため、慎重に設定を行いましょう。
:::

「リターンURL」はサインイン後に戻るコールバック先のページのURLになります。今回は１ページのみなので、**CloudFrontのディストリビューションドメイン**を指定します。

![スクリーンショット 2024-12-30 20.35.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/d15301ea-e88f-b8c6-e289-b9cc19c42fbb.png)


ここまで入力したら、ページ下部の「作成」を押下してユーザープールを作成します！

### 3.2. フロントエンドで認証

ユーザープール作成が完了したら、**Quick Setupガイド**なるものが表示されます。今はこんなのあるんですね！！
今回はReactでアプリケーションを作成しているので、「**React**」を選択します。すると、**Cognitoでログイン機能を実装するためのサンプルコードを提示してくれます。**

![スクリーンショット 2024-12-30 20.43.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/4b4135fb-941b-c5a1-a474-abb335686b62.png)

手順①の設定は一旦触らずに、手順②から進めます。
**oidc-client-ts**と**react-oidc-context**というライブラリが必要になるので、手順にあるコマンドを実行します。

```
npm install oidc-client-ts react-oidc-context --save
```

:::note info
**注目ポイント: Cognitoから提供されるマネージドサインイン画面**

Reactから利用できるCognitoの認証UIといえば「**ui-react**」が代表的なライブラリですが、Cognitoのアップデートにより上記の新しいUIが提供されています。こちらは後述する認証用Contextも提供してくれているので、個人的には使い勝手が良さそうに感じます。
:::

インストールが完了したら、手順③にあるサンプルコードを現在のプロジェクトに反映していきます。サンプルに「index.js」とありますが、今回のプロジェクトだと対応するファイルは「main.jsx」になります。

![スクリーンショット 2024-12-30 20.43.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/d127a3a0-f7f3-cabf-d3a7-9d6738da684c.png)


サンプルコードを反映させたmain.jsxが以下になります。
Appコンポーネントを 「**AuthProvider**」 でラップするイメージです。

```jsx: main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { AuthProvider } from "react-oidc-context";
import './index.css'
import App from './App.jsx'

const cognitoAuthConfig = {
  authority: "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_ユーザープールID",
  client_id: "アプリケーションクライアントID",
  redirect_uri: "http://localhost:5173",
  response_type: "code",
  scope: "phone openid email",
};

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <AuthProvider {...cognitoAuthConfig}>
      <App />
    </AuthProvider>
  </StrictMode>
)
```

次は、手順④のサンプルコードです。こちらは今回のプロジェクトだと「App.jsx」に対応しています。

![スクリーンショット 2024-12-30 20.45.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/f064ec5f-1b48-0dfe-1d8c-b4e5c654fca3.png)

「**useAuth**」からCognitoの認証情報を取得することができます。main.jsxで追加した**AuthProviderはContext**であるため、Appコンポーネント配下のコンポーネントは全てuseAuthで認証情報を取得できます。
今回は試しにサインインしたユーザーのメールアドレス、IDトークン、アクセストークン、Refreshトークンを表示してみます。

```jsx:App.jsx
import { useAuth } from "react-oidc-context";
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

function App() {
  const auth = useAuth();

  if (auth.isAuthenticated) {
    return (
      <>
        <div>
          <a href="https://vite.dev" target="_blank">
            <img src={viteLogo} className="logo" alt="Vite logo" />
          </a>
          <a href="https://react.dev" target="_blank">
            <img src={reactLogo} className="logo react" alt="React logo" />
          </a>
        </div>
        <h1>Vite + React</h1>
        <div>  
          <pre> Hello: {auth.user?.profile.email} </pre>
          <pre> ID Token: {auth.user?.id_token} </pre>
          <pre> Access Token: {auth.user?.access_token} </pre>
          <pre> Refresh Token: {auth.user?.refresh_token} </pre>

          <button onClick={() => auth.removeUser()}>Sign out</button>
        </div>
        <p className="read-the-docs">
          Click on the Vite and React logos to learn more
        </p>
      </>
    )
  }

  return (
    <div>
      <button onClick={() => auth.signinRedirect()}>Sign in</button>
    </div>
  );
}

export default App
```

それでは、フロントエンドのコードの修正が完了したので、**2.3**.と同じ手順でコードをビルドし、S3バケットにアップロードしましょう。


### 3.3. 動作確認

S3バケットにアップロード後、直ぐにWebアプリケーションにアクセスしても、キャッシュが残っていて変更が反映されていない場合があります。
なので、CloudFrontの「**キャッシュ削除**」**からキャッシュを削除**しましょう。

![スクリーンショット 2024-12-30 20.58.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/89e8ac9a-fafc-63cb-24a7-d1a2d3921b4e.png)

キャッシュ削除タブを開き、削除ボタンを押下すると、以下のようなオブジェクトパスを入力する画面に移ります。ここに「/*」と入力し、「キャッシュ削除の作成」を押下してキャッシュを削除します。

![スクリーンショット 2024-12-30 20.58.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/b6396b0f-c3b1-0ed6-af5f-7bba0bfe2834.png)

削除完了したら、動作確認です！

**2.4**.と同様に、ブラウザからCloudFrontのディストリビューションドメインにアクセスします。
「**Sign in**」ボタンだけのページが表示されたら成功です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/aed15f5a-6564-785b-bf3e-950adafbb541.png" width=600>

Sing inボタンを押下すると、Cognitoから提供されているサインイン画面が開きます。
まだユーザーがないので、今回は「**Create an account**」を押下します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/03d53108-bb42-6e55-2d68-18547ba86ae0.png" width=600>

サインアップ画面に遷移するので、必要な情報を入力していきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/9425012f-191d-66d4-dd95-cdbee4782ad9.png" width=600>

パスワードを入力する際に、ちゃんとバリデーションもかかっています！素晴らしい！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/2383be6f-6973-b5a7-04db-22a3f2535455.png" width=600>

必要な情報を入力し「Sign up」ボタンを押下すると、確認コードが指定したメールアドレスに送信されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/b306ccfe-aa23-fdbe-1981-a4895dc4abf8.png" width=600>

届いた確認コードを入力し、「Confirm account」を押下すると、アプリケーションが開きます！
ほとんどぼかしていますが、各種トークンが取得でき、ページに表示できていることが分かります！


![スクリーンショット 2024-12-30 23.39.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/395af4ef-1a77-db88-9125-52a7cb40e7c1.png)




## 4. APIの実装

実際のWebアプリケーションは、ページ上から何らかのAPIを実行できることがほとんどかと思います。（データベースから情報を取ってきたり、何かフォームに入力して送信したり...）

このステップでは**API Gateway、Lambda**を利用して、アプリケーションから実行できるAPIを実装します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/834017d5-a092-c86e-d81f-d3d317c70a1b.png" width=400>


### 4.1. APIの処理を担うLambdaの作成

APIを実装する前に、APIの処理を担うLambdaを先に作成します。

#### 4.1.1. データベースからデータを取得する処理の実装

まずは、DynamoDBからデータを取得してくるLambdaです。

![test 4 2.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/e2eb8fd8-be15-29ee-e1a2-43a476a939d8.png)

DynamoDBのテーブルは以下のようなものを作成します。（作成手順は省略します）
- テーブル名：**react-frontend**
- パーティションキー: **id（文字列）**

テーブルには以下のようなデータを投入します。
![スクリーンショット 2025-01-09 1.14.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/aa70047d-04c3-25c2-fa86-db95dccd04ac.png)

Lambdaは以下のようなものを作成します。（こちらも作成手順は省略します）
- Lambda名：**get_sentence**
- ライタイム：**Node.js 22x**
- アーキテクチャ: x86_64
- 実行ロール: 「**基本的なLambdaアクセス権限で新しいロールを作成**」

作成後、Lambdaの詳細画面の「コード」タブから以下のようなコードを入力します。

![スクリーンショット 2025-01-09 1.19.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/1a7a3bc6-1f1b-322b-fdd6-82fef777bc02.png)

```javascript:index.mjs
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb"; 

const ddb = new DynamoDBClient();

export const handler = async (event) => {
  const input = {
    TableName: "react-frontend",
    Key: { 
      "id": { S: "1" },
    }
  };

  const command = new GetItemCommand(input);
  const data = await ddb.send(command);

  const response = {
    headers: {
      "Content-Type": "application/json",
      "Access-Control-Allow-Origin": "*"
    },
    statusCode: 200,
    body: JSON.stringify('Welcome to React World!'),
  };
  return response;
};
```
:::note info
**注目ポイント: Lambdaのreturn値について**

内容としては、DynamoDBのAWS SDKを利用して先ほど作成したテーブルからデータを取得するだけの単純なものになりますが、APIの処理を実現するLambdaの場合は、returnする内容に以下のような**headers**を含めることが重要です！
```javascript
headers: {
  "Content-Type": "application/json",
  "Access-Control-Allow-Origin": "*"
}
```
:::

また、このLambdaはDynamoDBを参照するため、Lambda作成時に自動的に作成されたIAMロールのポリシーを以下のように修正しておきます。

```:get_sentenceのポリシー
"Statement": [
    {
        "Effect": "Allow",
        "Action": "logs:CreateLogGroup",
        "Resource": "arn:aws:logs:ap-northeast-1:アカウントID:*"
    },
    {
        "Effect": "Allow",
        "Action": [
            "logs:CreateLogStream",
            "logs:PutLogEvents"
        ],
        "Resource": [
            "arn:aws:logs:ap-northeast-1:アカウントID:log-group:/aws/lambda/get_sentence:*"
        ]
    },
    {
        "Sid": "Statement1",
        "Effect": "Allow",
        "Action": [
            "dynamodb:GetItem"
        ],
        "Resource": [
            "*"
        ]
    }
]
```

#### 4.1.2. 生成AIモデルとやり取りする処理の実装

もう一つ、Bedrockを利用して生成AIモデルとやり取りするLambdaを実装します。

![test 4 3.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/40e0fe78-1bff-e53b-aac4-e701436cba93.png)


Lambda実装の前にBedrockのコンソールにて以下のモデルを有効化しておいてください。
- **Claude 3.5 Sonnet**

Bedrockのモデル有効化が終わったら、get_sentenceと同様に以下のようなLambdaを作成します。
- Lambda名：**post_bedrock**
- ライタイム：**Node.js 22x**
- アーキテクチャ: x86_64
- 実行ロール: 「**基本的なLambdaアクセス権限で新しいロールを作成**」

Lambda作成後、「コード」タブから以下のようなコードを入力します。

```javascript:index.mjs
import {
  BedrockRuntimeClient,
  InvokeModelCommand,
} from "@aws-sdk/client-bedrock-runtime";

const MODEL_ID = "anthropic.claude-3-5-sonnet-20240620-v1:0";

export const handler = async (event) => {
  const body = JSON.parse(event.body);

  const client = new BedrockRuntimeClient();

  const payload = {
    anthropic_version: "bedrock-2023-05-31",
    max_tokens: 1000,
    messages: [{ role: "user", content: [{ type: "text", text: body["text"]}] }],
  };
  
  const apiResponse = await client.send(
    new InvokeModelCommand({
      contentType: "application/json",
      body: JSON.stringify(payload),
      modelId: MODEL_ID,
    }),
  );

  const decodedResponseBody = new TextDecoder().decode(apiResponse.body);
  const responseBody = JSON.parse(decodedResponseBody);
  const responses = responseBody.content;

  const response = {
    headers: {
      "Content-Type": "application/json",
      "Access-Control-Allow-Headers" : "Content-Type",
      "Access-Control-Allow-Origin": "*"
    },
    statusCode: 200,
    body: JSON.stringify(responses[0]["text"]),
  };
  return response;
};
```

こちらも、Lambdaに渡された「event」に含まれる内容をBedrockのモデルへの入力とし、そのレスポンスを返すだけの単純な処理です。get_sentenceと同様に、returnの内容に**headers**を含めることがキモになります。

このLambdaはBedrockにアクセスするため、ポリシーを以下のように修正してください。

```:post_bedrockのポリシー
"Statement": [
    {
        "Effect": "Allow",
        "Action": "logs:CreateLogGroup",
        "Resource": "arn:aws:logs:ap-northeast-1:アカウントID:*"
    },
    {
        "Effect": "Allow",
        "Action": [
            "logs:CreateLogStream",
            "logs:PutLogEvents"
        ],
        "Resource": [
            "arn:aws:logs:ap-northeast-1:アカウントID:log-group:/aws/lambda/post_bedrock:*"
        ]
    },
    {
        "Sid": "Statement1",
        "Effect": "Allow",
        "Action": [
            "bedrock:InvokeModel"
        ],
        "Resource": [
            "*"
        ]
    }
]
```

また、Bedrockからのレスポンスを得るまでに少し時間がかかるため、「設定」タブからLambdaのタイムアウト時間を**1分**に変更しておきます。

![スクリーンショット 2025-01-05 13.24.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/2c499ab6-4ce2-d73f-7aa3-ff78c3ce8094.png)



### 4.2. API Gatewayの作成

裏の処理が実装できましたので、APIを実装していきます！

#### 4.2.1. データベースからデータを取得するAPIの実装

まずはデータベースからデータを取得するAPIの実装から。

API Gatewayのコンソールから「APIの作成」を押下し、作成画面を開きます。

今回は**REST API**を作成するので、「**REST API**」の「構築」を押下します。

![スクリーンショット 2025-01-02 2.25.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/cf8be898-1143-b5f9-7fe2-583a3b480bbe.png)

REST APIの作成画面に遷移します。「API名」にAPI名を入力し（記事では例として「frontend-api」）、「エンドポイントタイプ」は「リージョン」で「APIを作成」を押下し、APIを作成します。

![スクリーンショット 2025-01-02 2.25.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/d339a520-bc9e-257a-5ce5-945a9cc95a6d.png)

APIの作成が完了したら、以下のようなリソースの画面に遷移しますので、「リソースの作成」を押下します。

![スクリーンショット 2025-01-02 2.26.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/110f0912-3a64-a2c2-b1bf-dac5a862d9ed.png)

リソース作成画面で、リソースパスに「**/**」、リソース名には「**sentence**」と入力します。こちらがAPI実行時のパス名になります。また、「**CORS（クロスオリジンリソース共有）**」**に忘れずにチェック**をつけて、「リソースを作成」します。

![スクリーンショット 2025-01-02 2.27.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/811eb291-a563-e2cf-2c46-c4dab49361a8.png)

:::note info
**注目ポイント: CORSにチェックを忘れずに！**

この**CORS**にチェックをつけることがポイントになります。これを忘れると、API実行時に**CORSエラー**が発生してしまいます。
CORSの解説をしようとすると、それだけで一つの記事になってしまうので、詳しく知りたい方は以下の記事をご参考ください。

[オリジン間リソース共有 (CORS) - HTTP - MDN Web Docs](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)
:::

リソースの作成ができたら、「メソッドを作成」しましょう。

![スクリーンショット 2025-01-02 2.27.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/c0e4627a-4fc4-315c-5e9c-0e525cef12eb.png)

メソッド作成画面では、以下のように設定します。
- ①メソッドタイプは「**GET**」を選択します
- ②統合タイプは裏の処理をLambdaで実装しているので「**Lambda関数**」を選択します
- ③「**Lambdaプロキシ統合**」にチェックをつけます
- ④Lambda関数は先ほど作成した「**get_sentence**」を選択します

以上でGETメソッドの作成は完了です！

![スクリーンショット 2025-01-02 2.28.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/0348bbc3-b2c1-c616-6da9-9d0168c4c697.png)


作成したメソッドの動作確認をしてみましょう。
メソッド詳細の「**テスト**」タブを開きます。

![スクリーンショット 2025-01-02 2.28.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/457bbbfc-7c3d-bbbc-17ac-16616eccecf5.png)


「テストメソッド」が開くので、今回は特に何も入力せずに「**テスト**」を押下してテスト実行します。

![スクリーンショット 2025-01-02 2.29.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/7bfca10d-4d97-a7b0-e0af-f5bcbcfdf6d2.png)

以下のように、データベースに登録した値である「**Welcome to React World!**」が帰ってくれば成功です！

![スクリーンショット 2025-01-02 2.29.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/981787f1-5007-9187-da5e-993e41ebcf48.png)

これで一つAPIが作成できました！
次はBedrockとやり取りするAPIを作っていきます！



#### 4.2.2. 生成AIモデルとやり取りするAPIの実装

リソース画面にて新しく「リソースを作成」を開きます。
リソースパスに「**/**」、リソース名には「**message**」と入力します。**CORSにチェックを入れるのを忘れずに！**

![スクリーンショット 2025-01-02 2.35.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/ed1d7a95-2198-b6c2-3b78-967e7e3d9e77.png)

メソッドの作成に移ります。今回はメソッドタイプに「**POST**」を選択します。統合タイプは同様に「Lambda関数」を選択し、プロキシ統合にもチェック。Lambda関数には「**post_bedrock**」を選択し、メソッドを作成します。

![スクリーンショット 2025-01-02 2.36.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/7bc870f8-a076-6f9b-b753-f27add65251d.png)


同様にテストしてみましょう。
今回はBedrockへの入力をリクエストパラメータとしてAPIに渡すため、「リクエスト本文」に以下のようなパラメータを設定し、テスト実行します。
```
{
   "text": "こんにちは！"
}
```
![スクリーンショット 2025-01-02 2.37.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/59133e34-1c74-cd6b-29f6-451003ddca10.png)

以下のようにBedrockからのレスポンスが返ってきたら成功です！

![スクリーンショット 2025-01-02 2.52.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/9e218cb7-7012-e21e-a257-65582675d96e.png)

### 4.3. APIのデプロイ
リソースの作成が一通り完了したので、APIをステージにデプロイしましょう。
リソース画面の右上の「**APIをデプロイ**」を押下します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/998a7db4-8212-60dc-198f-34988fc9fbb2.png" width=400>


デプロイモーダルが開くので、「ステージ」に「**新しいステージ**」、「ステージ名」に「**api**」と入力し、「**デプロイ**」します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/75cf4fff-bc00-3f45-e908-ee2bbc72fe54.png" width=400>

デプロイが完了したら、ステージ画面に遷移します。「**URLを呼び出す**」に表示されているURLにリソースパスを付けてアクセスすると、APIを実行出来るようになります。

![スクリーンショット 2025-01-02 2.53.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/3ca92578-7b5d-7d7d-2c97-e13a18c1be1e.png)


:::note info
**注目ポイント: APIのデプロイは忘れずに！**

コンソール上でAPIを作成/変更した際は、必ずこの**デプロイ**を忘れないようにしましょう。デプロイしないと編集した内容が反映されません。（慣れないうちはデプロイを忘れて「あれ？」となることが多いです...）
:::

これでAPIの作成が一通り完了したのですが、せっかくアプリケーションにCognitoで認証をかけるので、**APIもCognitoでサインインしたユーザーのみ実行出来るようにしたい**と思います！


### 4.4. オーソライザーの設定

APIの認証は、左メニューの「**オーソライザー**」から設定できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/4049c4b9-58d2-0379-677f-7c433c912eaf.png" width=300>


「オーソライザー」を開くと一覧画面が開くので、「オーソライザーを作成」していきます。

![スクリーンショット 2024-12-31 1.02.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/9802e27f-2882-a5e4-9d45-715938154f52.png)

オーソライザー作成画面で、以下の設定を行い、作成します。
- ①オーソライザー名は「**cognito-authorizer**」と入力
- ②オーソライザーのタイプは「**Cognito**」を選択
- ③Cognitoユーザープールは**3.1**.で作成したものを選択
- ④トークンのソースは「**Authorization**」と入力

![スクリーンショット 2024-12-31 1.03.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/e535805e-5156-33d3-564d-ef923d869f9f.png)

オーソライザーの作成が完了しました！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/e9ebc438-4c2c-9e3e-7371-1d2d8166914f.png" width=400>

それではオーソライザーをAPIに設定していきましょう。
リソースパス「/sentence」の**GETメソッド**における「**メソッドリクエスト**」を編集します。

![スクリーンショット 2025-01-12 17.57.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/d0f7fba7-f812-70a0-b6b4-72b652a7025e.png)


「メソッドリクエストの設定」欄の「**認可**」で先ほど作成したオーソライザーを選択して、その他設定は触らずに保存します。これで、**「GET /sentence」はCongito認証を通した場合のみ実行出来るようになりました。**

![スクリーンショット 2025-01-02 3.12.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/97c0cd23-420a-2925-4ac9-6a89de72a121.png)
**※ 編集後、APIの「デプロイ」を忘れずに！**



動作確認として、curlコマンドを使って認証情報を含めずにAPIを実行してみましょう。

```
curl -X GET https://APIのID.execute-api.ap-northeast-1.amazonaws.com/api/sentence
```

実行してみると、以下のように「**Unauthorized**」が返ってくるので、認証されていないアクセスは弾かれていることがわかります。

```
% curl -X GET https://APIのID.execute-api.ap-northeast-1.amazonaws.com/api/sentence
> {"message":"Unauthorized"}
```

次に、認証情報を含めてAPIを実行してみます。

認証情報として、Cognito認証が通った後に発行される**IDトークン**をリクエストヘッダー（**Authorizationヘッダー**）に含めることになるので、まずはIDトークンを取得します。以下のコマンドを実行してください。（ユーザーは**3.3**.で作成したものを利用します。）

```
aws cognito-idp admin-initiate-auth --user-pool-id CognitoユーザープールID --client-id アプリクライアントID --auth-flow ADMIN_NO_SRP_AUTH --auth-parameters USERNAME=ユーザ名,PASSWORD=パスワード
```
実行すると以下のような結果が返ってくるので、その中のIDトークンを利用します。

```
{
    "ChallengeParameters": {},
    "AuthenticationResult": {
        "AccessToken": "xxxxxx",
        "ExpiresIn": 3600,
        "TokenType": "Bearer",
        "RefreshToken": "xxxxxx",
        "IdToken": "xxxxxx"
    }
}
```

IDトークンをヘッダーに含めた場合のAPI実行コマンドは以下のようになりますので、実行してみます。

```
curl -X GET -H "Authorization: Bearer IDトークン" \ 
https://APIのID.execute-api.ap-northeast-1.amazonaws.com/api/sentence
```
認証を通しているので、APIの実行に成功しました！

```
% curl -X GET -H "Authorization: Bearer IDトークン" \
　https://APIのID.execute-api.ap-northeast-1.amazonaws.com/api/sentence
 
> "Welcome to React World!"
```

:::note
**注目ポイント: OPTIONメソッドにオーソライザーをつけるべき？**

OPTIONメソッドにオーソライザーと付けていませんが、忘れているわけではありません。OPTIONメソッドにオーソライザーをつけると、認証を通していても401エラーになってしまいます。

詳しくは省略しますが、**OPTIONメソッドはブラウザが実際のリクエスト（今回はGET）を行う前に自動的に行うプリフライトリクエストであり、Authorizationヘッダーが含まれないから**です。認証情報がないリクエストなのに、オーソライザーが設定されているので401エラーになってしまう、というわけです。
:::

「/sentence」のオーソライザー設定が完了しましたので、**「/message」の設定も同様に実施します。（手順は省略）**
設定/デプロイが完了したら、以下のcurlコマンドを実行して動作確認しましょう。

```
% curl -X POST \ 
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer IDトークン" -d '{"text": "こんにちは！"}' \ 
  https://APIのID.execute-api.ap-northeast-1.amazonaws.com/api/message
  
> "こんにちは！お元気ですか？何かお手伝いできることはありますか？ご質問やお話したいトピックがあれば、お気軽におっしゃってください。"
```

こちらも問題なさそうですね！

以上で、APIのオーソライザー設定が完了です！


## 5. フロントエンドからAPIを実行できるようにする

それではいよいよ作成したAPIをフロントエンドに組み込んでいきましょう！

### 5.1. フロントエンドの構成の修正

組み込みにあたり、ついでにフロントエンドの構成も以下のように変更してみます。

```
react-project/
    ├── src/
    │   ├── assets/
    │   │   └── react.svg
    │   ├── components/
    │   │   ├── css/
    │   │   │   └── header.css
    │   │   ├── ProtectRoute.jsx
    │   │   ├── Header.jsx
    │   │   └── Layout.jsx
    │   ├── pages/
    │   │   ├── HomePage.jsx
    │   │   └── Login.jsx
    │   ├── services/
    │   │   └── api.js
    │   ├── utils/
    │   │   └── AuthToken.js
    │   ├── App.css
    │   ├── App.jsx
    │   ├── index.css
    │   └── main.jsx
    ├── .env
    └── .env.production
```
- **components**: ページ間で共通のコンポーネントをここに作成していきます
    - **ProtectRoute.jsx**: 認証情報が有効かどうかをチェックするコンポーネント
    - **Header.jsx**: ページ上部に表示するヘッダーのコンポーネント
    - **Layout.jsx**: ヘッダーなどをページ間で共通して表示するコンポーネントを配置
- **pages**: ページコンポーネント
    - **HomePage.jsx**: ホームページ
    - **Signin.jsx**: サインインページ
- **services**:
    - **api.js**: APIの呼び出し処理をまとめている
- **utils**:
    - **AuthToken.js**: Cognitoの認証情報をセッションストレージから取得する関数
- **.env、.env.production**: 環境変数ファイル。詳細は後述

:::note info
今回の改修でページが複数になりルーティングが必要になるので、**react-router-dom**のインストールしておきます。
```
npm install react-router-dom
```
:::

### 5.2. 簡単なコンポーネントの解説（ホームページを除く）

**main.jsx**は以下のように変更しています。
```jsx: main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { AuthProvider } from "react-oidc-context";
import './index.css'
import App from './App.jsx'

export const cognitoAuthConfig = {
  authority: import.meta.env.VITE_COGNITO_AUTHORITY,
  client_id: import.meta.env.VITE_COGNITO_CLIENT_ID,
  redirect_uri: import.meta.env.VITE_COGNITO_REDIRECT_URI,
  response_type: "code",
  scope: "phone openid email",
  loadUserInfo: true,
};

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <AuthProvider {...cognitoAuthConfig}>
      <App />
    </AuthProvider>
  </StrictMode>,
)
```
**cognitoAuthConfig**のパラメータに**loadUserInfo**を追加しています。こちらを有効にすることで、認証情報が**ブラウザのセッションストレージ**に保持されます。
また、authority、client_id、redirect_uriは環境変数に切り出すことにしました。環境変数のファイルは以下になります。

```.env
# Viteを利用している場合、変数名の先頭に「VITE_」をつける必要がある
VITE_API_ENDPOINT="https://APIのID.execute-api.ap-northeast-1.amazonaws.com/api"
VITE_COGNITO_AUTHORITY="https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_CognitoのユーザープールID"
VITE_COGNITO_CLIENT_ID="CognitoのアプリケーションクライアントID"
#開発中はローカル実行する想定
VITE_COGNITO_REDIRECT_URI="http://localhost:5173/home"
```
```.env.production
# Viteを利用している場合、変数名の先頭に「VITE_」をつける必要がある
VITE_API_ENDPOINT="https://APIのID.execute-api.ap-northeast-1.amazonaws.com/api"
VITE_COGNITO_AUTHORITY="https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_CognitoのユーザープールID"
VITE_COGNITO_CLIENT_ID="CognitoのアプリケーションクライアントID"
VITE_COGNITO_REDIRECT_URI="https://CloudFrontのディストリビューションドメイン/home"
```

redirect_uriを**3.2**.の時点から変更して「**/home**」にリダイレクトするようにしています。
また、ローカルで動作確認しながら開発できるように、環境変数を「**.env**」と「**.env.production**」に分けています。
- **.env**
    - 環境変数の定義ファイル。**npm run dev** などでローカル実行する際に読み込まれる
- **.env.production**
    - こちらはビルドする際に読み込まれる。（ビルド時に.envよりも優先されるイメージ）


:::note info
**注目ポイント: サインイン後リダイレクト先の変更**

コード上で**redirect_uri**を変更しているので、Cognitoの設定も合わせて変更する必要があります。
Cognitoコンソールの左メニュー「アプリケーションクラアント」から「**ログインページ**」タブを開き、「**マネージドログインページの設定**」から「**許可されているコールバックURL**」をコードに合わせて変更しましょう。
- ローカル開発中：**http\://localhost:5173/home**
- ビルド/デプロイ時：**https\://CloudFrontのディストリビューションドメイン/home**

![スクリーンショット 2025-01-12 1.18.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/becaa65b-921b-0965-2bb0-7d4d02427aeb.png)
:::

**App.jsx**のコードは以下のように変更しています。
サインインページ、ホームページ、ページ間共通のパーツはコンポーネント化しているので、App.jsxではそれらの**ルーティングの定義がメイン**となっています。

```jsx: App.jsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { ProtectedRoute } from './components/ProtectedRoute';
import { Signin } from './pages/Signin';
import { HomePage } from './pages/HomePage';
import { Layout } from './components/Layout';
import './App.css'

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/signin" element={<Signin />} />
        <Route 
          path="/*" 
          element={
            <Layout>
              <Routes>
                <Route path="/home" element={
                  <ProtectedRoute>
                    <HomePage />
                  </ProtectedRoute>
                } />
              </Routes>
            </Layout>
          } 
        />
        <Route path="/" element={<Navigate to="/home" replace />} />
      </Routes>
    </BrowserRouter>
  );
}
```

サインインページのコードは以下になります。
**useAuth**を利用して認証情報を取得し、サインイン済みであればホームページに遷移、未サインインであれば**Sign in**ボタンが表示されます。**Sign in**ボタンを押下すると、**auth.signinRedirect**が呼び出され、Cognitoから提供されているサインイン画面にリダイレクトされます。

```jsx:Signin.jsx
import { Navigate } from 'react-router-dom';
import { useAuth } from "react-oidc-context";

export function Signin() {
  const auth = useAuth();

  if (auth.isLoading) return <div>Loading...</div>;
  
  if (auth.isAuthenticated) {
    return <Navigate to="/home" replace />;
  }

  return (
    <div>
      <h1>ログインページ</h1>
      <button onClick={() => auth.signinRedirect()}>Sign in</button>
    </div>
  );
}
```

**ProjectedRoute.jsx**は、ページを開く際にユーザーの認証が有効かどうかをチェックし、有効でなければサインページに遷移するようなコンポーネントになっています。

```jsx: ProtectedRoute.jsx
import { Navigate } from 'react-router-dom';
import { useAuth } from "react-oidc-context";

export function ProtectedRoute({ children }) {
  const auth = useAuth();
  
  if (auth.isLoading) {
    return <div>Loading...</div>;
  }
  
  if (!auth.isAuthenticated) {
    return <Navigate to="/signin" replace />;
  }
  return children;
}
```

**Header.jsx**はページ間で共通するコンポーネントとなっており、画面上部に表示されるヘッダーを実装しています。アプリケーションのタイトル表示だけだと味気ないので、サインアウトボタンも追加しています。

```jsx: Header.jsx
import { Navigate } from 'react-router-dom';
import { useAuth } from "react-oidc-context";
import "./css/header.css";

export function Header() {
    const auth = useAuth();

    const signOutButton = () => {
        auth.removeUser();
        
        return <Navigate to="/login" replace />;
    };

    return (
        <div className="header-area">
            <h2 className="header-text">Reactの勉強</h2>
            <button className="header-button" onClick={signOutButton}>
                ログアウト
            </button>
        </div>
    )
}

// header.css（別ファイル）
.header-area {
    background-color:gainsboro;
    height: 50px;
    display: flex;
    justify-content: space-between;
}

.header-text {
  font-size: 40px;
  margin: 0 auto;
  margin-inline-start: 20px;
  font-weight: bold;
  font-family: "Mochiy Pop One";
  width: 400px;
}

.header-button {
    transform: scale(0.8);
}
```
**Layout.jsx**は、上記のヘッダーのような画面をまたいで表示されるコンポーネントを配置するためのコンポーネントになります。

```jsx:Layout.jsx
import { Header } from "./Header";

export function Layout({ children }) {
    return (
      <>
        <Header />
        {children}
      </>
    );
}
```
App.jsx内で以下のように利用します。もしホームページ以外の画面を追加する場合は、**Routeコンポーネント**を追加していく感じになります。
```jsx
<Layout>
  <Routes>
    <Route path="/home" element={
      <ProtectedRoute>
        <HomePage />
      </ProtectedRoute>
    } />
    {/* <Route path="/xxxx" element={
      <ProtectedRoute>
        <xxxxx />
      </ProtectedRoute>
    } /> */}
    {/* .... */}
  </Routes>
</Layout>
```

ホームページ周りを除いたコンポーネントの簡単な解説は以上です。

### 5.3. ホームページからAPIを実行できるようにしてみる

コンポーネントからAPIを実行出来るようにするための、APIの呼び出し処理を**api.js**にまとめています。
```js: api.js
import { getAuthToken } from '../utils/AuthToken';

/*
* データベースからデータを取得するAPIを実行
* @return {String} 取得したデータ
*/
export const getSentence = async () => {
    // セッションストレージから認証情報を取得
    const auth = getAuthToken()

    // 認証情報をヘッダーに含め、APIを実行
    const res = await fetch(
      `${import.meta.env.VITE_API_ENDPOINT}/sentence`,
      {
        method: 'GET',
        headers: {
          'Authorization': 'Bearer '+ auth.token,
          'Content-Type': 'application/json',
        }
      });
    const data = await res.json()
    return data
}

/*
* Bedrockとやり取りするAPIを実行
* @param {String} ユーザーの入力
* @return {String} Bedrockのレスポンス
*/
export const postMessage = async (text) => {
    // セッションストレージから認証情報を取得
    const auth = getAuthToken()

    // 認証情報をヘッダーに含め、APIを実行
    const res = await fetch(
        `${import.meta.env.VITE_API_ENDPOINT}/message`,
        {
            method: 'POST',
            headers: {
                'Authorization': 'Bearer '+ auth.token,
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                'text': text
            })
        });
    const data = await res.json()
    return data
}
```
- **getSentence**: **4.2.1**.で実装した、**データベースからデータを取得するAPI**を実行
- **postMessage**: **4.2.2**.で実装した、**Bedrockとやり取りするAPI**を実行
リクエストする際に、後述する**getAuthToken**を利用して認証情報を取得し、ヘッダーに含めています。



**AuthToken.js**で、ブラウザのセッションストレージに格納されているユーザーの認証情報を取得して返す**getAuthToken**を実装しています。今回は利用していませんが、トークンと合わせて有効期限も返します。

```js: AuthToken.js
import { cognitoAuthConfig } from "../main.jsx";

/*
* セッションストレージからユーザーの認証情報を取得する
* @return {token: String, expiredAt: String} ユーザーの認証情報
*/
export function getAuthToken() {
    // セッションストレージから
    const oidcData = sessionStorage.getItem(
        `oidc.user:${cognitoAuthConfig.authority}:${cognitoAuthConfig.client_id}`,
    );

    if (!oidcData) {
        return null;
    }

    const authInfo = JSON.parse(oidcData);

    return {
        token: authInfo.id_token, // アクセストークン
        expiredAt: authInfo.expires_at, // 有効期限
    };
};
```

上記を踏まえて、API呼び出しを出来るようにしたホームページのコードが以下になります。

```jsx: HomePage.jsx
import { getSentence, postMessage } from '../services/api';
import { useAuth } from "react-oidc-context";
import { Navigate } from 'react-router-dom';
import { useState, useEffect } from 'react'


export function HomePage() {
  const auth = useAuth();
  const [sentence, setSentence] = useState("");
  const [text, setText] = useState("");
  const [messages, setMessages] = useState([]);

  // ページが読み込まれたタイミングでgetSentenceを実行する
  useEffect(() => {
    getSentence().then(sentence => setSentence(sentence))
  },[]);

  if (auth.isLoading) return <div>Loading...</div>;

  // 認証が切れていたらサインインページにリダイレクトする
  if (!auth.isAuthenticated) {
    return <Navigate to="/signin" replace />;
  }

  // useAuthで取得した情報からユーザー名（メールアドレス）を取り出す
  const userName = auth.user?.profile.email

  // postMessageを実行する
  // Bedrockとのやりとりに置いて、ユーザーの入力とBedrockからのレスポンスをリアルタイムに画面に表示するように、状態を更新する
  const post = async () => {
    setMessages(prevResults => [...prevResults, {actor: "user", content: text}])
    const response = await postMessage(text);

    setMessages(prevResults => [...prevResults, {actor: "ai", content: response}])
  }

  return (
    <>
        <div>
            <h2>ようこそ {userName} さん</h2>
            <p>{sentence}</p>
            {messages.map((item) => {
                return (
                <div>
                    { item.actor == "user" ? "あなた" : "AI"} : {item.content}
                </div>
                );
            })}
        </div>
        <textarea
            cols={100}
            rows={5}
            placeholder="何か入力"
            value={text}
            onChange={(e) => setText(e.target.value)}
        />
        <div>
            <button onClick={post}>
                POST
            </button>
        </div>
    </>
  );
}
```

api.jsで定義したAPIを然るべきタイミング実行し、その結果を表示しています。
- ページ読み込み時に**useEffect**により**getSentence**を実行。その結果をページ上に表示している
- テキストフィールドにユーザーが何かしら入力し、**POST**ボタンを押下したタイミングで、**postMessage**が実行される。ユーザーとBedrockのやりとりを状態で管理しており、POSTボタン押下時にユーザーの入力が、API実行完了時にBedrockからのレスポンスが表示されるようになっている。

これで、APIを実行できるフロントエンドの実装が完了です！

### 5.4. 動作確認

それでは動作確認をしてみましょう！

まずは実装したフロントエンドのコードをビルドします。
```:再掲:ビルドコマンド
npm run build
```
前述の通り、ビルドの際は環境変数として**.env.production**が読み取られるので、特にコードとしては意識する必要はありませんが、ローカル実行しつつ開発していた場合は、**Cognitoの「マネージドログインページの設定」のコールバックURLを戻すことを忘れないようにしましょう！**

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/0fdd8432-2d0f-7abe-43b8-78bf0ffafc0e.png" width=500>

ビルドが完了したら、成果物を**2.3**.の手順と同様にS3バケットにアップロードし、**3.3**.と同様にCloudFrontのキャッシュを削除しましょう。

:::note info
**注目ポイント: ページリロードの際に403エラーになってしまう！**

今回の修正でページ遷移が追加されました。この修正により、CloudFront+S3でホスティングする場合、現状のCloudFrontの設定だとリロードの際に403エラーになってしまいます。（SPAの特性によるものなのですが、詳しくは省略。）

こちらの対応策の一つとして、**CloudFrontディストリビューションのカスタムレスポンスで403エラーの際に「/」（今回だとホーム画面）に遷移するように**設定します。

ディストリビューション詳細画面の「**エラーページ**」タブから「**カスタムエラーレスポンスを作成**」。
![スクリーンショット 2025-01-03 13.22.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/e50d0b79-d311-ee7a-2961-a7f4dcff521b.png)

エラーレスポンス作成画面で以下のように設定し、カスタムエラーレスポンスを作成します。
![スクリーンショット 2025-01-03 13.23.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/6b377433-d940-1ea6-5849-3d69368a2aa7.png)

対応策の「一つ」と言いましたが、**Lambda@Edge (or CloudFront Functions)** を利用してレスポンスを操作する方法もありますので、是非調べてみて下さい！
（例えばメンテナンスページを設ける場合は上記の対応策が使えない可能性があるので、こちらを選択する方が安全かもしれません）
:::

CloudFrontのディストリビューションドメインにブラウザからアクセスすると、以下のようなサインインページが表示されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/61890ef1-5358-5fa2-985d-38a87abfe6f0.png" width=600>

「**Sign in**」ボタンを押下すると、Cognitoのマネージドサインインページが開きますので、サインインします。（**3.3**.の時に作成したユーザーでサインインしてください。）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/6b847a65-d93a-31bd-4be1-8e6e71688f12.png" width=600>

サインインに成功すると、ホームページが開きます！
下画像赤枠の部分に「**Welcome to React World!**」と表示されているので、**getSentence**が問題なく実行できていることがわかります。

![スクリーンショット 2025-01-04 1.13.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/42b24d73-06c2-a5c0-9fc3-f9faa891e362.png)

次は、Bedrockとやり取りしてみましょう。
テキストエリアに「**2025年をエンジニアとして良い年にするために、どのように過ごしたら良いですか？**」と入力し、「**POST**」ボタンを押下します

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/c24a2914-4a8c-4607-62f2-418db031482d.png" width=600>


下画像のようにBedrockからレスポンスが返ってきたら成功です！
無難な回答をしてくれているような気がします！

![https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_3026734_54c347c4-6024-a991-945f-9ce12459fe61.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/9e897429-161d-ad4b-3753-3b3c9b330d24.jpeg)

かなり長くなりましたが、これで**ReactによるWebアプリケーションの実装と、そのホスティングが完了しました！**


# Next Action

ここまで簡単なWebアプリケーションのホスティングまで出来ましたが、より良いものにしていくためのNext Actionとして以下のような対応が挙げられます。

### WAFでのアクセス制御
今回はURLさえ知っていれば誰でも利用できるアプリケーションの想定ですが、アクセスを制限する場合もあるかと思います。その際に、CloudFrontやAPIGatewayに**WAF:ウェブアプリケーションファイアウォール**を設定してアクセスを制御します。

また、開発中は開発者のみアクセスできるようにWAFで制限をかけ、商用リリース時にWAFを外す、といったような使い方も出来ます。

WAFの設定はAWSコンソール上から簡単に出来ますので、以下の公式ドキュメントを参考にしてみてください。
- WAFの利用方法：
  https://docs.aws.amazon.com/waf/latest/developerguide/web-acl.html
  
- CloudFrontにWAFを適用する方法：
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/WAF-one-click.html

### 独自ドメインの取得
現状ではCloudFrontのディストリビューション作成時に自動生成されるURLでアプリケーションにアクセスできるようになっていますが、もちろん**独自ドメインを取得し、それをURLとして利用することも可能**です。

AWSのサービスである **Route53** と **AWS Certificate Manager(ACM)** を利用します。
- **Route53**: AWSが提供するドメインネームサービス
- **ACM**: AWSが提供するSSL/TLS証明書の発行、管理、および自動化を行うフルマネージドサービス

諸々の設定方法に関しては本記事では省略しますが、以下の記事が参考になります。

https://dev.classmethod.jp/articles/cloudfront-custom-domain-name/

### フロントエンドのスタイリング

本記事ではフロントエンドの見た目を整える**スタイリング**をほとんどしていません。本来は**css**を利用してスタイリングを行ったり、**Material UI**のような**UIコンポーネントライブラリ**を利用することが多いです。以下のような様々な手法がありますので、自分の実現したい画面デザインに合ったものを選択しましょう。

- **スタイリング**
    - 従来のcss、sass、scssなどを利用する方法
    - **cssフレームワーク**を利用
        - [Tailwin](https://tailwindcss.com/)、[BLUMA](https://bulma.io/)、[Bootstrap](https://getbootstrap.jp/)など
- **UIフレームワーク**
    - [Material UI](https://mui.com/material-ui/?srsltid=AfmBOooi2yNK3f_rCaMZNZUy_QN3oij5nv6PPs-5ChU2cR9663tfgThi)
    - [Chakra UI](https://www.chakra-ui.com/)
    - [shadcn/ui](https://ui.shadcn.com/)
    など

### CI/CDの整備
本記事では、CloudFrontなどのリソースをAWSのコンソールから手動で作成していましたが、これらはコード化（**IaC: Infrastructure as Code**）して管理することができます。IaCツールの一つである**Terraform**の例だと、

1. 作成したいAWSリソースの定義のコードをTerraformで作成
2. Terraformのデプロイコマンドを実行することにより、リソースがAWSにデプロイされる


のような流れでリソースを管理、作成できるというワケです。

また、上記のIaCと組み合わせて、コードをGitHub等のリポジトリにpushやマージした際に、自動的に上記のようなデプロイコマンドやビルド/テストコマンドを実行させる、といったような**CI/CD**の整備も是非やっておきたいところです。

IaC、CI/CD共にさまざまなツール、手法がありますので、以下を参考にしてみて下さい！

- IaCのツール
    - [AWS CDK](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/home.html)
    - [Terraform](https://www.terraform.io/)
    - [CloudFormation](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/Welcome.html)
    など
- CI/CD
    - [AWSのCodeシリーズ](https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/how-to-create-pipeline.html)
    - [GitHub Actions](https://github.co.jp/features/actions)
    - [CircleCI](https://circleci.com/ja/)
    など


# 最後に
今回は久しぶりにフロントエンドを触る機会がありましたので、AWS上でWebアプリケーションを展開する方法についてのおさらいしてみました。

APIGatewayのCORS周りなどのハマりポイントなど、初見だとハマるポイントも色々解説出来たかと思いますので、これからWebアプリケーションを作成してみようと考えている方の参考になると幸いです。
Webホスティングの方法はCloudFront+S3以外にも、冒頭でさらっと触れた**AWS Amplify**や**App Runner**など色々ありますので、色々比較してみて実装したいWebアプリケーションにあったものを選択するのが良いかと思います。

本記事はこれで以上になります。ご高覧ありがとうございました！


# Appendix
### Congnitoのマネージドサインインページのデザイン変更

今回利用しているCognitoのマネージドサインインページですが、**実はデザインを変更することが出来ます。**

Cognitoユーザープールのアプリケーションクライアントページの「ログインページ」を開くと、「**マネージドログインのスタイル**」がありますので、ここから編集画面を開きます。

![スクリーンショット 2025-01-13 13.19.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/9fb8399b-bd16-53bc-0d2c-d08c5c2ad8a5.png)


プレビュー画面の右上の「**ブランディングデザイナーを起動**」から色々いじることが出来ます。

![スクリーンショット 2025-01-13 13.20.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/b8eede3f-d191-ff10-0862-6fd50b39afe1.png)

また、「**コンポーネント**」タブを開くと、コンポーネント単位でもデザインの編集ができるようです。

![スクリーンショット 2025-01-13 13.21.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/6bf5a6cc-30cc-df0b-708a-5e27d67cc3ef.png)

例えば、「**ページの背景**」だと、サインインページの背景画像をお好みのものに変えることが出来ます。下画像では、実際に背景を変更しています。

![スクリーンショット 2025-01-13 16.08.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/ed33914a-79aa-d31c-31fe-5bd33ba06b77.png)

最終的に、背景画像とボタンのスタイルを変更、あとReactのロゴを表示するようにしてみました！
編集が完了したら、画像では見切れていますが、「**変更を保存**」することで、反映されます。

![スクリーンショット 2025-01-13 13.34.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/7a3e31e0-0ad3-d7bb-4e66-d337e0bfbda9.png)

実際にアプリケーションにアクセスしてみると、変更が反映されていることが確認できました！

![スクリーンショット 2025-01-13 13.37.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/47411468-e431-26eb-ef8a-47903c618af8.png)