手を動かして0から理解するAmplify Gen2

今週は、AWS Startup Loft Osaka Pop-Upにお邪魔してきました。

https://aws.amazon.com/jp/blogs/startup/aws-startup-loft-osaka-pop-up-2024/

コワーキングスペースを提供してくれたり、日替わりでさまざまなイベントが開催されていました。（火曜日から木曜日まで、3日間もお邪魔しました。暖かく迎えていただいて大変感謝しております）

木曜日にはAmplifyに関するイベントがあり、GAされたばかりのGen2に関して学ぶことができました。なんと **AmplifyのプロダクトマネージャのMattさんからのDeep dive** もあり、 **裏側までちら見せ** してもらうという贅沢な場でした。（現地はAM 6:00ぐらいと言ってた気がします。Mattさんありがとうございました！！）

:::note
Amplify Gen2を始めるなら今です！！
:::

## Amplifyとは

Amplifyは、「AWSを意識しないでAWSを使える」という一見不思議なサービスです。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/73945b09-1b65-e398-46c7-82f6b750051d.png)

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

ただ、その威力は偉大です。Amplifyを使うことで、 **フロントエンド（＝ユーザーへ届ける価値）に開発リソースを集中させて、バックエンドは、AWSさんがよしなにやってくれる** というありがたいサービスです。

:::note
イベントのLTでもAmplifyを活用して、スピード感のある開発をされている事例の発表がありました。
:::

## Amplify Gen2とは

イベントの発表で、Amplify Gen1に対して以下のようなフィードバックがあったと紹介されていました。

* 魔法の理解
    * Amplifyが抽象化してインチキしているような感覚が気に入っているが、抽象化されすぎていてAmplifyが未対応の機能を実装しようとすると難しくなる
* ローカル開発の高速化
    * ローカルでの変更を素早く確認したい。また、複数開発者で干渉することなく開発を行いたい
* より安全な本番ロールアウト
    * 本番ロールアウトを安全にしたい
    * 柔軟なデプロイオプションがほしい

これらを解決するためにGen2では以下のように改善されました。（これは私の理解です。もっと他に色々あると思います）

* AWSリソースの構築はCDKで行う
* SandBox機能の追加
* Gitとの統合

## とりあえず始めたい方向け

Amplify Gen2のドキュメントにクイックスタートが用意されています。とりあえず触ってみようという方はこちらから入門するのが良いと思います。

https://docs.amplify.aws/react/start/quickstart/

私も実際に試したところすんなり動作しました。ただ、中身の理解ができなかったので、Amplify未導入環境から導入して理解を深めました。

:::note
前置きが長くなりましたが、私が手を動かして理解を進めた際の内容を共有させていただきます。
:::

## 開発環境

VSCodeのdevcontainerで以下の開発環境を整えました。

* node: v22.1.0
* npm: 10.7.0
* AWS CLI: 2.15.52

AWSの認証情報は`default`プロファイルに設定済みとして説明をすすめます。

:::note
Amplifyは大阪リージョンで使えるので、ぜひ大阪リージョン（ap-northeast-3）で使ってみてください。
:::

## Reactプロジェクトを作成

[Vite](https://ja.vitejs.dev/)（ヴィートと発音するようです）を使って、ReactとTypeScriptのプロジェクトを作成します。

```shell
npm create vite@latest gen2-app -- --template react-ts
```

`gen2-app`ディレクトリが作成され、以下のファイルが生成されます。

```console:tree
.
├── index.html
├── package.json
├── public
│   └── vite.svg
├── README.md
├── src
│   ├── App.css
│   ├── App.tsx
│   ├── assets
│   │   └── react.svg
│   ├── index.css
│   ├── main.tsx
│   └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

4 directories, 13 files
```

:::note

package.jsonはこんな感じです。

<details><summary>package.json</summary>

```json:package.json
{
  "name": "gen2-app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.66",
    "@types/react-dom": "^18.2.22",
    "@typescript-eslint/eslint-plugin": "^7.2.0",
    "@typescript-eslint/parser": "^7.2.0",
    "@vitejs/plugin-react": "^4.2.1",
    "eslint": "^8.57.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.6",
    "typescript": "^5.2.2",
    "vite": "^5.2.0"
  }
}
```

</details>

:::

まずはこの状態で画面を確認します。

```shell
cd gen2-app
npm install
npm run dev
```

`http://localhost:5173/`にアクセスするとトップページが確認できます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/2b366ae4-9cf6-5f58-61b3-c76e17b1f8a6.png)

## プロジェクトにAmplifyを追加

Amplifyを追加します。必要なライブラリをインストールします。

```shell
npm add --save-dev @aws-amplify/backend@latest @aws-amplify/backend-cli@latest
```

`amplify/backend.ts`を作成します。

```typescript:amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';

defineBackend({});
```

最低限これで完了です。（この先で、いくつか編集していきます）


## サンドボックスの起動

Gen2の目玉機能である **サンドボックス** を起動してみましょう。

サンドボックス機能の詳細はこちらに記載があります。

https://aws.amazon.com/jp/blogs/news/team-workflows-amplify/

`npm run dev`を実行中のターミナルと別のターミナルを開き、以下のコマンドを実行します。

```shell
npx ampx sandbox
```

:::note
サンドボックスをはじめて使用する際には、以下のエラーが表示されます。

> The given region has not been bootstrapped. Sign in to console as a Root user or Admin to complete the bootstrap process, then restart the sandbox.

この場合は、URLへアクセスを促すダイアログが表示されると思うので、指示に従ってブートストラップを行ってください。

https://ap-northeast-3.console.aws.amazon.com/amplify/create/bootstrap?region=ap-northeast-3

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/a72f4180-2cd4-c696-354b-47042d870dd2.png)

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/d6079c9f-95b2-4766-ed57-9f832554843c.png)

かっこいい！！！（裏側ではおそらく`cdk bootstrap`を実行しているものと思われます）

ブートストラップ完了後、再度`npx ampx sandbox`を実行してください。

:::

途中、以下のような黄色や赤、緑とカラフルな文字が表示されますが、正常ですのでお待ち下さい。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/2639e489-09a6-58be-1cd0-18e6181959c4.png)

以下のメッセージが表示されたらサンドボックスは作成完了です。

```
[Sandbox] Watching for file changes...
File written: amplify_outputs.json
```

Gen2ではこのターミナルを起動しっぱなしで作業をします。ローカルのファイルを更新するとそれを検知し、AWSのリソースに変更が必要な場合は随時反映されます。

自動生成される`amplify_outputs.json`は以下のような内容です。まだAWSのリソースをひとつも作成していないため、バージョン番号のみです。

```json
{
  "version": "1"
}
```

:::note
AWSが提供しているテンプレートの`.gitignore`には以下の記述があるので、以下のファイルはバージョン管理不要です

```text:.gitignore
# amplify
.amplify
amplify_outputs*
amplifyconfiguration*
```

:::


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

### バックエンド側

それではAWSのリソースを追加しましょう。
まずは、Cognitoによる認証を追加します。

`amplify/auth/resource.ts`を作成します。

```typescript:amplify/auth/resource.ts
import { defineAuth } from '@aws-amplify/backend';

export const auth = defineAuth({
    loginWith: {
        email: true
    }
});
```

次に`amplify/backend.ts`に`auth`を追記します。

```diff_typescript:amplify/backend.ts
  import { defineBackend } from '@aws-amplify/backend';
+ import { auth } from './auth/resource';

- defineBackend({});
+ defineBackend({
+     auth,
+ });
```

`amplify/backend.ts`を保存すると、バックエンド（＝AWS）にCognitoが必要と判断され、自動でデプロイが行われます。

言葉では伝わりづらいかもと思い、動画にしました。

<iframe width="560" height="315" src="https://www.youtube.com/embed/UTDF4Tdq8Sw?si=KOS-6RfDy-xW7Pzp" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

CloudFormationを確認すると、Cognitoが作成されていることがわかります。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/8baa2544-66c1-2ce5-221e-d2b20002065e.png)

このように、 **「AWSのリソースを変えたからデプロイして確認」** ではなく、 **「コードを変更したら即座に反映」** という動きをするのがAmplify Gen2の特徴です。

:::note
フロントエンド側のソースを修正して保存したら即座に反映されますよね。それと同等のことがAWSのリソースでもできる感覚です！！
:::


<details><summary>この時点のamplify_outputs.json</summary>

Cognitoの設定が追加されています。

```json:amplify_outputs.json
{
  "auth": {
    "user_pool_id": "ap-northeast-3_ULpRA2YO9",
    "aws_region": "ap-northeast-3",
    "user_pool_client_id": "19ep8arft18kequmngk0495cgf",
    "identity_pool_id": "ap-northeast-3:b01cbcaa-5bf3-47f2-8bc3-2b534c3d39ca",
    "standard_required_attributes": [
      "email"
    ],
    "username_attributes": [
      "email"
    ],
    "user_verification_types": [
      "email"
    ],
    "password_policy": {
      "min_length": 8,
      "require_numbers": true,
      "require_lowercase": true,
      "require_uppercase": true,
      "require_symbols": true
    },
    "unauthenticated_identities_enabled": true
  },
  "version": "1"
}

```

</details>


### フロントエンド側

認証のバックエンドは完成しましたのでフロントエンド側を修正します。
フロントエンド側は、Gen2になってもこれまでと変わりません。

[Amplify UI Library](https://ui.docs.amplify.aws/)を追加します。

```shell
npm add @aws-amplify/ui-react
```

フロントエンドのソースは2ヵ所修正します。

* `src/main.tsx`: Amplifyの設定を読み込む記述を追加
* `src/App.tsx`: `Authenticator`コンポーネントを追加

```diff_typescript:src/main.tsx
  import React from 'react'
  import ReactDOM from 'react-dom/client'
  import App from './App.tsx'
  import './index.css'
  
+ import '@aws-amplify/ui-react/styles.css'
  
+ import { Amplify } from 'aws-amplify'
+ import outputs from '../amplify_outputs.json'
  
+ Amplify.configure(outputs)
  
  ReactDOM.createRoot(document.getElementById('root')!).render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
  )
```

```diff_typescript:src/App.tsx
  import { useState } from 'react'
  import reactLogo from './assets/react.svg'
  import viteLogo from '/vite.svg'
  import './App.css'
  
+ import { Authenticator } from '@aws-amplify/ui-react'
  
  function App() {
    const [count, setCount] = useState(0)
  
    return (
+     <Authenticator>
+       {({ signOut, user }) => (
      <>
      <div>
        <a href="https://vitejs.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 className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
+       )}
+     </Authenticator>
    )
  }
  
  export default App
```

はい、完成です。

バックエンド側のリソース（Cognito）はすでに存在するので、UIで`Create Account`タブからサインアップできます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/6d8e441f-66d2-fb62-154a-f222b27f1a04.png)


## 関数（Lambda）を追加

バックエンドの作り方がわかってきたと思います。
次はサインインした後に呼び出す`hello-world`という名前のLambda関数を作ります。

### バックエンド側

`amplify/function/hello-world/resource.ts`を作成します。authと違い、複数リソースになるため、functionの下に個々のディレクトリがある形式です。

```typescript:amplify/function/hello-world/resource.ts
import { defineFunction } from '@aws-amplify/backend';

export const helloWorld = defineFunction({
    name: 'hello-world',
    entry: './handler.ts',
})
```

:::note
TypeScriptなので、他にどんなパラメータが設定できるかも簡単に確認できていいですね。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/b613be8f-40f9-59f6-68c7-5d4bddcead54.png)
:::

Lambda関数のコードを作成します。LambdaでTypeScriptを使用する際のドキュメント（[こちら](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/typescript-handler.html)）を参考に、`@types/aws-lambda`ライブラリを導入します。

```shell
npm add --save-dev @types/aws-lambda
```

`amplify/function/hello-world/handler.ts`を作成します。

```typescript:amplify/function/hello-world/handler.ts
import { Handler } from 'aws-lambda';

export const handler: Handler = async (event, context) => {
    return { message: 'Hello, World!!' }
}
```

:::note
フロントエンドのコードもCDKもLambdaも全部TypeScriptで記述できるのはいいですね！
:::

最後に、`amplify/backend.ts`に追加します。

```diff_typescript:amplify/backend.ts
  import { defineBackend } from '@aws-amplify/backend';
  import { auth } from './auth/resource';
+ import { helloWorld } from './function/hello-world/resource'
  
  defineBackend({
      auth,
+     helloWorld,
  });
```

`amplify/backend.ts`を保存したタイミングでAWSにLambda作成が開始されます。

もう少し続きます。

#### Lambdaを呼び出すための権限を付与

Cognitoで認証したユーザーに、このLambdaの実行権限を付与します。

`amplify/backend.ts`を修正し、まずCognitoで認証した際に付与されるIAMロールを取得します。
そして、「`helloWorld`関数を`Invokeする権限`を`authenticatedUserIamRole`に付与する」という記述を行います。

```diff_typescript:amplify/backend.ts（抜粋）
- defineBackend({
+ const backend = defineBackend({
      auth,
      helloWorld,
      invokeBedrock,
  });

+ const authenticatedUserIamRole = backend.auth.resources.authenticatedUserIamRole;
+ backend.helloWorld.resources.lambda.grantInvoke(authenticatedUserIamRole);
```

#### フロントエンドへの連携

最後に **「amplify_outputs.jsonにLambda関数名（物理名）を出力する」** 処理を追加します。
こうすることで、フロントエンド側から、Lambda関数の物理名を参照することができます。

```diff_typescript:amplify/backend.ts（抜粋）
+ backend.addOutput({
+     custom: {
+         helloWorldFunctionName: backend.helloWorld.resources.lambda.functionName,
+     },
+ });
```

amplify_outputs.jsonの出力に以下の項目が追加されます。


```json:amplify_outputs.json
{
  ...
  "custom": {
    "helloWorldFunctionName": "amplify-gen2app-node-sand-helloworldlambda89B27E55-2un3X5SixeeW"
  }
}
```

### フロントエンド側

フロントエンド側にLambdaを呼び出す処理を追加します。

まずは必要なimportを追加します。

```typescript
import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda'
import { fetchAuthSession } from 'aws-amplify/auth'

import outputs from "../amplify_outputs.json"
```

`fetchAuthSession`関数でcredentials （アクセスキー、シークレットアクセス、セッショントークン）を取得します。

```typescript
const { credentials } = await fetchAuthSession()
```

`amplify_outputs.json`からリージョンとLambda関数名を取得します。

```typescript
const awsRegion = outputs.auth.aws_region
const functionName = outputs.custom.helloWorldFunctionName
```

:::note
Lambda関数名は先程`backend.addOutput`で出力した値です
:::

これでLambdaを呼び出す準備が整いました。SDKを使ってLambdaを呼び出します。

```typescript
const labmda = new LambdaClient({ credentials: credentials, region: awsRegion })
const command = new InvokeCommand({
  FunctionName: functionName,
});
const apiResponse = await labmda.send(command);

if (apiResponse.Payload) {
  const payload = JSON.parse(new TextDecoder().decode(apiResponse.Payload))
  setText(payload.message)
}
```

<details><summary>この時点のApp.tsx</summary>

```diff_jsx:App.tsx
  import { useState } from 'react'
  import reactLogo from './assets/react.svg'
  import viteLogo from '/vite.svg'
  import './App.css'
  
  import { Authenticator } from '@aws-amplify/ui-react'
+ import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda'
+ import { fetchAuthSession } from 'aws-amplify/auth'
  
+ import outputs from "../amplify_outputs.json"
  
  function App() {
    const [count, setCount] = useState(0)
+   const [text, setText] = useState("")
  
+   async function invokeHelloWorld() {
+ 
+     const { credentials } = await fetchAuthSession()
+     const awsRegion = outputs.auth.aws_region
+     const functionName = outputs.custom.helloWorldFunctionName
+ 
+     const labmda = new LambdaClient({ credentials: credentials, region: awsRegion })
+     const command = new InvokeCommand({
+       FunctionName: functionName,
+     });
+     const apiResponse = await labmda.send(command);
+ 
+     if (apiResponse.Payload) {
+       const payload = JSON.parse(new TextDecoder().decode(apiResponse.Payload))
+       setText(payload.message)
+     }
+   }
  
    return (
      <Authenticator>
        {({ signOut, user }) => (
      <>
        <div>
          <a href="https://vitejs.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 className="card">
          <button onClick={() => setCount((count) => count + 1)}>
            count is {count}
          </button>
          <p>
            Edit <code>src/App.tsx</code> and save to test HMR
          </p>
        </div>
        <p className="read-the-docs">
          Click on the Vite and React logos to learn more
        </p>
+       <p>
+         <button onClick={invokeHelloWorld}>invokeHelloWorld</button>
+         <div>{text}</div>
+       </p>
      </>
        )}
      </Authenticator>
    )
  }
  
  export default App
  
```

</details>

ボタンをクリックすると、このようにLambdaのレスポンスを画面表示できました。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/6e016229-0ff3-280d-8b74-c4ce561bb4b9.png)


## 関数（Lambda）を追加（発展編）

先程のLambda関数は固定文字を返すだけでしたので、Bedrockを呼び出してその結果を返すLambdaを作成します。

### バックエンド側

Lambda関数のコードを作成する前に、Bedrock RuntimeのSDKをインストールします。

```shell
npm add --save-dev @aws-sdk/client-bedrock-runtime
```

`amplify/function/invoke-bedrock/resource.ts`を作成します。

```typescript:amplify/function/invoke-bedrock/resource.ts
import { defineFunction } from '@aws-amplify/backend';

export const invokeBedrock = defineFunction({
    name: 'invoke-bedrock',
    entry: './handler.ts',
})
```

### ストリーミングレスポンス

Lambdaは、ストリーミングでレスポンスを返却することができるので、Bedrockから受けたストリーミングのレスポンスをLambdaの結果として返却する方法で実装します。
以下のドキュメントを参考にしました。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-response-streaming.html

https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-runtime_example_bedrock-runtime_Claude3_Text_section.html


```typescript:amplify/function/invoke-bedrock/handler.ts
import { Context, Handler } from 'aws-lambda';
import { Writable } from 'stream';

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

type eventType = {
    prompt: string
}

const modelId = "anthropic.claude-3-haiku-20240307-v1:0"

export const handler: Handler = awslambda.streamifyResponse(
    async (event: eventType, responseStream: Writable, _context: Context) => {

        const client = new BedrockRuntimeClient({ region: "us-east-1" });

        const payload = {
            anthropic_version: "bedrock-2023-05-31",
            max_tokens: 1000,
            messages: [
                {
                    role: "user",
                    content: [{ type: "text", text: event.prompt }],
                },
            ],
        };

        const command = new InvokeModelWithResponseStreamCommand({
            contentType: "application/json",
            body: JSON.stringify(payload),
            modelId,
        });

        const apiResponse = await client.send(command);

        if (apiResponse.body) {
            for await (const item of apiResponse.body) {
                if (item.chunk) {
                    const chunk = JSON.parse(new TextDecoder().decode(item.chunk.bytes));
                    const chunk_type = chunk.type;

                    if (chunk_type === "content_block_delta") {
                        const text = chunk.delta.text;
                        responseStream.write(text);
                    }
                } else if (item.internalServerException) {
                    throw item.internalServerException
                } else if (item.modelStreamErrorException) {
                    throw item.modelStreamErrorException
                } else if (item.throttlingException) {
                    throw item.throttlingException
                } else if (item.validationException) {
                    throw item.validationException
                }
            }
        }

        responseStream.end()
    }
)
```

:::note
Bedrockは大阪リージョンにまだ提供されていのないので、バージニア北部のBedrockを呼び出します。
:::

`amplify/backend.ts`に`invoke-bedrock`Lambda関数を追加します。

```diff_typescript:amplify/backend.ts（抜粋）
+ import { invokeBedrock } from './function/invoke-bedrock/resource'
  
  defineBackend({
      auth,
      helloWorld,
+     invokeBedrock,
  });
```

Cognitoで認証したユーザーがこのLambda関数を呼べるように追加します。

```diff_typescript:amplify/backend.ts（抜粋）
+ backend.invokeBedrock.resources.lambda.grantInvoke(authenticatedUserIamRole);
```

フロントエンド側が、Lambda関数名を取得できるようにOutputに追加します。

```diff_typescript:amplify/backend.ts（抜粋）
  backend.addOutput({
      custom: {
          helloWorldFunctionName: backend.helloWorld.resources.lambda.functionName,
+         invokeBedrockFunctionName: backend.invokeBedrock.resources.lambda.functionName,
      },
  });
```

最後に、Lambda関数がBedrockを呼び出すための権限を設定します。

```diff_typescript:amplify/backend.ts（抜粋）
+ import * as iam from 'aws-cdk-lib/aws-iam';

+ const bedrockStatement = new iam.PolicyStatement({
+     actions: ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
+     resources: ["arn:aws:bedrock:us-east-1::foundation-model/*"]
+ })

+ backend.invokeBedrock.resources.lambda.addToRolePolicy(bedrockStatement)
```

:::note
このあたりの書き方は、CDKですね。
:::

<details><summary>この時点のamplify/backend.ts</summary>

```diff_typescript:backend.ts
  import { defineBackend } from '@aws-amplify/backend';
+ import * as iam from 'aws-cdk-lib/aws-iam';
  import { auth } from './auth/resource';
  import { helloWorld } from './function/hello-world/resource'
+ import { invokeBedrock } from './function/invoke-bedrock/resource'
  
  const backend = defineBackend({
      auth,
      helloWorld,
+     invokeBedrock,
  });
  
  const authenticatedUserIamRole = backend.auth.resources.authenticatedUserIamRole;
  backend.helloWorld.resources.lambda.grantInvoke(authenticatedUserIamRole);
+ backend.invokeBedrock.resources.lambda.grantInvoke(authenticatedUserIamRole);
  
+ const bedrockStatement = new iam.PolicyStatement({
+     actions: ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
+     resources: ["arn:aws:bedrock:us-east-1::foundation-model/*"]
+ })
  
+ backend.invokeBedrock.resources.lambda.addToRolePolicy(bedrockStatement)
  
  backend.addOutput({
      custom: {
          helloWorldFunctionName: backend.helloWorld.resources.lambda.functionName,
+         invokeBedrockFunctionName: backend.invokeBedrock.resources.lambda.functionName,
      },
  });
```

</details>

### フロントエンド側

フロントエンド側にLambdaを呼び出す処理を追加します。
先程のHello Worldとは異なり、パラメータを渡してレスポンスを受け取ります。
レスポンスはストリーミング形式です。

:::note
もう殆どAmplify関係なく、通常のReact開発という感じです。
:::

```jsx
  const [prompt, setPrompt] = useState("")
  const [aiMessage, setAiMessage] = useState("")
  
  async function invokeBedrock() {

    const { credentials } = await fetchAuthSession()
    const awsRegion = outputs.auth.aws_region
    const functionName = outputs.custom.invokeBedrockFunctionName

    const labmda = new LambdaClient({ credentials: credentials, region: awsRegion })
    const command = new InvokeWithResponseStreamCommand({
      FunctionName: functionName,
      Payload: new TextEncoder().encode(JSON.stringify({ prompt: prompt }))
    })
    const apiResponse = await labmda.send(command);

    let completeMessage = ''
    if (apiResponse.EventStream) {
      for await (const item of apiResponse.EventStream) {
        if (item.PayloadChunk) {
          const payload = new TextDecoder().decode(item.PayloadChunk.Payload)
          completeMessage = completeMessage + payload
          setAiMessage(completeMessage)
        }
      }
    }
  }
```

<iframe width="560" height="315" src="https://www.youtube.com/embed/Z47RnNnO7qE?si=UgFCotFfqyqCClEF" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

<details><summary>src/App.tsxの最終形</summary>

少し整形したり、undefinedのチェックを追加しています。

```jsx:src/App.tsx
import { useState } from 'react'
import './App.css'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'

import { Authenticator } from '@aws-amplify/ui-react'
import { InvokeCommand, InvokeWithResponseStreamCommand, LambdaClient } from '@aws-sdk/client-lambda'
import { fetchAuthSession } from 'aws-amplify/auth'

import outputs from "../amplify_outputs.json"


function App() {
  const [text, setText] = useState("")
  const [prompt, setPrompt] = useState("")
  const [aiMessage, setAiMessage] = useState("")

  async function invokeHelloWorld() {

    const { credentials } = await fetchAuthSession()
    const awsRegion = outputs.auth.aws_region
    const functionName = outputs.custom.helloWorldFunctionName

    const labmda = new LambdaClient({ credentials: credentials, region: awsRegion })
    const command = new InvokeCommand({
      FunctionName: functionName,
    });
    const apiResponse = await labmda.send(command);

    if (apiResponse.Payload) {
      const payload = JSON.parse(new TextDecoder().decode(apiResponse.Payload))
      setText(payload.message)
    }
  }

  async function invokeBedrock() {

    const { credentials } = await fetchAuthSession()
    const awsRegion = outputs.auth.aws_region
    const functionName = outputs.custom.invokeBedrockFunctionName

    const labmda = new LambdaClient({ credentials: credentials, region: awsRegion })
    const command = new InvokeWithResponseStreamCommand({
      FunctionName: functionName,
      Payload: new TextEncoder().encode(JSON.stringify({ prompt: prompt }))
    })
    const apiResponse = await labmda.send(command);

    let completeMessage = ''
    if (apiResponse.EventStream) {
      for await (const item of apiResponse.EventStream) {
        if (item.PayloadChunk) {
          const payload = new TextDecoder().decode(item.PayloadChunk.Payload)
          completeMessage = completeMessage + payload
          setAiMessage(completeMessage)
        }
      }
    }
  }

  return (
    <Authenticator>
      {({ signOut, user }) => (
        <>
          <div>
            <a href="https://docs.amplify.aws" target="_blank">
              <img src="https://docs.amplify.aws/assets/icon/favicon-purple-96x96.png" className="logo amplify" alt="Amplify logo" />
            </a>
            <a href="https://vitejs.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>Amplify + Vite + React</h1>
          <p>
            Hello, {user?.signInDetails?.loginId}
            <br />
            <button onClick={signOut}>Sign Out</button>
          </p>
          <p>
            <button onClick={invokeHelloWorld}>invokeHelloWorld</button>
            <div>{text}</div>
          </p>
          <p>
            <textarea
              onChange={(e) => setPrompt(e.target.value)}
              value={prompt}
              style={{ width: '50vw', textAlign: 'left' }}
            ></textarea>
            <br />
            <button onClick={invokeBedrock}>invokeBedrock</button>
            <div style={{ width: '50vw', textAlign: 'left' }}>{aiMessage}</div>
          </p>
        </>
      )}
    </Authenticator>
  )
}

export default App
```

</details>

これでバックエンド、フロントエンド含めて一通り完成しました。

## Gen2のその他の機能

認証と関数以外の機能もあります。

* Data（AppSync）

https://docs.amplify.aws/react/build-a-backend/data/

* Storage（S3）

https://docs.amplify.aws/react/build-a-backend/storage/

* Server Side Rendering

https://docs.amplify.aws/react/build-a-backend/server-side-rendering/

## 本番環境にデプロイ

ローカルでの開発がいい感じに終わったので、本番環境としてデプロイしましょう。

まず、これまでのソースをGitHubにプッシュします。プライベートリポジトリで構いません。

AWSのマネジメントコンソールにて、Amplifyの画面を表示します。`新しいアプリ`ボタンをクリックします。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/31936801-a9a7-f55e-6418-9a7fe1fdf193.png)

`GitHub`を選択し、`次へ`ボタンをクリックします。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/5763b1b0-e53e-6df9-c14d-58cb58d9c40b.png)

アクセスを許可するウィンドウが表示されるので、対象のリポジトリを選び、`Install & Authorize`ボタンをクリックします。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/d36a0ede-5f43-68d2-6bc0-c7c9004a4476.png)

Amplifyの画面に戻るので、リポジトリ、ブランチを選択して`次へ`ボタンをクリックします。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/1af84c8b-6c01-10b1-9e20-e6739ae1f8de.png)

プロジェクトの自動検出が行われます。このまま`次へ`ボタンをクリックします。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/c82aa4f1-527d-4c78-926f-eb9bb860c3fb.png)

確認画面が表示されます。`保存してデプロイ`をクリックします。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/d60c6ffc-a7b3-8200-77e7-b1585bb70109.png)

しばらくするとデプロイが完了します。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/913823ef-bb4d-5117-f56b-ba0f831ff49d.png)

:::note
ソースコードがビルドエラーになり、一度デプロイに失敗しましたが、ソースを修正してプッシュすると、自動でビルドが実行されました。
:::

Amplifyが自動でドメインを発行してくれます。発行されたURLにアクセスすると、アプリが確認できます。

また、Amplifyの画面で認証や関数などの管理ができます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/b92bef96-3e5c-a408-acfe-81348d5a05bd.png)

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/213cde6e-9d5c-ca84-a708-5047283676c2.png)

:::note
これは結構便利な気がします。
:::

今回は試せてませんがデプロイに関しても色々機能が提供されているようです。

* Git flow、GitHub/プルリクエストフロー
* モノレポとマルチリポジトリ

https://aws.amazon.com/jp/blogs/news/team-workflows-amplify/

---

最後まで読んでいただいてありがとうございます。Amplify Gen2に興味を持っていただけたら幸いです。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/bc9dfc90-f55d-6532-0454-f358f0218d21.png)