AWS Amplifyを試してみた

# 学んだこと

- めちゃくちゃお手軽にアプリをデプロイできる
- やはりQuickStartからまずやってみるのが一番いい
  - ハンズオンやってみた系で行こうとおもったらハンズオンの手順よりも新しくていい手順が途中で見つかって切り替えた
  - お忙しい方は ここで気づいた からみてください
- 次回やってみたいこと
    - データ周りいろいろ試してみたい https://docs.amplify.aws/react/build-a-backend/data/
    - AI Kitやりたい https://docs.amplify.aws/react/ai/

# AWS Amplifyとは
AWS Amplify には、ウェブアプリケーションやモバイルアプリの構築に必要なものがすべて揃っています。開始もスケールも簡単です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/7c3e85a7-0099-d27a-4609-9c5b0e63b4bb.png)


## フルスタック React アプリケーションを構築する
AWS Amplify を使用してシンプルなウェブアプリケーションを作成する

https://aws.amazon.com/jp/getting-started/hands-on/build-react-app-amplify-graphql/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/e0e9b4da-39cd-0bea-4247-dd363e116c7f.png)

今回はこの手順に沿ってやってみたいと思います。

### 1.Reactアプリケーションを作成

React アプリケーションを作成し、AWS Amplify のウェブホスティングサービスを利用してそれをクラウドにデプロイします

```bash
npx create-react-app amplifyapp
cd amplifyapp
npm start
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/fcc711df-0e0c-cb42-de6c-31c3695d2f05.png)


### 2.Githubリポジトリを初期化

https://github.com/ でリポジトリを作成する

```bask
git init
git add .
git commit -m "initial commit"
git remote add origin git@github.com:username/reponame.git<作成したリポジトリURL>
git branch -M main
git push -u origin main
```

### 3.AWS Amplifyにデプロイする

https://console.aws.amazon.com/ から[AWS Amplify] を選択し、サービスコンソールを開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/253ac40b-148b-3d24-6698-a8ab9174fc31.png)

アプリケーションをデプロイをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/f34858de-9eff-82e1-094f-43c8f0a92034.png)

Githubを選択して次へを押すと、Githubのアカウントの連携ページが開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/f4d710c8-256d-8e18-927b-b71141d80f08.png)

先ほど作成したリポジトリで許可をします。
<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/f68390de-0842-a592-e48f-d95d4f14f512.png">

保存してデプロイをクリックするとアプリのデプロイが実行されます。


<img width="49%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/77765d4e-0a99-d0c5-8936-d93772592501.png">
<img width="49%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/5b019fbb-0ba2-9353-7cdd-9c3769952542.png">

--------------------

### 4.コードの変更を自動的にデプロイする

コードを編集し、pushします。

```javascript:src/App.js 
import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <h1>Hello from V2</h1>
      </header>
    </div>
  );
}

export default App;
```

```
git add .
git commit -m "changes for v2"
git push origin main
```

変更をpushしたらデプロイが走りはじめました。

<img width="49%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/5e4eb846-a780-2b23-e733-481e1eab26ed.png">
<img width="49%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/a1026278-69be-9105-7bc2-42266330bb71.png">

:::note info
変更を簡単に反映することができました。
:::

--------------------

## ローカルAmplifyアプリを初期化する

Amplify CLI をインストール、設定します。

### 1.Amplify CLIをインストールする

```bash
npm install -g @aws-amplify/cli
amplify configure
```

```
Follow these steps to set up access to your AWS account:

Sign in to your AWS administrator account:
https://console.aws.amazon.com/
Press Enter to continue

Specify the AWS Region
? region:  us-east-1
Follow the instructions at
https://docs.amplify.aws/cli/start/install/#configure-the-amplify-cli

to complete the user creation in the AWS console
https://console.aws.amazon.com/iamv2/home#/users/create
Press Enter to continue

Enter the access key of the newly created user:
```

IAMユーザーを作成する。ポリシーはAdministratorAccess-Amplifyを指定する。

https://docs.amplify.aws/gen1/javascript/tools/cli/start/set-up-cli/#configure-the-amplify-cli

ここまでは問題なく進んだんですが、次のAmplifyアプリを初期化するのこのタブが見当たらず... 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/f09e4218-7cf2-122b-c5d2-82979bc64db6.png)

今はどういう感じでバックエンド構築するのか調べたところ、次のページが見つかりました。

https://docs.amplify.aws/react/start/quickstart/?plugin_version=chrome_3.5.8#make-backend-updates


Set up local AWS credentialsまではやってると思うので、Deploy cloud sandboxから実施します。

```bash
npx ampx sandbox
npm error could not determine executable to run

npm error A complete log of this run can be found in:
```

じ...実行できない

ちょっとUIから見てみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/5158dd20-7bfd-334e-9d5a-00041130c834.png)

 画面真ん中、mainのカード部分をクリックすると、次のような画面が開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/b6e4e558-07d2-1046-72ae-48aa8a55f145.png)

データのところを開いてみました。
これっぽいのでちょっとやってみます。

```bash
% npm create amplify@latest --legacy-peer-deps
Need to install the following packages:
create-amplify@1.0.6
Ok to proceed? (y) y


> amplifyapp@0.1.0 npx
> create-amplify

? Where should we create your project? .

```
(依存関係でinstallうまくいかなかったので --legacy-peer-depsつけました、のちに苦しみます)


```bash
% npx ampx sandbox
SSMCredentialsError: AccessDeniedException: User: arn:aws:iam:::user/amplify-dev is not authorized to perform: ssm:GetParameter on resource: arn:aws:ssm:us-east-1::parameter/cdk-bootstrap//version because no identity-based policy allows the ssm:GetParameter action
Resolution: Make sure your AWS credentials are set up correctly and have permissions to call SSM:GetParameter
```

なにやらアクセス権限のエラーが出てます。
作成したIAMユーザにAmazonSSMReadOnlyAccessポリシーを追加しました。
3分くらい待って反映されたので再度 `npx ampx sandbox` を実行...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/84b8d588-171f-2849-eeb0-286b7e087400.png)

今すぐ設定を初期化をクリック

<img width="49%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/e9895d49-ef64-532b-913b-86a426fe88eb.png">

<img width="49%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/1d493d1b-62ca-ca44-6cbe-1843584f61ed.png">

できたっぽいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/3c75dd2d-1042-8188-fead-a336776fe143.png)

さっき無理くりバージョンエラー通したとこのせいでデプロイ通らない...
バージョンを修正します。

```bash
npm install "typescript@^4.0.0"
git add .
git commit -m “バージョン修正”
git push origin main
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/f104f6a3-2a8c-d5b4-e17b-df827a087173.png)

無事通りました:sweat: 

## 認証を追加する

Amplify CLI とライブラリを使用して、認証を設定し、アプリに追加します

ハンズオンの手順だとGen1のCLI使ってるのでGen2の手順でやっていきます

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

### 1. Amplifyライブラリをインストールする

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

### 2.フロントに認証フローを追加する

https://docs.amplify.aws/react/build-a-backend/auth/connect-your-frontend/using-the-authenticator/

```jsx:src/App.js
import { Authenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import outputs from "amplify_outputs.json";
import { Amplify } from 'aws-amplify';

Amplify.configure(outputs);

export default function App() {
  return (
    <Authenticator>
      {({ signOut, user }) => (
        <main>
          <h1>Hello {user?.username}</h1>
          <button onClick={signOut}>Sign out</button>
        </main>
      )}
    </Authenticator>
  );
}
```

amplify_outputs.jsonが元々プロジェクトルートにあったんですがエラーになってしまうのでsrcに移しました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/9ba7e7d3-66f1-93b2-3116-a439f212b325.png)

なんとか表示はできました

# ここで気づいた

ハンズオンの手順いろいろ古いのでそれでいろいろつまづいている...？
下記のクイックスタートからやった方が楽だったかも...？
https://docs.amplify.aws/react/start/quickstart/

ちょっとやり直してみました。

## Deploy a fullstack app to AWS

### 1. テンプレートからリポジトリを作成する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/742c54b1-6e78-9bc1-3d97-dec12ddd1f2f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/79250b76-a853-26a7-c770-0526042ae1ca.png)


### 2. アプリケーションをデプロイする

初回との違いみたいなとこで行くと... フレームワークの部分がAmplify Gen 2になっている :thinking: 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/c2462ca2-1059-5013-1afa-7d76fb1e07f5.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/7ab6630e-2647-cdaa-2018-78ab25dce25e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/d68b102f-47ed-089f-6525-470f8826411b.png)

保存してデプロイすると次のようなページが起動しました。 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/bce30410-a33d-2da7-3041-c502823b51de.png)

## Make frontend updates

### 3. ローカル環境を準備する

```
git clone https://github.com/<github-user>/amplify-vite-react-template.git
cd amplify-vite-react-template && npm install
```

amplify_outputs.jsonをダウンロードし、プロジェクトルートに配置する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/dad499d0-e607-3114-2119-796019b97df0.png)

```
├── amplify
├── src
├── amplify_outputs.json <== backend outputs file
├── package.json
└── tsconfig.json
```

### 4.TODO削除関数を実装する

リストがクリックされたらTodoが削除されるように変更します。

```tsx:src/App.tsx
import { generateClient } from "aws-amplify/data";
import { useEffect, useState } from "react";
import type { Schema } from "../amplify/data/resource";

const client = generateClient<Schema>();

function App() {
  const [todos, setTodos] = useState<Array<Schema["Todo"]["type"]>>([]);

  useEffect(() => {
    client.models.Todo.observeQuery().subscribe({
      next: (data) => setTodos([...data.items]),
    });
  }, []);

  function createTodo() {
    client.models.Todo.create({ content: window.prompt("Todo content") });
  }

  function deleteTodo(id: string) {
    client.models.Todo.delete({ id })
  }

  return (
    <main>
      <h1>My todos</h1>
      <button onClick={createTodo}>+ new</button>
      <ul>
        {todos.map((todo) => (
          <li 
            onClick={() => deleteTodo(todo.id)}
            key={todo.id}>
            {todo.content}
          </li>
        ))}
      </ul>
      <div>
        🥳 App successfully hosted. Try creating a new todo.
        <br />
        <a href="https://docs.amplify.aws/react/start/quickstart/#make-frontend-updates">
          Review next step of this tutorial.
        </a>
      </div>
    </main>
  );
}

export default App;
```

サーバー起動

```bash
npm run dev
```

http://localhost:5173/ にアクセスして、TODOをクリックしたら削除できることを確認します。

### 5.ログインUIを実装する

```tsx:src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { Authenticator } from '@aws-amplify/ui-react';
import App from "./App.tsx";
import "./index.css";
import { Amplify } from "aws-amplify";
import outputs from "../amplify_outputs.json";
import '@aws-amplify/ui-react/styles.css';

Amplify.configure(outputs);

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <Authenticator>
      <App />
    </Authenticator>
  </React.StrictMode>
);

```

```tsx:src/App.tsx
import { useAuthenticator } from '@aws-amplify/ui-react';
import { generateClient } from "aws-amplify/data";
import { useEffect, useState } from "react";
import type { Schema } from "../amplify/data/resource";

const client = generateClient<Schema>();

function App() {
  const { signOut } = useAuthenticator();
  const [todos, setTodos] = useState<Array<Schema["Todo"]["type"]>>([]);

  useEffect(() => {
    client.models.Todo.observeQuery().subscribe({
      next: (data) => setTodos([...data.items]),
    });
  }, []);

  function createTodo() {
    client.models.Todo.create({ content: window.prompt("Todo content") });
  }

  function deleteTodo(id: string) {
    client.models.Todo.delete({ id })
  }

  return (
    <main>
      <h1>My todos</h1>
      <button onClick={createTodo}>+ new</button>
      <ul>
        {todos.map((todo) => (
          <li 
            onClick={() => deleteTodo(todo.id)}
            key={todo.id}>
            {todo.content}
          </li>
        ))}
      </ul>
      <div>
        🥳 App successfully hosted. Try creating a new todo.
        <br />
        <a href="https://docs.amplify.aws/react/start/quickstart/#make-frontend-updates">
          Review next step of this tutorial.
        </a>
      </div>
      <button onClick={signOut}>Sign out</button>
    </main>
  );
}

export default App;

```

メールアドレスとパスワードをいれてCreateAccountします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/b2c4411a-30e9-d198-d701-70fc2cda7ca8.png)

メールに記載されてるConfirmation Codeを入力し、Confirmをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/e392f29a-9431-ef6e-07d2-86d3b62e815e.png)

SignOutをクリックしたり、登録したメールアドレスとパスワードでログインできることを確認しました。

### 6.変更を反映する

```bash
git commit -am "added authenticator"
git push
```

## Make backend updates

ここまででもう データも認証も使えるようになっている... 最新の手順めっちゃいいですね

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/dd610a08-3e17-49ac-957c-d356921d5f91.png)

![スクリーンショット 2024-11-29 21.35.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/79c59feb-99e7-d5f9-66e6-08365c5539f0.png)


```bash
npx ampx sandbox
```

> 本番ブランチに影響を与えずにバックエンドを更新するには、Amplify のクラウド サンドボックスを使用します。この機能は、チーム内の各開発者に個別のバックエンド環境を提供するため、ローカル開発とテストに最適です。

### 7. ユーザーごとの認証を実装する

```ts:amplify/data/resource.ts
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";

const schema = a.schema({
  Todo: a
    .model({
      content: a.string(),
    }).authorization(allow => [allow.owner()]),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'userPool',
  },
});
```

現在は本番ではなく、sandboxを使用してるので本番で登録しているユーザではログインができないです。
新しくユーザを登録します。

![スクリーンショット 2024-11-29 21.45.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/baca24af-b685-7c57-12a6-b4504a0d7230.png)
![スクリーンショット 2024-11-29 21.48.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/120e051d-a376-92a4-0f00-a8571a2e285b.png)

sandboxで登録したTODOは本番に影響がないことも確認しました。
![スクリーンショット 2024-11-29 21.49.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/ff8081e2-7f27-a6a9-45f9-9c5a0d3b61b2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/9a8e380d-4eb3-6249-c9bc-7f58e6e18c23.png)

変更を反映する

```bash
git commit -am "added per-user data isolation"
git push
```

### 8. 新しいデータを追加してみた

content、name、descriptionを持つNoteを追加します

```ts:data/resource.ts
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";

const schema = a.schema({
  Todo: a
    .model({
      content: a.string(),
    }).authorization(allow => [allow.owner()]),
  Note: a.model({
    content: a.string(),
    name: a.string(),
    description: a.string()
  }).authorization(allow => [allow.owner()]),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'userPool',
  },
});
```

```tsx:App.tsx
import { Button, Flex, Heading, Text, TextField, useAuthenticator, View } from '@aws-amplify/ui-react';
import { generateClient } from "aws-amplify/data";
import { ChangeEvent, FormEvent, useEffect, useState } from "react";
import type { Schema } from "../amplify/data/resource";

const client = generateClient<Schema>();

function App() {  
  const { user, signOut } = useAuthenticator();
  const [todos, setTodos] = useState<Array<Schema["Todo"]["type"]>>([]);
  const [notes, setNotes] = useState<Array<Schema["Note"]["type"]>>([]);
  const [formData, setFormData] = useState({
    name: '',content: '',description: ''
  });
  useEffect(() => {
    client.models.Todo.observeQuery().subscribe({
      next: (data) => setTodos([...data.items]),
    });
    client.models.Note.observeQuery().subscribe({
      next: (data) => setNotes([...data.items]),
    });
  }, []);

  async function createNote(e?: FormEvent<HTMLFormElement>) {
    e?.preventDefault();
    client.models.Note.create(formData);
    setFormData({
      name: '',content: '',description: ''
    });
  }

  function createTodo() {
    client.models.Todo.create({ content: window.prompt("Todo content") });
  }

  function deleteNote(id: string) {
    client.models.Note.delete({ id });
  }

  function deleteTodo(id: string) {
    client.models.Todo.delete({ id })
  }

  const changeNote = (e: ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value,
    });
  };
  return (
    <main>
      <h1>{user?.signInDetails?.loginId}'s todos</h1>
      <button onClick={createTodo}>+ new</button>
      <ul>
        {todos.map((todo) => (
          <li 
            onClick={() => deleteTodo(todo.id)}
            key={todo.id}>
            {todo.content}
          </li>
        ))}
      </ul>
      <h1>{user?.signInDetails?.loginId}'s notes</h1>
      <View as="form" margin="3rem 0" onSubmit={createNote}>
        <Flex direction="row" justifyContent="center">
          <TextField
            name="name"
            placeholder="Note Name"
            label="Note Name"
            labelHidden
            variation="quiet"
            onChange={changeNote}
            required
          />
          <TextField
            name="description"
            placeholder="Note Description"
            label="Note Description"
            labelHidden
            variation="quiet"
            onChange={changeNote}
            required
          />
          <TextField
            name="content"
            placeholder="Note Content"
            label="Note Content"
            labelHidden
            variation="quiet"
            onChange={changeNote}
            required
          />
          <Button type="submit" variation="primary">
            Create Note
          </Button>
        </Flex>
      </View>
      <Heading level={2}>Current Notes</Heading>
      <View margin="3rem 0">
        {notes.map((note) => (
          <Flex
            key={note.id || note.name}
            direction="row"
            justifyContent="center"
            alignItems="center"
          >
            <Text as="strong" fontWeight={700}>
              {note.name}
            </Text>
            <Text as="span">{note.content}</Text>
            <Button variation="link" onClick={() => deleteNote(note.id)}>
              Delete note
            </Button>
          </Flex>
        ))}
      </View>
      <div>
        🥳 App successfully hosted. Try creating a new todo.
        <br />
        <a href="https://docs.amplify.aws/react/start/quickstart/#make-frontend-updates">
          Review next step of this tutorial.
        </a>
      </div>
      <button onClick={signOut}>Sign out</button>
    </main>
  );
}

export default App;
```