【今日から始めるAWS】Amplifyを使ってReactアプリをデプロイする

#はじめに
30代未経験からエンジニア転職をめざすコーディング初学者のYNと申します。お読みいただきありがとうございます。
今回ご紹介する`Amplify`では、AWSのサービスを組み合わせて、驚くほど簡単に、サーバレスなwebアプリをデプロイし、かつCI/CDパイプラインまで作ることができます。
本記事では、[チュートリアル](https://aws.amazon.com/getting-started/hands-on/build-react-app-amplify-graphql/)に従って進めることで、`Amplify`の基本的な仕組みの理解を備忘録としてまとめさせていただきました。
**また、[チュートリアル](https://aws.amazon.com/getting-started/hands-on/build-react-app-amplify-graphql/)の通りに進めると、（2020年8月現在）ビルドエラーによりデプロイが失敗しますので、対処法も併せてまとめておきたいと思います。**


#今回やったこと
[チュートリアル](https://aws.amazon.com/getting-started/hands-on/build-react-app-amplify-graphql/)に従い、`Amplify`を使って、下記AWSサービスを組み合わせてReactアプリをデプロイしました。

* `DynamoDB`
* `Cognito`
* `AppSync`
* `S3`
* `CloudFront`


<img width="710" alt="スクリーンショット 2020-08-02 17.39.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572098/7177e44d-e1f9-48d2-279e-0b2b20e6fd7b.png">
*上記アーキテクチャ図は[こちらの記事](https://xp-cloud.jp/blog/2020/03/18/6870/)から拝借いたしました。



#1. Reactアプリをデプロイし、パイプラインを作成
[チュートリアル#1](https://aws.amazon.com/getting-started/hands-on/build-react-app-amplify-graphql/module-one/)に従い、githubレポジトリにcommit（masterブランチにマージ）すると自動的にデプロイされるパイプラインを作ります。
`Amplify`では、[`CloudFront`](https://www.acrovision.jp/service/aws/?p=763)というAWSの[CDNサービス](https://knowledge.sakura.ad.jp/19191/)と自動的に連携して、サーバレスにwebサイトをデプロイできます。




#2. Amplifyの初期設定を行う
[チュートリアル#2](https://aws.amazon.com/getting-started/hands-on/build-react-app-amplify-graphql/module-two/)をに従い、`Amplify`の初期設定を行います。
やることは下記3つです。

* **AmplifyCLIをローカルPCにグローバルインストール**

```
$ npm install -g @aws-amplify/cli
```

* **AWSアカウントの紐付け**（[解説動画](https://www.youtube.com/watch?v=fWbM5DLh25U)あり。
[こちらの記事](https://qiita.com/Junpei_Takagi/items/f2bc567761880471fd54#aws%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%AE%E7%B4%90%E4%BB%98%E3%81%91)も参考にさせていただきました。）

```
$ amplify configure
```

* **AWSバックエンドのセットアップ**

```
$ amplify init --appId [your-app-id]
```


#3. Cognitoを使ってwebページにログイン機能を実装する
[チュートリアル#3](https://aws.amazon.com/getting-started/hands-on/build-react-app-amplify-graphql/module-three/)をに従って進めていきます。
`Amplify`を使ってReactアプリを`Cognito`と連携させ、驚くほど簡単にユーザー管理機能を実装することができます。


やることは下記3つです。

* **Amplifyライブラリのインストール（ルートディレクトリでnode_modulesに追加)**

```
$ npm install aws-amplify @aws-amplify/ui-react
```

* **Amplifyを使って認証機能の追加**

```
$ amplify add auth
$ amplify push --y
```
上記の操作により、バックエンド設定ファイルが生成され、`/src`に`aws-exports.js`が追加されます。

* **Reactに認証機能コンポーネントを追加**

`aws-exports.js`をフロントエンドで読み込むことにより、AmplifyのバックエンドとReactアプリをつなぐことができます。

```javascript:src/index.js
// 下記をimport
import Amplify from 'aws-amplify';
import config from './aws-exports';
Amplify.configure(config);
```

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

// 下記ｌコンポーネントをinportしてwrapする
import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react'

function App() {
  return (
    <div className="App">
      <header>
        <img src={logo} className="App-logo" alt="logo" />
        <h1>We now have Auth!</h1>
      </header>
      <AmplifySignOut />
    </div>
  );
}

export default withAuthenticator(App);
```


ここで実装したユーザー管理情報は、`Cognito`のコンソール画面で操作することができます。

この後、リモートのmasterブランチにマージすると自動的に変更が反映されてwebページがデプロイされます。
**が、この状態ではビルドエラーが発生しますので、次に対処法をまとめます。**

#3-2. ビルドエラーを解決する 
**[チュートリアル](https://aws.amazon.com/getting-started/hands-on/build-react-app-amplify-graphql/)の通りに進めるとビルドエラーによりデプロイが失敗します**ので、ここに対処法をまとめます。
console画面より、ビルドに関して下記３点の追加設定が必要です。

* **フロントエンドでバックエンドをデプロイする**

下記のようにビルドのYML設定を追記します。
<img width="1644" alt="スクリーンショット 2020-08-05 11.05.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572098/5caded53-8cc8-ee8f-5e26-636205326e16.png">
上記により、フロントエンドを構築するまえに適切なバックエンド環境を構築し、内部で`aws-exports.js`を生成します。これをしないと、フロントエンドのビルド時に`aws-exports.js`が見つからずエラーになってしまいます。

* **フロントエンドでバックエンドリソースをデプロイするためのアクセス許可**

上記のように、フロントエンドでバックエンドをデプロイするためには、[ユーザーガイド](https://docs.aws.amazon.com/ja_jp/amplify/latest/userguide/how-to-service-role-amplify-console.html)に従い、サービスロールを作成する必要があります。
<img width="1662" alt="スクリーンショット 2020-08-05 11.44.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572098/76768970-d147-6976-61c8-0c5416182f1a.png">


* **ビルド設定を`Amplify CLI`の最新版に更新**

ビルド設定の`Build image settings`を編集し、`Amplify CLI`のversionを最新のものに更新します。これをしないと、デフォルトのruntimeに設定されているnodeのversionが古いため、ビルドが失敗します。
<img width="1636" alt="スクリーンショット 2020-08-05 11.49.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572098/34a10598-76ec-52cf-9cd4-ce540c7c73f2.png">




#4. GraphQL-APIを使ってReactアプリとデータベースを連携させる
[チュートリアル#4](https://aws.amazon.com/getting-started/hands-on/build-react-app-amplify-graphql/module-four/)をに従い、`GraphQL`の初期設定、およびデータベースとの連携を行います。

*`GraohQL`はfacebookが開発したwebAPIで、クエリ定義の自由度が高く、一度のリクエストで効率的に多くのデータを得ることができます。stateとの連携が容易でReactと相性がよく、とても使いやすいAPIです。
やることは下記3つです。

* **GraphQL-APIとデータベースの設定**

```
$ amplify add api
? Please select from one of the below mentioned services: GraphQL
? Provide API name: notesapp
? Choose the default authorization type for the API: API Key
? Enter a description for the API key: demo
? After how many days from now the API key should expire: 7 (or your preferred expiration)
? Do you want to configure advanced settings for the GraphQL API: No, I am done.
? Do you have an annotated GraphQL schema?  No
? Do you want a guided schema creation?  Yes
? What best describes your project: Single object with fields
? Do you want to edit the schema now? Yes
```

* **GraphQL-APIを`App-Sync`にデプロイ**

```
$ amplify push --y
```
上記の操作により、AWSの`App-Sync`と`Dynamo-DB`のバックエンド自動的に構築されます。また、クエリーやミューテーションの設定ファイルが自動で生成されて`/src/graphql`に追加されます。（すごい）


* **GraphQLをつかってReactアプリとデータベースを連携させる**

```javascript:src/app.js
import React, { useState, useEffect } from "react";
import "./App.css";
import { API } from "aws-amplify";
import { withAuthenticator, AmplifySignOut } from "@aws-amplify/ui-react";
import { listNotes } from "./graphql/queries";
import {
  createNote as createNoteMutation,
  deleteNote as deleteNoteMutation,
} from "./graphql/mutations";

const initialFormState = { name: "", description: "" };

function App() {
  const [notes, setNotes] = useState([]);
  const [formData, setFormData] = useState(initialFormState);

  useEffect(() => {
    fetchNotes();
  }, []);
  async function fetchNotes() {
    const apiData = await API.graphql({ query: listNotes });
    setNotes(apiData.data.listNotes.items);
  }

  async function createNote() {
    if (!formData.name || !formData.description) return;
    await API.graphql({
      query: createNoteMutation,
      variables: { input: formData },
    });
    setNotes([...notes, formData]);
    setFormData(initialFormState);
  }

  async function deleteNote({ id }) {
    const newNotesArray = notes.filter((note) => note.id !== id);
    setNotes(newNotesArray);
    await API.graphql({
      query: deleteNoteMutation,
      variables: { input: { id } },
    });
  }
  return (
    <div className="App">
      <h1>My Notes App</h1>
      <input
        onChange={(e) => setFormData({ ...formData, name: e.target.value })}
        placeholder="Note name"
        value={formData.name}
      />
      <input
        onChange={(e) =>
          setFormData({ ...formData, description: e.target.value })
        }
        placeholder="Note description"
        value={formData.description}
      />
      <button onClick={createNote}>Create Note</button>
      <div style={{ marginBottom: 30 }}>
        {notes.map((note) => (
          <div key={note.id || note.name}>
            <h2>{note.name}</h2>
            <p>{note.description}</p>
            <button onClick={() => deleteNote(note)}>Delete note</button>
          </div>
        ))}
      </div>
      <AmplifySignOut />
    </div>
  );
}

export default withAuthenticator(App);
```

#5. S3を使ってReactアプリに写真を保存する
[チュートリアル#5](https://aws.amazon.com/getting-started/hands-on/build-react-app-amplify-graphql/module-five/)をに従い、Reactアプリと`S3`を連携させて、写真をストレージに保存できるようにします。

やることは下記3つです。

* **GraphQLスキーマの更新**

```javascript:amplify/backend/api/notesapp/schema.graphql

type Note @model {
  id: ID!
  name: String!
  description: String
  image: String
}
```

* **S3によるストレージ機能の追加**

```
$ amplify add storage
$ amplify push --y
```

* **ReactアプリとS3を連携させる**

```javascript:src/app.js
import React, { useState, useEffect } from "react";
import "./App.css";
import { API, Storage } from "aws-amplify";
import { withAuthenticator, AmplifySignOut } from "@aws-amplify/ui-react";
import { listNotes } from "./graphql/queries";
import {
  createNote as createNoteMutation,
  deleteNote as deleteNoteMutation,
} from "./graphql/mutations";

const initialFormState = { name: "", description: "" };

function App() {
  const [notes, setNotes] = useState([]);
  const [formData, setFormData] = useState(initialFormState);

  useEffect(() => {
    fetchNotes();
  }, []);
  
  async function fetchNotes() {
    const apiData = await API.graphql({ query: listNotes });
    const notesFromAPI = apiData.data.listNotes.items;
    await Promise.all(
      notesFromAPI.map(async (note) => {
        if (note.image) {
          const image = await Storage.get(note.image);
          note.image = image;
        }
        return note;
      })
    );
    setNotes(apiData.data.listNotes.items);
  }

  async function createNote() {
    if (!formData.name || !formData.description) return;
    await API.graphql({
      query: createNoteMutation,
      variables: { input: formData },
    });
    if (formData.image) {
      const image = await Storage.get(formData.image);
      formData.image = image;
    }
    setNotes([...notes, formData]);
    setFormData(initialFormState);
  }

  async function deleteNote({ id }) {
    const newNotesArray = notes.filter((note) => note.id !== id);
    setNotes(newNotesArray);
    await API.graphql({
      query: deleteNoteMutation,
      variables: { input: { id } },
    });
  }

  async function onChange(e) {
    if (!e.target.files[0]) return;
    const file = e.target.files[0];
    setFormData({ ...formData, image: file.name });
    await Storage.put(file.name, file);
    fetchNotes();
  }

  return (
    <div className="App">
      <h1>My Notes App</h1>
      <input
        onChange={(e) => setFormData({ ...formData, name: e.target.value })}
        placeholder="Note name"
        value={formData.name}
      />
      <input
        onChange={(e) =>
          setFormData({ ...formData, description: e.target.value })
        }
        placeholder="Note description"
        value={formData.description}
      />
      <input type="file" onChange={onChange} />
      <button onClick={createNote}>Create Note</button>
      <div style={{ marginBottom: 30 }}>
        {notes.map((note) => (
          <div key={note.id || note.name}>
            <h2>{note.name}</h2>
            <p>{note.description}</p>
            <button onClick={() => deleteNote(note)}>Delete note</button>
            {note.image && <img src={note.image} style={{ width: 400 }} />}
          </div>
        ))}
      </div>
      <AmplifySignOut />
    </div>
  );
}

export default withAuthenticator(App);

```

これで、`Cognito` `AppSync` `S3`を組み合わせてReactアプリをデプロイすることができました。

#最後に
`Amplify`を使うことで、モダンなサーバーレスアプリを簡単にデプロイすることができます。コーディング初学者には敷居が高く感じられるAWSですが、とても親近感を感じることができました。
チュートリアルには、「50分で終わる」と書かれていましたが、コーディング初心者でもその程度で出来ました。如何に`Amplify`が素晴らしいサービスであるかが納得いただけるかと思います。（ただし、ビルドエラーの対処には3日かかりました。苦笑）

お読みいただきありがとうございました。


#参考にさせていただいた記事

* ビルド失敗時のエラー対処
https://stackoverflow.com/questions/59708481/aws-amplify-deploy-failure-due-to-aws-exports
https://github.com/aws-amplify/amplify-cli/issues/3149
https://docs.aws.amazon.com/ja_jp/amplify/latest/userguide/amplify-console-ug.pdf
https://shimba.io/tatsuyoshi/article/01E13AH3HGHRR0CQC4BNC966MF
https://kahoo.blog/aws-amplify-setup-frontend-and-backend-deploy/

* Amplify解説
https://egghead.io/courses/building-serverless-web-applications-with-react-aws-amplify