新年なのでLLMとのチャットアプリをイチから作ってみた②Bedrockと接続

画面のモックができたので、AWSと接続していきましょう。

:::note
これから説明する手順で進めると、ソースコード中にAWSの認証情報を含めずにBedrockにアクセスできます。

認証情報をベタ書きしてBedrockを呼び出したいところですがグッとこらえて順に進めてください。
:::


## 目次

全3回に分けて投稿しています。

* [新年なのでLLMとのチャットアプリをイチから作ってみた①](feb2875d2059b9074ce8)
* 新年なのでLLMとのチャットアプリをイチから作ってみた②  ←このページ
* [新年なのでLLMとのチャットアプリをイチから作ってみた③](0b735c6d2fa033f532c3)


## アーキテクチャの検討

フロントエンドからAWSのバックエンドのBedrockを呼ぶ場合、こういった構成を思い浮かべるのではないでしょうか？

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/fdc8f319-8c98-44a0-2bba-1b765283ff1f.png)

この構成でも良いのですが、Amplifyを使うともっとシンプルな構成にすることができます。

Amplifyの認証機能はCognitoの機能で実現されています。Cognitoには、IDプールの認証情報からIAMの一時クレデンシャルを生成する機能があります。この機能を使うことでフロントエンド（ブラウザ）が一時クレデンシャルをもっている状態になり、ブラウザから直接AWSサービスを呼ぶことが可能になります。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/b5253840-46d7-9df3-32af-819b1831bdfa.png)

ビジネスロジックをバックエンド側で実装したい場合など、API GatewayとLambdaを使用するケースももちろんあると思いますが、単純にAWSサービスを呼ぶだけであれば、Amplifyを使うと便利です。（API Gatewayにはペイロードサイズやタイムアウトの制限があるので、これらを回避するためにも有効です）

参考：[Accessing AWS services using an identity pool after sign-in](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-integrating-user-pools-with-identity-pools.html)

他の応用例としてはこのようなものも考えられます。

* S3に直接ファイルをアップロードする。署名付きURLを都度発行する必要なし。
* API Gatewayを挟まず、Lambdaの関数URLを直接呼び出す。最大15分のバックエンド処理が可能。


## Amplifyを導入

:::note
2024/01/05時点でv5/v6/Gen2と3バージョン混在状態ですが、v6ですすめます。
:::

[Amplify Documentation](https://docs.amplify.aws/)、[Set up Amplify CLI](https://docs.amplify.aws/react/start/getting-started/installation/)、[Create your application](https://docs.amplify.aws/javascript/start/project-setup/create-application/)を参考に進めていきましょう。

1. Amplify CLIをインストール

    ```shell
    npm install -g @aws-amplify/cli
    ```
1. Amplify CLIの設定  

    :::note
    devcontainerで実行している場合、xdg-utilsをインストールすることで、URLが自動で開くようになります。

    ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/bce623b3-83cf-c206-c7fb-a3854cdafa54.png)

    ```shell
    sudo apt update
    sudo apt install xdg-utils
    ```
   :::

    ウィザードを進めていきます。

    1. AWSのマネジメントコンソールが表示されるのでログインします。
    1. リージョンを選択（ap-northeast-1）
    1. ドキュメントが開きます。
    1. IAMのユーザー作成画面が開きます。ドキュメントに従い`AdministratorAccess-Amplify`ポリシーをアタッチしたユーザーを作成し、認証情報を生成します。
    1. Access KeyとSecret access keyをウィザードに入力します。
    1. プロファイル名を入力（またはdefaultのまま）します。

1. Amplify Backendの作成

    ```shell
    amplify init
    ```
    
    1. プロジェクト名を入力します。
    1. プロジェクトの設定を検知してくれるので、そのままYで進めます。
    1. 先程設定したプロファイルを指定します

1. Amplifyライブラリーのインストール

    ```shell
    npm install aws-amplify
    ```
1. `+page.svelte`のScriptタグ内に以下のコードを追加します。

    ```javascript
    import { Amplify } from "aws-amplify";
    import amplifyconfig from "../amplifyconfiguration.json";
    Amplify.configure(amplifyconfig);
    ```

これでAmplifyの導入は完了です。

## Amplifyの認証機能（Cognito）を追加

Cognitoの認証機能を有効化します。Cognitoにはログイン画面が予め用意されているのでこの機能を活用します。

参考：
[Add social provider sign-in](https://docs.amplify.aws/javascript/build-a-backend/auth/add-social-provider/)
[AmplifyでCognitoのHosted UIを利用した認証を最低限の実装で動かしてみて動作を理解する](https://dev.classmethod.jp/articles/learn-authentication-using-cognitos-hosted-ui-with-amplify/)
[Setting up and using the Amazon Cognito hosted UI and federation endpoints](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-integration.html)

:::note
参考URLはソーシャルログインを使用する感じになってますが、Cognitoのユーザー管理機能だけをでも利用可能です。
:::

1. Amplify Authを追加

    ```shell
    amplify add auth
    ```

    サインインのリダイレクトURLとサインアウトのリダイレクトURLは、どちらも`http://localhost:5173/`と入力します。（末尾は必ず`/`で終わる必要があります）

    ```
    Using service: Cognito, provided by: awscloudformation
    
    The current configured provider is Amazon Cognito. 
    
    Do you want to use the default authentication and security configuration? Default configuration with Social Provider (Federation)
    Warning: you will not be able to edit these selections. 
    How do you want users to be able to sign in? Username
    Do you want to configure advanced settings? No, I am done.
    What domain name prefix do you want to use? myskeletonapp77ad9d4f-77ad9d4f
    Enter your redirect signin URI: http://localhost:5173/
    ? Do you want to add another redirect signin URI No
    Enter your redirect signout URI: http://localhost:5173/
    ? Do you want to add another redirect signout URI No
    Select the social providers you want to configure for your user pool: 
    ✅ Successfully added auth resource myskeletonapp77ad9d4f locally

    ✅ Some next steps:
    "amplify push" will build all your local backend resources and provision it in the cloud
    "amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud
    ```

    設定した内容を、AWS環境に反映します。

    ```shell
    amplify push
    ```

1. ログインボタン、ログアウトボタンを追加

    [Hub](https://docs.amplify.aws/javascript/build-a-backend/utilities/hub/)というユーティリティが用意されており、Hosted UIからのリダイレクトを上手に処理してくれます。
    
    以下をScriptタグ内に追加

    ```javascript
    import { Hub } from "aws-amplify/utils";
    import {
      getCurrentUser,
      signInWithRedirect,
      signOut,
    } from "aws-amplify/auth";

    Hub.listen("auth", async ({ payload }) => {
      console.log(payload);
      switch (payload.event) {
        case "signInWithRedirect":
          getUser();
          break;
      }
    });

    function handleSignInClick() {
      signInWithRedirect();
    }

    function handleSignOutClick() {
      signOut();
    }

    let isAuthorizedUser = false;

    async function getUser() {
      try {
        const user = await getCurrentUser();
        if (user.userId) {
          isAuthorizedUser = true;
        }
      } catch (Exception) {
        isAuthorizedUser = false;
      }
    }

    getUser();
    ```

    HTML部分にヘッダーを追加し、ボタンを配置します。

    ```html
    <AppBar
      gridColumns="grid-cols-2"
      slotDefault="place-self-left"
      slotTrail="place-content-end"
    >
      チャットアプリ
      <svelte:fragment slot="trail">
        <button
          type="button"
          class="btn btn-sm variant-filled-primary"
          on:click={handleSignOutClick}>ログアウト</button
        >
        <button
          type="button"
          class="btn btn-sm variant-filled-primary"
          on:click={handleSignInClick}>ログイン</button
        >
      </svelte:fragment>
    </AppBar>
    ```

1. サインアップしてユーザーを作成

    devサーバーを起動して確認してみましょう。

    ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/062807e9-1b1e-df1e-4ae6-be326713fb71.png)

    ログインボタンを押すと、Cognitoが用意したログイン画面に遷移します。`Sign up`リンクからユーザーの作成ができます。

    ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/d5a5766c-daaa-d0d8-4809-71c6d4588e89.png)

    ログインが成功すると、元の画面に戻ってきます。

    ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/843e93d5-efbe-7905-5346-758a04976bfc.png)

    :::note
    このままでは誰でもサインアップできる状態なので、テストで使用するユーザーが作成できたら、セルフサインアップの機能を無効化することをおすすめします。
    Amplifyから設定変更する方法が分からなかったので、マネジメントコンソールでの無効化手順です。
    
    1. [Cognito User Poolの管理画面](https://ap-northeast-1.console.aws.amazon.com/cognito/v2/idp/user-pools?region=ap-northeast-1)を開きます。
    1. 対象のユーザープール（おそらく最終更新時刻が最近のもの）を選択します。
    1. `サインアップエクスペリエンス`タブを選択します。
    1. セルフサービスのサインアップセクションの編集ボタンから、自己登録の無効化を行います。
   :::

ログイン状態によってチャット欄を非表示にしたりするコードを追加しました。（ヘッダー追加によるレイアウト崩れなどにも対応しました。）

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/e0567008-04ea-9f02-d7ed-ab19e96ea775.gif)

## 認証されたユーザーにBedrockへのアクセス権限を付与

Amplifyで作成されるCognitoのリソースはユーザープールだけでなく、IDプールも作成されます。 CognitoのIDプールの機能で、認証済みユーザーに対してIAMロールが付与されます。（未認証ユーザーにも付与されます。）

このIAMロールにBedrockへアクセスするポリシーを追加します。

参考：[Override Amplify-generated project-level IAM resources](https://docs.amplify.aws/javascript/tools/cli/project/override-iam/)

1. Amplifyプロジェクトを上書きするファイルを生成

    以下のコマンドで`my-skeleton-app/amplify/backend/awscloudformation/override.ts`が生成されます。

    ```shell
    amplify override project
    ```

1. 認証ユーザーのロールに、マネージドポリシーを追加します。

    `override.ts`に以下のコードを記述します。

    ```javascript
    const authRole = resources.authRole;
    
    const baseManagedPolicy = Array.isArray(authRole.managedPolicyArns)
      ? authRole.managedPolicyArns
      : [authRole.managedPolicyArns];
    
    authRole.managedPolicyArns = [
      ...baseManagedPolicy,
      "arn:aws:iam::aws:policy/AmazonBedrockFullAccess"
    ]
    ```

1. AWS側に反映します。

    ```shell
    amplify push
    ```

これでCognitoでログインしたユーザーに、Bedrockへのアクセス権限が付与されます。

## Bedrockとの統合

いよいよBedrockの出番です。

1. AWS SDKを追加します。

    Bedrock RuntimeのSDKを追加します。（`client-bedrock`ではなく、`client-bedrock-runtime`です。）
    ```shell
    npm i @aws-sdk/client-bedrock-runtime
    ```

1. Bedrockアクセス部分のコードを追加します。

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

    `aws-amplify/auth`ライブラリーのfetchAuthSession関数を使用することで、credentialsが取得できます。

    ```javascript
    async function invokeBedrock(prompt: string): Promise<string> {
    	const client = new BedrockRuntimeClient({
    		region: amplifyconfig.aws_project_region,
    		credentials: (await fetchAuthSession()).credentials,
    	});
    
    	const params = {
    		modelId: "anthropic.claude-instant-v1",
    		contentType: "application/json",
    		accept: "*/*",
    		body: JSON.stringify({
    			prompt: `\n\nHuman: ${prompt}\n\nAssistant:`,
    			max_tokens_to_sample: 1000,
    			temperature: 0.5,
    			top_k: 250,
    			top_p: 1,
    			stop_sequences: ["\n\nHuman:"],
    		}),
    	};
    
    	const command = new InvokeModelCommand(params);
    	const response = await client.send(command);
    
    	return JSON.parse(response.body.transformToString("utf-8"))["completion"];
    }
    ```

    :::note
    これで認証情報をベタ書きする必要がなくなりました
   :::

1. 呼び出し元のaddMessageやaddAiMessageを合わせて修正します。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/e2bd60bb-809d-c491-15cc-bd3b28246d62.png)

Bedrockとチャットができました！！


## Amplifyでホスティング

Amplifyには、フロントエンドのコードをホスティングする機能もあります。作成したチャットアプリをAmplifyにホスティングしてみましょう。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/824915fc-4b62-3539-8943-7916026182c7.png)


SvelteKitのプロジェクトをAmplifyでホストするために、`adapter-static`を使用する必要があるようです。

1. `adapter-static`をインストール

    ```shell
    npm i -D @sveltejs/adapter-static
    ```

1. `svelte.config.js`に設定を追加

    1行目のimport文を、`adapter-static`を参照するように変更します。

    ```diff
    - import adapter from '@sveltejs/adapter-auto';
    + import adapter from '@sveltejs/adapter-static';
    ```

    `adapter()`を変更します。pagesとassetsの値は、`amplify init`の際にしれっと設定されていたdistを指定しましょう。

    ```diff
    - adapter: adapter()
    + adapter: adapter({
    +   pages: 'dist',
    +   assets: 'dist',
    +   fallback: 'index.html',
    + })
    ```

1. ソースコードをビルド

    buildコマンドでビルドします。

    ```shell
    npm run build
    ```

    `dist`ディレクトリにビルド結果が出力されます。

1. Amplify Hostingの設定

    Amplify Hostingの設定を行います。ウィザードに従うだけです。

    ```shell
    amplify add hosting
    ```
    ```
    ✔ Select the plugin module to execute · Hosting with Amplify Console (Managed hosting with custom domains, Continuous deployment)
    ? Choose a type Manual deployment

    You can now publish your app using the following command:

    Command: amplify publish
    ```

1. 公開する

   ソースをアップロードして公開します。 

    ```shell
    amplify publish
    ```

    ```
    ✓ built in 9.75s
    ✔ Zipping artifacts completed.
    ✔ Deployment complete!
    https://dev.d2kjvugewtnasz.amplifyapp.com
    ```

    これで公開されますが、ログインするために、まだもうちょっとやることがあります。

1. サインイン/サインアウト時のドメイン登録

    サインイン、サインアウトは事前に登録したドメインからでないと実施できません。Amplify Hostingで発行したドメインを追加します。

    ```shell
    amplify update auth
    ```

    ウィザードに従いながら、サインインとサインアウトにそれぞれドメインを登録します。（ドメインの末尾に`/`を付ける必要があるので注意）

    ```
    Please note that certain attributes may not be overwritten if you choose to use defaults settings.
    Using service: Cognito, provided by: awscloudformation
    What do you want to do? Add/Edit signin and signout redirect URIs
    Which redirect signin URIs do you want to edit? 
    Do you want to add redirect signin URIs? Yes
    Enter your new redirect signin URI: https://dev.d2kjvugewtnasz.amplifyapp.com/
    ? Do you want to add another redirect signin URI No
    Which redirect signout URIs do you want to edit? 
    Do you want to add redirect signout URIs? Yes
    Enter your new redirect signout URI: https://dev.d2kjvugewtnasz.amplifyapp.com/
    ? Do you want to add another redirect signout URI No
    ✅ Successfully updated auth resource myskeletonapp77ad9d4f locally

    ✅ Some next steps:
    "amplify push" will build all your local backend resources and provision it in the cloud
    "amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud

    ✅ Successfully updated resource update locally

    ✅ Some next steps:
    "amplify push" will build all your local backend resources and provision it in the cloud
    "amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud
    ```

1. pushとpublish

    Cognitoのリソースを更新するために`amplify push`を、ソースコードを更新するために`amplify publish`を実行します。

    ```shell
    amplify push
    amplify publish
    ```

これで完成です。スマートフォンからも見れます！

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/68dadebc-0fd2-4974-c750-e51c76cdb329.png)

:::note
単一ページの場合は問題ありませんが、複数ページ構成にした場合、トップページ以外にアクセスすると、エラーになります。

リダイレクト設定を行うことで、複数ページでも動作するようになります。

参考：[Using redirects](https://docs.aws.amazon.com/amplify/latest/userguide/redirects.html#redirects-for-single-page-web-apps-spa)

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/2b8a428e-0e7c-9dc3-5621-8bcb56535cfc.png)
:::

---

最低限のチャットが作成できました。次回はもう少し見た目や使い勝手が良くなるように調整していきます。

---

ソースコードの全体はこちらで確認可能です。

https://github.com/moritalous/skeleton-chat/

* 該当のソースコード

https://github.com/moritalous/skeleton-chat/blob/main/src/routes/v2/%2Bpage.svelte