【初心者向けワークショップ】Cloud9+React+TypeScript+Amplify+Cypressでアプリの公開からCI/CD+E2Eテスト自動化までやってみよう！

# はじめに
AWSアカウントをまだ持っておられない方や開発環境が手元にない方でも、
簡単にWebアプリケーションの公開、CI/CDのパイプラインの構築、テスト自動化まで
体験できるワークショップです。

AWSアカウントの取得がまだの方は公式の[AWSアカウント作成の流れ]
(https://aws.amazon.com/jp/register-flow/)を参照して
アカウントを取得しておいてください。
初めての方でも上記ガイドが丁寧なため安心して進められます。

# 開発環境の構築
クラウドベースの統合開発環境(IDE)であるAWS Cloud9を使用します。
Cloud9上でのAWS amplify, AWS appsyncを利用する開発環境を構築していきます。

## Cloud9のセットアップ
* AWS マネジメントコンソールにログインして、サービスからAWS Cloud9を選択する
    * リージョンは東京
    * ![スクリーンショット 2020-08-25 10.30.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/e599cf35-8bd1-88a9-ff63-6eee32c7d872.png)
* 「Create environment」ボタンをクリックする
    * ![スクリーンショット 2020-08-25 10.34.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/3d04ea7d-4ffb-f622-35f2-b7cceda381fc.png)
* Name environmentの項目を入力して(以下は入力例)、「Next Step」ボタンをクリックする
    * Name : amplify-workshop
    * Description : amplify workshop
    * ![スクリーンショット 2020-08-25 10.40.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/affde05a-4073-3cf4-5a5a-4a4b99bf6564.png)
* Configure settingsの項目を入力して(以下は入力例)、「Next Step」ボタンをクリックする
    * Environment type : Create a new EC2 instance for environment (direct access)
    * Instance type : t2.small
    * Platform : Amazon Linux
    * Cost-saving setting : after 30 minutes (default)
    * ![スクリーンショット 2020-08-25 10.46.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/8fffc2f8-29c5-0605-275e-3c6f3ca572fa.png)
* 設定内容を確認して「Create Environment」をクリックする
    * ![スクリーンショット 2020-08-25 10.48.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/9abc2662-1ba3-99f9-3200-f0050d2854f3.png)
* しばらくして、次のような画面が出たら成功
    * ![スクリーンショット 2020-08-25 10.52.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/8b59d1b0-2d81-2b43-097f-a394f614962b.png)

## AWS 管理の一時認証情報 の無効化
Cloud9では、初回にコンソールを開くとAWS管理の一時認証情報が作成されます。
今回はAmplify CLIで発行するユーザの権限を使用するためCloud9の一時認証情報を無効化しておきます。

* AWS Cloud9 > Preferences > AWS SETTINGS > 「AWS managed temporary credentials:」 のトグルボタンをOffにする 
    * ![スクリーンショット 2020-08-25 10.55.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/ba6459ad-7130-057e-d302-02ea7c8f88bf.png)
    * ![スクリーンショット 2020-08-25 10.56.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/390182e2-2b4c-ce40-fc96-0cb8880cbaba.png)
    * ![スクリーンショット 2020-08-25 10.57.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/42b17ab7-a64f-7e77-a2b2-1288e816fa70.png)

## 開発に必要なパッケージのインストール
### create-react-appのインストール
* create-react-appコマンドとは
    * React.js のひな形を簡単に作れるありがたいコマンド
    * React.js で本格的なアプリを作る際に必要な設定変更などのステップがこのコマンドを使用することでカットできます
* インストール
    * `$ npm install -g create-react-app`
* 確認
    * `$ create-react-app --version` 

### Amplify-CLIのインストール
* Amplify-CLI
    * Amplify-CLIをインストールするには、Node.jsのバージョンが8.11.0以上である必要があります
* インストール
    * `$ npm install -g @aws-amplify/cli` 
* 確認
    * `$ amplify --version`

## 開発環境
ここまでで構築した開発環境は以下のとおりです。

```
ec2-user:~/environment $ node -v
v10.22.0
ec2-user:~/environment $ npm -v
6.14.6
ec2-user:~/environment $ create-react-app --version
3.4.1
ec2-user:~/environment $ amplify --version
4.27.3
```

## AWSアカウントの紐付け

```
ec2-user:~/environment/amplify-sns-workshop/boyaki (master) $ 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:  ap-northeast-1
Specify the username of the new IAM user:
? user name:  amplify-workshop
Complete the user creation using the AWS console
https://console.aws.amazon.com/iam/home?region=undefined#/users$new?step=final&accessKey&userNames=amplify-workshop&permissionType=policies&policies=arn:aws:iam::aws:policy%2FAdministratorAccess
Press Enter to continue

Enter the access key of the newly created user:
? accessKeyId:  *********************
? secretAccessKey:  ****************************************
This would update/create the AWS Profile in your local machine
? Profile Name:  amplify-test

Successfully set up the new user.
```

### Amplify用のIAMユーザ作成
`$ amplify configure` を実行した際に、「Complete the user creation using the AWS console」の下に表示されるURLにブラウザでアクセスすると、IAMユーザの追加画面に遷移します。
以下のように、進めていきます。

* ユーザー詳細の設定
    * ユーザー名 : ターミナルで指定したuser nameが入力されている
    * ユーザー名が入力されていることを確認して「次のステップ」を選択する
    * ![スクリーンショット 2020-08-25 13.12.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/670592aa-a536-50a6-0808-e4044e3f9b4e.png)
* アクセス許可の設定
    * 既存ポリシー「AdministratorAccess」のポリシーが選択された状態になっているので、特に必要で無ければ「次のステップ」を選択する
    * ![スクリーンショット 2020-08-25 13.13.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/bf0bbdf1-4bd2-d8f6-4806-559d0ae97c9c.png)
* タグの追加
    * 特に必要で無ければ「次のステップ」を選択する
    * ![スクリーンショット 2020-08-25 13.14.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/33696176-b249-7f26-c570-5480d01227de.png)
* 確認
    * 「ユーザーの作成」を選択する
    * ![スクリーンショット 2020-08-25 13.14.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/faa7f135-84a3-b148-84a5-64e6e3f37cc6.png)
    * ![スクリーンショット 2020-08-25 13.26.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/6d6093c7-a19f-e6cb-90b0-c47f3380f653.png)


以上で、Amplify用のIAMユーザ作成が完了しました。
コンソールに戻り、accessKeyId、secretAccessKeyを入力します。

## AWS Cloud9で利用しているEBSボリューム領域を拡張する
ワークショップを進める上で `./node_modules` 配下が膨れていくため、
こちらの記事を参考にしてボリュームを拡張することをお勧めします。
[AWS Cloud9 で利用しているEBS ボリューム領域を拡張する](https://qiita.com/ktrkmk/items/8cf1e100da2e717f3be2)

# 新規 Reactアプリケーションの作成

## 作業ディレクトリの作成

```
ec2-user:~/environment $ mkdir amplify-sns-workshop
ec2-user:~/environment $ cd amplify-sns-workshop/
ec2-user:~/environment/amplify-sns-workshop $ 
```

## Reactアプリケーションのひな形の作成
create-react-appコマンドを利用して、Reactアプリケーションのひな形を作成していきます。
以下、JavaScriptを使用する場合の手順ですが、TypeScriptを使用する場合の差分も追記しています。

(JavaScriptの場合)

```
ec2-user:~/environment/amplify-sns-workshop $ npx create-react-app boyaki

Creating a new React app in /home/ec2-user/environment/amplify-sns-workshop/boyaki.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template...


(中略)


Created git commit.

Success! Created boyaki at /home/ec2-user/environment/amplify-sns-workshop/boyaki
Inside that directory, you can run several commands:

  npm start
    Starts the development server.

  npm run build
    Bundles the app into static files for production.

  npm test
    Starts the test runner.

  npm run eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!

We suggest that you begin by typing:

  cd boyaki
  npm start

Happy hacking!
ec2-user:~/environment/amplify-sns-workshop $ cd boyaki
ec2-user:~/environment/amplify-sns-workshop/boyaki (master) $ 
```

(TypeScriptの場合)

```
$ create-react-app boyaki --typescript
Creating a new React app in /home/ec2-user/environment/amplify-sns-workshop-ts/boyaki.

The --typescript option has been deprecated and will be removed in a future release.
In future, please use --template typescript.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template-typescript...

(中略)

Created git commit.

Success! Created boyaki-ts at /home/ec2-user/environment/amplify-sns-workshop-ts/boyaki
Inside that directory, you can run several commands:

  npm start
    Starts the development server.

  npm run build
    Bundles the app into static files for production.

  npm test
    Starts the test runner.

  npm run eject
    Removes this tool and copies build dependencies, configuration files
Compiled successfully!
```

tsファイルが作られています。

```
ec2-user:~/environment/amplify-sns-workshop/boyaki (master) $ ls -l src/
total 44
-rw-rw-r-- 1 ec2-user ec2-user  564 Sep  2 00:39 App.css
-rw-rw-r-- 1 ec2-user ec2-user  280 Sep  2 00:39 App.test.tsx
-rw-rw-r-- 1 ec2-user ec2-user  556 Sep  2 00:39 App.tsx
-rw-rw-r-- 1 ec2-user ec2-user  218 Sep  2 00:46 aws-exports.js
-rw-rw-r-- 1 ec2-user ec2-user  366 Sep  2 00:39 index.css
-rw-rw-r-- 1 ec2-user ec2-user  503 Sep  2 00:39 index.tsx
-rw-rw-r-- 1 ec2-user ec2-user 2671 Sep  2 00:39 logo.svg
-rw-rw-r-- 1 ec2-user ec2-user   40 Sep  2 00:39 react-app-env.d.ts
-rw-rw-r-- 1 ec2-user ec2-user 5295 Sep  2 00:39 serviceWorker.ts
-rw-rw-r-- 1 ec2-user ec2-user  255 Sep  2 00:39 setupTests.ts
```

最後にReactアプリケーションのひな形ができていることを確認していきます。
`$ npm start` を実行して、Cloud9のメニューの「Preview」 > 「Preview Running Application」を実行します。
以下のように表示されていたらOKです。

* ![スクリーンショット 2020-08-25 13.40.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/347f593b-2ac9-f6a1-e3f4-da180cb06e35.png)
* ![スクリーンショット 2020-08-25 13.41.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/182c42d3-70c9-60d2-9b76-050b9ea3eb00.png)


## Amplifyの初期化
`$ cd boyaki` で移動したReactのプロジェクトルートディレクトリ(boyaki)のトップで `$ amplify init`を実行します。
これにより、AWS上にリソースを構築してAmplify用のプロジェクトを準備されます。

```
ec2-user:~/environment/amplify-sns-workshop/boyaki (master) $ amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project boyaki
? Enter a name for the environment production
? Choose your default editor: Vim (via Terminal, Mac OS only)
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using react
? Source Directory Path:  src
? Distribution Directory Path: build
? Build Command:  npm run-script build
? Start Command: npm run-script start
Using default provider  awscloudformation

For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html

? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use amplify-test
Adding backend environment production to AWS Amplify Console app: d1vrmb49bf11cu
⠦ Initializing project in the cloud...


(中略)


✔ Successfully created initial AWS cloud resources for deployments.
✔ Initialized provider successfully.
Initialized your environment successfully.

Your project has been successfully initialized and connected to the cloud!

Some next steps:
"amplify status" will show you what you've added already and if it's locally configured or deployed
"amplify add <category>" will allow you to add features like user login or a backend API
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify console" to open the Amplify Console and view your project status
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud

Pro tip:
Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything

```

## サンプルアプリケーションを作成
AWSの公式の[チュートリアル](https://amplify-sns.workshop.aws/30_mock.html)に沿ってアプリケーションを作成していきます。
ユーザーが最大140字程度の短いテキストを投稿できる「Boyaki」というアプリケーションで以下の機能があります。

* ログイン機能
* ユーザーが自分名義でつぶやきを投稿する機能

ここで、最終目標のCypressを使ったテスト実行に最低限必要な機能(チュートリアルの3.2[認証機能]〜3.5[Post機能: Front-end])を作成していきます。

### 認証機能の追加

* `$ amplify add auth` を実行し、認証機能をアプリへ追加する

```
ec2-user:~/environment/amplify-sns-workshop/boyaki (master) $ amplify add auth
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
 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.
Successfully added resource boyakie06b1145 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
```

* `$ amplify status` を実行し、Auth Categoryのリソースが追加されていることを確認する

```
ec2-user:~/environment/amplify-sns-workshop/boyaki (master) $ amplify status

Current Environment: production

| Category | Resource name  | Operation | Provider plugin   |
| -------- | -------------- | --------- | ----------------- |
| Auth     | boyakie06b1145 | Create    | awscloudformation |
```

* `$ amplify push` を実行してクラウドへ変更を反映する

```
ec2-user:~/environment/amplify-sns-workshop/boyaki (master) $ amplify push
✔ Successfully pulled backend environment production from the cloud.

Current Environment: production

| Category | Resource name  | Operation | Provider plugin   |
| -------- | -------------- | --------- | ----------------- |
| Auth     | boyakie06b1145 | Create    | awscloudformation |
? Are you sure you want to continue? Yes
⠏ Updating resources in the cloud. This may take a few minutes...


(中略)


✔ All resources are updated in the cloud
```

### 認証機能のフロントエンドへの実装
AmplifyはReactで利用可能な便利なコンポーネントをまとめたライブラリ、aws-amplify-reactが提供されています。
このアプリケーションではwithAuthenticatorコンポーネントを利用して、認証されたユーザーのみがアプリケーションを操作できるよう実装します。

* aws-amplify-reactとAmplify Frameworkをアプリケーションに追加する
    * `$ npm install --save aws-amplify@2.2.7 aws-amplify-react@3.1.8
` 

(JavaScriptの場合)

* `./src/App.js` ファイルを以下の内容に置き換える
    * `withAuthenticator`を使用するとデフォルトで電話番号の入力が求められるが、signUpConfigを指定することで電話番号の入力をSkipできる

```javascript:./src/App.js
import React from 'react';
import Amplify from '@aws-amplify/core';
import awsmobile from './aws-exports';
import { withAuthenticator } from 'aws-amplify-react';

Amplify.configure(awsmobile);

function App() {
  return (
    <h1>
      Hello World!
    </h1>
  );
}

export default withAuthenticator(App, {
  signUpConfig: {
    hiddenDefaults: ['phone_number']
  }
});
```

(TypeScriptの場合)

* `./src/App.tsx` ファイルを以下の内容に置き換える
* withAuthenticatorのところで以下のようにエラーが出るので、適当に true や null を渡す、または@ts-ignore でエラーを無視してください
    * ここでは後者を選択

```:エラー
/home/ec2-user/environment/amplify-sns-workshop/boyaki/src/App.tsx
TypeScript error in /home/ec2-user/environment/amplify-sns-workshop/boyaki/src/App.tsx(16,39):
Argument of type '{ signUpConfig: { hiddenDefaults: string[]; }; }'
is not assignable to parameter of type 'boolean | undefined'.
  Type '{ signUpConfig: { hiddenDefaults: string[]; }; }' 
is not assignable to type 'true'. 
```

```:withAuthenticatorの詳細
withAuthenticator(
  Comp: any, 
  includeGreetings?: boolean,
  authenticatorComponents?: any[],
  federated?: any,
  theme?: any,
  signUpConfig?: {}
):
```

```typescript:./src/App.tsx
import React from 'react';
import Amplify from '@aws-amplify/core';
import awsmobile from './aws-exports';
import { withAuthenticator } from 'aws-amplify-react';

Amplify.configure(awsmobile);

function App() {
  return (
    <h1>
      Hello World!
    </h1>
  );
};

//@ts-ignore
export default withAuthenticator(App, {
 signUpConfig: {
   hiddenDefaults: ["phone_number"]
 }
});
```


* `$ npm start` を実行して、Cloud9のメニューの「Preview」 > 「Preview Running Application」を実行して動作確認(認証画面が出る)する
    * ![スクリーンショット 2020-08-25 14.48.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/b56e0c8d-b628-8551-c51b-4f195efa2523.png)

これで認証機能の実装は完了です。
「Create account」からアカウント登録してログインすると、「Hello World!」と画面に表示されます。
ログアウトのボタンがなく、ログアウトはPOST機能の追加のところで併せて実装するので、後段で認証機能の確認は行います。

### バックエンド側のPOST機能の実装
認証機能が実装完了したので、次にPostを管理するGraphQL APIを作成します。
`$ amplify add api`でGraphQLを選択すると、GraphQLのマネージドサービスであるAWS AppSyncがプロビジョニングされます。

* `$ amplify add api`でGraphQL APIの作成

```
ec2-user:~/environment/amplify-sns-workshop/boyaki (master) $ amplify add api
? Please select from one of the below mentioned services: GraphQL
? Provide API name: BoyakiGql
? Choose the default authorization type for the API Amazon Cognito User Pool
Use a Cognito user pool configured as a part of this project.
? Do you want to configure advanced settings for the GraphQL API No, I am done.
? Do you have an annotated GraphQL schema? No
? Choose a schema template: One-to-many relationship (e.g., “Blogs” with “Posts” and “Comments”)

The following types do not have '@auth' enabled. Consider using @auth with @model
         - Blog
         - Post
         - Comment
Learn more about @auth here: https://docs.amplify.aws/cli/graphql-transformer/directives#auth


GraphQL schema compiled successfully.

Edit your schema at /home/ec2-user/environment/amplify-sns-workshop/boyaki/amplify/backend/api/BoyakiGql/schema.graphql or place .graphql files in a directory at /home/ec2-user/environment/amplify-sns-workshop/boyaki/amplify/backend/api/BoyakiGql/schema
? Do you want to edit the schema now? No
Successfully added resource BoyakiGql 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
```

* Post(投稿)を管理するAPIを作成する
    * `./amplify/backend/api/BoyakiGql/schema.graphql`を以下の内容に置き換える

```graphql:./amplify/backend/api/BoyakiGql/schema.graphql
type Post
  @model (subscriptions: { level: public })
  @auth(rules: [
    {allow: owner, ownerField:"owner", provider: userPools, operations:[read, create]}
    {allow: private, provider: userPools, operations:[read]}
	])
  @key(name: "SortByTimestamp", fields:["type", "timestamp"], queryField: "listPostsSortedByTimestamp")
  @key(name: "BySpecificOwner", fields:["owner", "timestamp"], queryField: "listPostsBySpecificOwner")
{
  type: String! # always set to 'post'. used in the SortByTimestamp GSI
  id: ID
  content: String!
  owner: String
  timestamp: AWSTimestamp!
}
```

### バックエンド側のPOST機能の実装

* フロントの構築に必要なライブラリをインストールする

```
$ npm install --save @material-ui/core @material-ui/icons moment react-router react-router-dom
```

* TypeScriptの場合は以下もインストールしてください
    * インストールしていない場合、以下のようなエラーが発生

```
$ npm install -D @types/react @types/react-dom @types/react-router-dom 
```

```インストールがされていないときに発生するエラー
/home/ec2-user/environment/amplify-sns-workshop/boyaki/src/App.tsx
TypeScript error in /home/ec2-user/environment/amplify-sns-workshop/boyaki/src/App.tsx(9,8):
Could not find a declaration file for module 'react-router-dom'. '/home/ec2-user/environment/amplify-sns-workshop/boyaki/node_modules/react-router-dom/index.js' implicitly has an 'any' type.
  Try `npm install @types/react-router-dom` if it exists or add a new declaration (.d.ts) file containing `declare module 'react-router-dom';
```

以下のようにビューを形成するファイルを作成します。
JavaScript、TypeScriptの両方のコードを載せていますので使用する言語に合わせてお使いください。
TypeScriptに対して、JavaScriptのコードをそのまま用いると、以下の'status' は型 'ThemeOptions' に存在しません。と怒られるので注意してください。

```
/home/ec2-user/environment/amplify-sns-workshop-ts/boyaki-ts/src/App.tsx
TypeScript error in /home/ec2-user/environment/amplify-sns-workshop-ts/boyaki-ts/src/App.tsx(48,3):
Argument of type '{ palette: { type: "dark"; primary: { main: string; contrastText: string; }; background: { default: string; paper: string; }; divider: string; }; overrides: { MuiButton: { color: string; }; }; typography: { ...; }; status: { ...; }; }'
is not assignable to parameter of type 'ThemeOptions'.
```

* 左側のメニュー一覧やPostやLOGOUTボタンを表示する部分
    * Sidebar.js(Sidebar.tsx)
        * Sidebarの役割は以下の３つ
            * Logoutボタンの表示
            * Global TimelineやProfileへの切り替えなど、メニュー機能
            * Postの投稿
* 右側のPost一覧を表示する部分
    * メニューで選択した対象に応じていずれかを表示
        * Global Timeline: すべてのユーザーのPostを表示
            * AllPost.js(AllPost.tsx)
        * Profile: 特定のユーザーのPostを表示
            * PostsBySpecifiedUser.js(PostsBySpecifiedUser.tsx)
* どちらのPost一覧を表示するかのルーティング
    * App.js(App.tsx)

#### App.js(App.tsx)
(JavaScriptの場合)

* `./src/App.js`を[App.js](https://amplify-sns.workshop.aws/30_mock/30_post_front_end/_index.ja.files/App.js)の内容に書き換える

(TypeScriptの場合)

```typescript:./src/App.tsx
import React from 'react';
import './App.css';
import { withAuthenticator } from 'aws-amplify-react';
import {
  HashRouter,
  Switch,
  Route,
  Redirect,
} from 'react-router-dom';
import { makeStyles, createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline';
import AllPosts from './containers/AllPosts';
import PostsBySpecifiedUser from './containers/PostsBySpecifiedUser';
import Amplify from '@aws-amplify/core';
import PubSub from '@aws-amplify/pubsub'
import awsmobile from './aws-exports';

Amplify.configure(awsmobile);
PubSub.configure(awsmobile)

const drawerWidth = 240;
const theme = createMuiTheme({
  palette: {
    type: 'dark',
    primary: {
      main: '#1EA1F2',
      contrastText: "#fff",
    },
    background: {
      default: '#15202B',
      paper: '#15202B',
    },
    divider: '#37444C',
  },
  overrides: {
    MuiButton: {
      color: 'white',
    },
  },
  typography: {
    fontFamily: [
      'Arial', 
    ].join(','),
  },
  status: {
    danger: 'orange',
  },
} as any);
const useStyles = makeStyles(theme => ({
  root: {
    display: 'flex',
    height: '100%',
    width: 800,
    marginLeft: 'auto',
    marginRight: 'auto',
  },
  appBar: {
    marginLeft: drawerWidth,
  },
  drawer: {
    width: drawerWidth,
    flexShrink: 0,
  },
  drawerPaper: {
    width: drawerWidth,
  },
  toolbar: theme.mixins.toolbar,
  content: {
    flexGrow: 1,
    backgroundColor: theme.palette.background.default,
    padding: theme.spacing(3),
  },
}));

function App() {
  const classes = useStyles();
  return (
    <div className={classes.root} >
      <ThemeProvider theme={theme}>
        <CssBaseline />
        <HashRouter>
          <Switch>
            <Route exact path='/' component={AllPosts} />
            <Route exact path='/global-timeline' component={AllPosts} />
            <Route exact path='/:userId' component={PostsBySpecifiedUser}/>
            <Redirect path="*" to="/" />
          </Switch>
        </HashRouter>
      </ThemeProvider>
    </div>
  );
}

//@ts-ignore
export default withAuthenticator(App, {
  signUpConfig: {
    hiddenDefaults: ['phone_number']
  }
});
```

#### Sidebar.js(Sidebar.tsx)

(JavaScriptの場合)

* ディレクトリ/ファイルを作成して、`./src/containers/Sidebar.js`を[Sidebar.js](https://amplify-sns.workshop.aws/30_mock/30_post_front_end/_index.ja.files/Sidebar.js)の内容に書き換える

```
$ mkdir src/containers
$ touch src/containers/Sidebar.js
```

(TypeScriptの場合)

```
$ mkdir src/containers
$ touch src/containers/Sidebar.tsx
```

```typescript:./src/containers/Sidebar.tsx
import React from 'react';

import { makeStyles } from '@material-ui/core/styles';
import {
  Button,
  Drawer,
  List,
  ListItem,
  ListItemText,
  TextField,
  ListItemIcon,
} from '@material-ui/core';
import {
  Person as PersonIcon,
  Public as PublicIcon,
} from '@material-ui/icons';

import API, { graphqlOperation } from '@aws-amplify/api';
import Auth from '@aws-amplify/auth';

import { createPost } from '../graphql/mutations';
import { useHistory } from 'react-router';

const drawerWidth = 340;
const MAX_POST_CONTENT_LENGTH = 140;

const useStyles = makeStyles(theme => ({
  drawer: {
    width: drawerWidth,
    flexShrink: 0,
    position: 'relative',
  },
  drawerPaper: {
    width: drawerWidth,
    position: 'relative',
  },
  toolbar: theme.mixins.toolbar,
  textField: {
    width: drawerWidth,
  },
  list: {
    // overflowWrap: 'break-word',
    width: 300,
  },
}));

export default function Sidebar({activeListItem}:any) {
  const classes = useStyles();
  const history = useHistory();

  const [value, setValue] = React.useState('');
  const [isError, setIsError] = React.useState(false);
  const [helperText, setHelperText] = React.useState('');

  const handleChange = (event:any):any => {
    setValue(event.target.value);
    if (event.target.value.length > MAX_POST_CONTENT_LENGTH) {
      setIsError(true);
      setHelperText(String(MAX_POST_CONTENT_LENGTH - event.target.value.length));
    } else {
      setIsError(false);
      setHelperText('');
    }
  };

  const onPost = async () => {
    const res = await API.graphql(graphqlOperation(createPost, { input: {
      type: 'post',
      content: value,
      timestamp: Math.floor(Date.now() / 1000),
    }})); 

    console.log(res)
    setValue('');
  }

  const signOut = () => {
    Auth.signOut()
      .then(data => console.log(data))
      .catch(err => console.log(err));
  }

  return (
    <Drawer
      className={classes.drawer}
      variant="permanent"
      classes={{
        paper: classes.drawerPaper,
      }}
      anchor="left"
    >
      <div className={classes.toolbar} />
      <List>
        <ListItem
          button
          selected={activeListItem === 'global-timeline'}
          onClick={() => {
            Auth.currentAuthenticatedUser().then((user) => {
              history.push('/global-timeline');
            })
          }}
          key='global-timeline'
        >
          <ListItemIcon>
            <PublicIcon />
          </ListItemIcon>
          <ListItemText primary="Global Timeline" />
        </ListItem>
        <ListItem
          button
          selected={activeListItem === 'profile'}
          onClick={() => {
            Auth.currentAuthenticatedUser().then((user) => {
              history.push('/' + user.username);
            })
          }}
          key='profile'
        >
          <ListItemIcon>
            <PersonIcon />
          </ListItemIcon>
          <ListItemText primary="Profile" />
        </ListItem>
        <ListItem key='post-input-field'>
          <ListItemText primary={
            <TextField
              error={isError}
              helperText={helperText}
              id="post-input"
              label="Type your post!"
              multiline
              rowsMax="8"
              variant="filled"
              value={value}
              onChange={handleChange}
              fullWidth
              margin="normal"
            />
          } />
        </ListItem>
        <ListItem key='post-button'>
          <ListItemText primary={
            <Button
              variant="contained"
              color="primary"
              disabled={isError}
              onClick={onPost}
              fullWidth
            >
              Post
            </Button>
          } />
        </ListItem>
        <ListItem key='logout'>
          <ListItemText primary={
            <Button
              variant="outlined"
              onClick={signOut}
              fullWidth
            >
              Logout
            </Button>
          } />
        </ListItem>
      </List>
    </Drawer>
  )
}
```

#### AllPost.js(AllPost.tsx)

(JavaScriptの場合)

* ファイルを作成して、`./src/containers/AllPosts.js`を[AllPosts.js](https://amplify-sns.workshop.aws/30_mock/30_post_front_end/_index.ja.files/AllPosts.js)の内容に書き換える

```
$ touch src/containers/AllPosts.js
```

(TypeScriptの場合)

```
$ touch src/containers/AllPosts.tsx
```

```typescript:./src/containers/AllPosts.tsx
import React, { useState, useEffect, useReducer } from 'react';
import API, { graphqlOperation } from '@aws-amplify/api';
import { listPostsSortedByTimestamp } from '../graphql/queries';
import { onCreatePost } from '../graphql/subscriptions';
import PostList from '../components/PostList';
import Sidebar from './Sidebar';

const SUBSCRIPTION = 'SUBSCRIPTION';
const INITIAL_QUERY = 'INITIAL_QUERY';
const ADDITIONAL_QUERY = 'ADDITIONAL_QUERY';
const reducer = (state:any, action:any) => {
  switch (action.type) {
    case INITIAL_QUERY:
      return action.posts;
    case ADDITIONAL_QUERY:
      return [...state, ...action.posts]
    case SUBSCRIPTION:
      return [action.post, ...state]
    default:
      return state;
  }
};

export default function AllPosts() {
  const [posts, dispatch] = useReducer(reducer, []);
  const [nextToken, setNextToken] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  const getPosts = async (type:any, nextToken = null) => {
    const res = await API.graphql(graphqlOperation(listPostsSortedByTimestamp, {
      type: "post",
      sortDirection: 'DESC',
      limit: 20, //default = 10
      nextToken: nextToken,
    }));
    console.log(res);
    dispatch({ type: type, posts: res.data.listPostsSortedByTimestamp.items })
    setNextToken(res.data.listPostsSortedByTimestamp.nextToken);
    setIsLoading(false);
  }

  const getAdditionalPosts = () => {
    if (nextToken === null) return; //Reached the last page
    getPosts(ADDITIONAL_QUERY, nextToken);
  }

  useEffect(() => {
    getPosts(INITIAL_QUERY);

    const subscription = API.graphql(graphqlOperation(onCreatePost)).subscribe({
      next: (msg:any) => {
        console.log('allposts subscription fired')
        const post = msg.value.data.onCreatePost;
        dispatch({ type: SUBSCRIPTION, post: post });
      }
    });
    return () => subscription.unsubscribe();
  }, []);


  return (
    <React.Fragment>
      <Sidebar 
        activeListItem='global-timeline'
      />
      <PostList
        isLoading={isLoading}
        posts={posts}
        getAdditionalPosts={getAdditionalPosts}
        listHeaderTitle={'Global Timeline'}
      />
    </React.Fragment>
  )
}
```

#### PostsBySpecifiedUser.js(PostsBySpecifiedUser.tsx)

(JavaScriptの場合)

* ファイルを作成して、`./src/containers/PostsBySpecifiedUser.js`を[PostsBySpecifiedUser.js](https://amplify-sns.workshop.aws/30_mock/30_post_front_end/_index.ja.files/PostsBySpecifiedUser.js)の内容に書き換える

```
$ touch src/containers/PostsBySpecifiedUser.js
```

(TypeScriptの場合)

```
$ touch src/containers/PostsBySpecifiedUser.tsx
```

```typescript:./src/containers/PostsBySpecifiedUser.tsx
import React, { useState, useEffect, useReducer } from 'react';
import API, { graphqlOperation } from '@aws-amplify/api';
import { useParams } from 'react-router';
import { listPostsBySpecificOwner } from '../graphql/queries';
import { onCreatePost } from '../graphql/subscriptions';
import PostList from '../components/PostList';
import Sidebar from './Sidebar';

const SUBSCRIPTION = 'SUBSCRIPTION';
const INITIAL_QUERY = 'INITIAL_QUERY';
const ADDITIONAL_QUERY = 'ADDITIONAL_QUERY';
const reducer = (state:any, action:any) => {
  switch (action.type) {
    case INITIAL_QUERY:
      return action.posts;
    case ADDITIONAL_QUERY:
      return [...state, ...action.posts]
    case SUBSCRIPTION:
      return [action.post, ...state]
    default:
      return state;
  }
};

export default function PostsBySpecifiedUser() {
  const { userId } = useParams();

  const [posts, dispatch] = useReducer(reducer, []);
  const [nextToken, setNextToken] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  const getPosts = async (type:any, nextToken = null) => {
    const res = await API.graphql(graphqlOperation(listPostsBySpecificOwner, {
      owner: userId,
      sortDirection: 'DESC',
      limit: 20,
      nextToken: nextToken,
    }));
    console.log(res);
    dispatch({ type: type, posts: res.data.listPostsBySpecificOwner.items })
    setNextToken(res.data.listPostsBySpecificOwner.nextToken);
    setIsLoading(false);
  }

  const getAdditionalPosts = () => {
    if (nextToken === null) return; //Reached the last page
    getPosts(ADDITIONAL_QUERY, nextToken);
  }


  useEffect(() => {
    getPosts(INITIAL_QUERY);

    const subscription = API.graphql(graphqlOperation(onCreatePost)).subscribe({
      next: (msg:any) => {
        const post = msg.value.data.onCreatePost;
        if (post.owner !== userId) return;
        dispatch({ type: SUBSCRIPTION, post: post });
      }
    });
    return () => subscription.unsubscribe();
  }, []);


  return (
    <React.Fragment>
      <Sidebar 
        activeListItem='profile'
      />
      <PostList
        isLoading={isLoading}
        posts={posts}
        getAdditionalPosts={getAdditionalPosts}
        listHeaderTitle={userId}
      />
    </React.Fragment>
  )
}
```

#### PostList.js(PostList.tsx)

(JavaScriptの場合)

AllPosts.jsやPostsBySpecifiedUser.jsから渡されたPost一覧を表示するためのUIを作成していきます。

* ディレクトリ/ファイルを作成して、`./src/components/PostList.js`を[PostList.js](https://amplify-sns.workshop.aws/30_mock/30_post_front_end/_index.ja.files/PostList.js)の内容に書き換える

```
$ mkdir src/components
$ touch src/components/PostList.js
```


(TypeScriptの場合)

```typescript:./src/components/PostList.tsx
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import {
  Button,
  List,
  ListItem,
  Divider,
  ListItemText,
  ListItemAvatar,
  Avatar,
  Typography,
  CircularProgress,
} from '@material-ui/core';
import { useHistory } from 'react-router';
import moment from 'moment';

const useStyles = makeStyles(theme => ({
  listRoot: {
    width: '100%',
    wordBreak: 'break-all',
    overflow: 'scroll',
    borderRight: '1px solid #37444C',
  },
  alignCenter: {
    textAlign: 'center',
  },
  loader: {
    textAlign: 'center',
    paddingTop: 20,
  },
  maxWidth: {
    width: '100%',
  },
  listHeader: {
    position: 'sticky',
    top: 0,
    zIndex: 1200,
    backgroundColor: '#15202B',
    borderBottom: '1px solid #37444C',
  },
  clickable: {
    cursor: 'pointer',
  }
}));

export default function PostList({ isLoading, posts, getAdditionalPosts, listHeaderTitle, listHeaderTitleButton }:any) {
  const classes = useStyles();
  return (
    <div className={classes.listRoot}>
      {isLoading ?
        <div className={classes.loader}>
          <CircularProgress size={25} />
        </div>
        :
        <List disablePadding>
          <ListItem
            alignItems="flex-start"
            className={classes.listHeader}
          >
            <Typography
              variant='h5'
            >
              {listHeaderTitle}
              {listHeaderTitleButton && listHeaderTitleButton}
            </Typography>
          </ListItem>
          {posts.map((post:any):any => (
            <span>
              <PostItem post={post} />
              <Divider component="li" />
            </span>
          ))}
          <ListItem
            alignItems="flex-start"
            className={classes.alignCenter}
            key='loadmore'
          >
            <ListItemText
              primary={
                <Button variant='outlined' onClick={() => getAdditionalPosts()} className={classes.maxWidth}> Read More </Button>
              }
            />
          </ListItem>
        </List>
      }
    </div>
  )
}

function PostItem({ post }:any) {
  const classes = useStyles();
  const history = useHistory();
  const now = moment();
  console.log(now)

  const calcTimestampDiff = (timestamp:any) => {
    const scales:any = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds'];

    for (let i=0; i < scales.length; i++){
      const scale = scales[i];
      const diff = moment(now).diff(timestamp * 1000, scale);
      if( diff > 0) return diff + scale.charAt(0)
    }

    return 0 + scales[scales.length - 1].charAt(0)
  }

  return (
    <ListItem alignItems='flex-start' key={post.id}>
      <ListItemAvatar>
        <div className={classes.clickable} onClick={() => history.push('/' + post.owner)}>
          <Avatar alt={post.owner} src='/' />
        </div>
      </ListItemAvatar>
      <ListItemText
        primary={
          <React.Fragment>
            {post.owner}
            <Typography
              color='textSecondary'
              display='inline'
            >
              {' ' + String.fromCharCode(183) + ' ' + calcTimestampDiff(post.timestamp)}
            </Typography>
          </React.Fragment>
        }
        secondary={
          <Typography
            color='textPrimary'
          >
            {post.content}
          </Typography>
        }
      />
    </ListItem>
  )
}
```

#### amplify pushの実行
`$ amplify push`を実行すると、このアプリケーションで利用しているcreatePostなどのGraphQL Operationを行うためのコードが./src/graphql配下に書き出さしされます。
TypeScriptを使用する場合は、以下の `Choose the code generation language target` で`typescript`を選択してください。

```
ec2-user:~/environment/amplify-sns-workshop/boyaki (master) $ amplify push
✔ Successfully pulled backend environment production from the cloud.

Current Environment: production

| Category | Resource name  | Operation | Provider plugin   |
| -------- | -------------- | --------- | ----------------- |
| Api      | BoyakiGql      | Create    | awscloudformation |
| Auth     | boyakie06b1145 | No Change | awscloudformation |
? Are you sure you want to continue? Yes

GraphQL schema compiled successfully.

Edit your schema at /home/ec2-user/environment/amplify-sns-workshop/boyaki/amplify/backend/api/BoyakiGql/schema.graphql or place .graphql files in a directory at /home/ec2-user/environment/amplify-sns-workshop/boyaki/amplify/backend/api/BoyakiGql/schema
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 3
⠹ Updating resources in the cloud. This may take a few minutes...


(中略)


✔ Generated GraphQL operations successfully and saved at src/graphql
✔ All resources are updated in the cloud

GraphQL endpoint: https://xxxxxxxxxxxxxxxxxxxxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql
```

## 確認
### アカウント登録
`$ npm start`をして作成した機能が作れているか確認します。
Create accountリンクから、アカウントを作成します。
以下を入力して、「CREATE ACCOUNT」ボタンをクリックして入力したEmailアドレス宛にコードが届くので入力します。

 * Username : test_user1
 * Password : (任意)
 * Email
    * 受信できるEmailアドレスを入力

test_user1, test_user2の2つのアカウントを作成しておきます。

![スクリーンショット 2020-08-25 17.10.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/bf7f5733-a419-18b3-ee60-0a1e63100cd1.png)
![スクリーンショット 2020-08-25 17.10.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/24fac23f-2b7f-6ab8-723b-c8561463d238.png)

### サインイン
アカウント登録時に入力したUsernameとPasswordでサインインできることを確認します。
![スクリーンショット 2020-08-25 17.14.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/1e15cbf1-cd68-2c2d-eab4-bb1c8acf9c0a.png)

### サインアウト
「LOGOUT」ボタンをクリックすると、サインイン画面が表示されることを確認します。

[「LOGOUT」ボタンをクリック後の画面]
![スクリーンショット 2020-08-25 17.35.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/96636260-00e0-f875-7f72-7b08c2592504.png)


### 投稿
投稿できること、投稿が一覧に表示されることを確認します。

[Global Timeline]
全てのアカウント(test_user1とtest_user2)の投稿が表示されるビュー
![スクリーンショット 2020-08-25 17.17.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/0109c711-5aeb-938b-7189-47be0e384bfb.png)

[Profile]
サインしているアカウント(test_user1)の投稿のみ表示されるビュー
![スクリーンショット 2020-08-25 17.17.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/cb86f146-6d79-39b6-82d7-5bb12b1e2fce.png)



# アプリケーションのデプロイ

## 手動デプロイ
`$ amplify add hosting` を実行して、「Manual deployment」を選択します。
そのあと、`$ amplify publish`を実行して公開します。

```
ec2-user:~/environment/amplify-sns-workshop/boyaki (master) $ 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
```

```
ec2-user:~/environment/amplify-sns-workshop/boyaki (master) $ amplify publish
✔ Successfully pulled backend environment production from the cloud.

Current Environment: production

| Category | Resource name  | Operation | Provider plugin   |
| -------- | -------------- | --------- | ----------------- |
| Hosting  | amplifyhosting | Create    | awscloudformation |
| Auth     | boyakie06b1145 | No Change | awscloudformation |
| Api      | BoyakiGql      | No Change | awscloudformation |
? Are you sure you want to continue? Yes
⠋ Updating resources in the cloud. This may take a few minutes...


(中略)


The project was built assuming it is hosted at /.
You can control this with the homepage field in your package.json.

The build folder is ready to be deployed.
You may serve it with a static server:

  npm install -g serve
  serve -s build

Find out more about deployment here:

  bit.ly/CRA-deploy

✔ Zipping artifacts completed.
✔ Deployment complete!
https://production.xxxxxxxxxxxxxx.amplifyapp.com
```

「Deployment complete!」の下に表示されるURLに今度はお好きなブラウザでアクセスしてみましょう。
先程、Cloud9のPreviewで見ていたアプリケーションがブラウザからアクセスできるようになりました。
AWS Amplify Consoleからもデプロイが成功していることが確認できます。
![スクリーンショット 2020-08-25 20.09.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/fe77a7be-a9b3-3cf1-4f8c-4e48688acdea.png)


デプロイはこれで完了です。

## GitHubと連携してCI/CD環境を構築する
手動でデプロイをしましたが、Githubと連携すれば毎回`$ amplify publish`をしなくてもGithubへのコミットをトリガーに自動でデプロイさせることができます。

### Githubでソースコード管理するための設定
Githubにコードをpushするために、公開鍵・秘密鍵のペアを作成しておきます。
全てデフォルトで Enterを押して進めてください。

```
ec2-user:~/environment $ cd ~/.ssh
ec2-user:~/.ssh $ ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/home/ec2-user/.ssh/id_rsa): 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/ec2-user/.ssh/id_rsa.
Your public key has been saved in /home/ec2-user/.ssh/id_rsa.pub.
The key fingerprint is:
(省略)
The key's randomart image is:
+---[RSA 2048]----+
|       (省略)     |
+----[SHA256]-----+
```

`$ cat ~/.ssh/id_rsa.pub` で出力されるssh-rsa ... から始まる文字列をコピーします。
[https://github.com/settings/ssh/new](https://github.com/settings/ssh/new)にアクセスして、「Title」と「key」を入力して「Add SSH key」ボタンをクリックします。

*  Title : amplify-workshop
*  Key : コピーしたssh-rsa ... から始まる文字列

![スクリーンショット 2020-08-25 21.50.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/b56edb45-9a16-c189-49c0-d060b0da1634.png)


### Githubのリポジトリを作成
GitHubのアカウントにログインし、画面右上の「＋」ボタンから「New repository」を選択します。
以下のように入力して、「Create repository」ボタンをクリックして新規リポジトリを作成します。

* Repository name : amplify-workshop
* Description : amplify-workshop
* Repository type : private
* ![スクリーンショット 2020-08-25 21.11.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/3868ff6f-983f-4b9f-0cc7-fd7238382811.png)

### 作成したリポジトリにアプリコードを追加
リポジトリが作成できたら、gitのURLをコピーします。
![スクリーンショット 2020-08-25 21.17.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/ef26eb67-b84f-d5e7-ac9f-fda322db88e4.png)

以下を実行します。

```
$ rm -rf .git
$ git init
$ git add .
$ git commit -m "first commit"
$ git remote add origin <コピーしたgitのURL>
$ git push -u origin master
```

GitHub のページをリロードして、ソースコードが更新されていれば push は成功です。
![スクリーンショット 2020-08-25 22.02.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/3ed715cf-68aa-c306-9256-632d7e771e41.png)

### Amplify ConsoleとGitHubアカウントを紐付け
先程、`$ amplify publish`でアプリケーションをデプロイしました。
Amplify ConsoleとGitHubを連携することで、ソースコードのPushをしたタイミングで自動的にアプリケーションがデプロイされる仕組みを構築できます。
手動デプロイを設定しているため、`$ amplify remove hosting`で設定を削除します。

```
ec2-user:~/environment/amplify-sns-workshop/boyaki (master) $ amplify remove hosting

? Are you sure you want to delete the resource? This action deletes all files related to this resource from the backend directory. Yes
Successfully removed resource
```

`$ amplify push`で反映させておきます。

```
ec2-user:~/environment/amplify-sns-workshop/boyaki (master) $ amplify push
✔ Successfully pulled backend environment production from the cloud.

Current Environment: production

| Category | Resource name  | Operation | Provider plugin   |
| -------- | -------------- | --------- | ----------------- |
| Hosting  | amplifyhosting | Delete    | awscloudformation |
| Auth     | boyakie06b1145 | No Change | awscloudformation |
| Api      | BoyakiGql      | No Change | awscloudformation |
? Are you sure you want to continue? Yes
⠋ Updating resources in the cloud. This may take a few minutes...

(中略)

✔ All resources are updated in the cloud
```

次に、Amplify ConsoleとGitHubアカウントを紐付けする設定をしていきます。
`$ amplify add hosting`を実行します。

```
ec2-user:~/environment/amplify-sns-workshop/boyaki (master) $ amplify add hosting
? Select the plugin module to execute Hosting with Amplify Console (Managed hosting with custom domains, Continuous deployment)
? Choose a type Continuous deployment (Git-based deployments)
? Continuous deployment is configured in the Amplify Console. Please hit enter once you connect your repository 
```

`Continuous deployment is configured in the Amplify Console.`と表示されたらAmplify Consoleを開いて以下のように設定していきます。

#### Connect a frontend web app

![スクリーンショット 2020-08-25 22.20.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/2c7b878d-8171-7f45-3630-7fd99a8ed4ef.png)

#### リポジトリブランチの追加

![スクリーンショット 2020-08-25 22.22.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/d88f54db-2639-1e82-0099-3685af48fd65.png)

#### ビルド設定の構成

![スクリーンショット 2020-08-25 22.28.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/95b362f8-6c0f-9b38-ac67-85e34782d789.png)

「Create new role」ボタンをクリックして新しいロールを作成します。
ロールの設定項目は全てデフォルトを選択します。
![スクリーンショット 2020-08-25 22.29.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/c384e5d8-2e27-d9b8-e7ee-e79f7e3aa3af.png)
![スクリーンショット 2020-08-25 22.31.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/1a065e82-dd86-2de2-4716-358b04f1f231.png)
![スクリーンショット 2020-08-25 22.31.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/134d0e14-789c-039e-36ec-d887fbfb6d74.png)
![スクリーンショット 2020-08-25 22.31.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/8e15778a-1fc5-cc99-6bc2-07ab3437d1fc.png)

作成したロールを指定して、「次へ」をクリックします。
![スクリーンショット 2020-08-25 22.33.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/4af5b1d4-e81f-8402-c4b0-88b883f75931.png)
![スクリーンショット 2020-08-25 22.34.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/cc613b32-a57d-cc8f-36ca-9ff9135e3385.png)

#### 確認
表示されている項目に誤りがなければ、「保存してデプロイ」をクリックします。
![スクリーンショット 2020-08-25 22.39.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/6a159d4f-b243-f489-3e7b-936098290b11.png)
プロビジョン > ビルド > デプロイ > 検証と処理が進みます。
![スクリーンショット 2020-08-25 22.41.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/c0298aa1-4bf2-90e6-f55f-0ad88fd8afbb.png)
数分で検証まで終了します。All Greenです。
![スクリーンショット 2020-08-25 22.50.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/111d1a4f-7374-ead9-74dd-dc8e3ee65f79.png)


以上で、Github連携が完了です。
以下のように、ボタンのテキストを「Logout」 => 「Sign out」に変える変更をリポジトリに加えてみました。
自動でビルド、デプロイと走っていることがわかります。

```
ec2-user:~/environment/amplify-sns-workshop/boyaki (master) $ git diff
diff --git a/src/containers/Sidebar.js b/src/containers/Sidebar.js
index 8cac9c8..4e60e07 100644
--- a/src/containers/Sidebar.js
+++ b/src/containers/Sidebar.js
@@ -157,7 +157,7 @@ export default function Sidebar({activeListItem}) {
               onClick={signOut}
               fullWidth
             >
-              Logout
+              Sign out
             </Button>
           } />
         </ListItem>
ec2-user:~/environment/amplify-sns-workshop/boyaki (master) $ git add src/containers/Sidebar.js 
ec2-user:~/environment/amplify-sns-workshop/boyaki (master) $ git commit -m "Change text for logout."
[master c8c5e85] Change text for logout.
 Committer: EC2 Default User <ec2-user@ip-172-31-33-189.ap-northeast-1.compute.internal>
Your name and email address were configured automatically based
on your username and hostname. Please check that they are accurate.
You can suppress this message by setting them explicitly:

    git config --global user.name "Your Name"
    git config --global user.email you@example.com

After doing this, you may fix the identity used for this commit with:

    git commit --amend --reset-author

 1 file changed, 1 insertion(+), 1 deletion(-)
ec2-user:~/environment/amplify-sns-workshop/boyaki (master) $ git push origin master
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 454 bytes | 454.00 KiB/s, done.
Total 5 (delta 4), reused 0 (delta 0)
remote: Resolving deltas: 100% (4/4), completed with 4 local objects.
To github.com:RustyNail/amplify-workshop.git
   0b8142b..c8c5e85  master -> master
```

![スクリーンショット 2020-08-25 22.57.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/b0a6cf4f-4503-661d-9b76-8173a92b5d52.png)

表示も「SIGN OUT」に変わっています。
![スクリーンショット 2020-08-25 23.40.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/192930ff-1c09-f62d-2bcd-53076ca6d157.png)


## Cypressと連携してE2Eテスト
AWS AmplifyはE2Eテスティングフレームワークである[Cypress](https://www.cypress.io/)をサポートしています。
Amplify Consoleと統合することで簡単に E2E テストを CI/CD 環境に組み込むことができます。

### Cypress のインストール

```
$ npm install cypress mochawesome@4.1.0 mocha start-server-and-test --save-dev
```

### テストコードの記述
テストを記述するフォルダとファイルを作成します。

```
$ mkdir -p cypress/integration
$ touch cypress/integration/authenticator_spec.js
$ touch cypress.json
```

今回のテストでは、以下のフローが正しく機能するかのテストを記述していきます。

1. トップページにアクセスすると、ログイン画面を表示させる
2. テスト用のユーザ ID/パスワードでログインできる
3. ログイン後の画面に「Global」というテキストが表示されていることを確認する

#### cypress/integration/authenticator_spec.js

```js:cypress/integration/authenticator_spec.js
describe('Authenticator:', function() {
  beforeEach(function() {
    cy.visit('/');
  });

  describe('Sign In:', () => {
    it('allows a user to signin', () => {
      cy.get(selectors.usernameInput).type(
        'テスト用に作成したアカウント名'
      );
      cy.get(selectors.signInPasswordInput).type(
        'テスト用に作成したアカウントのパスワード'
      );
      cy.get(selectors.signInSignInButton)
        .contains('Sign In')
        .click();
      cy.get(selectors.root).contains('Global');
    });
  });
});
export const selectors = {
  usernameInput: '[data-test="username-input"]',
  signInPasswordInput: '[data-test="sign-in-password-input"]',
  signInSignInButton: '[data-test="sign-in-sign-in-button"]',
  root: '#root'
};
```

#### cypress.json
Cypressがテストを実行するためのサーバを指定します。

```json:cypress.json
{
  "baseUrl": "http://localhost:3000"
}
```

### Amplify ConsoleにE2Eテストの設定を追加
次に、Amplify ConsoleにE2Eテストを実行するように設定を変更します。 
Amplify Console の「ビルドの設定」> 「ビルド設定の追加」から「編集」ボタンをクリックします。
![スクリーンショット 2020-08-25 23.57.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/7a3b4645-d0db-a188-1b1e-9867329ba266.png)

以下のように編集してtestフェーズを追加します。

```:amplify.yml
version: 1
backend:
  phases:
    build:
      commands:
        - '# Execute Amplify CLI with the helper script'
        - amplifyPush --simple
frontend:
  phases:
    preBuild:
      commands:
        - npm ci
    build:
      commands:
        - npm run build
  artifacts:
    baseDirectory: build
    files:
      - '**/*'
  cache:
    paths:
      - node_modules/**/*
test:
  artifacts:
    baseDirectory: cypress
    configFilePath: "**/mochawesome.json"
    files:
      - "**/*.png"
      - "**/*.mp4"
  phases:
    preTest:
      commands:
        - yarn install
        - yarn add mocha mochawesome mochawesome-merge mochawesome-report-generator
    test:
      commands:
        - npx start-server-and-test 'yarn start' 3000 'npx cypress run --reporter mochawesome --reporter-options "reportDir=cypress/report/mochawesome-report,overwrite=false,html=false,json=true,timestamp=mmddyyyy_HHMMss"'
    postTest:
      commands:
        - npx mochawesome-merge cypress/report/mochawesome-report/mochawesome*.json > cypress/report/mochawesome.json
```

設定したビルド設定をダウンロードし、プロジェクトのトップディレクトリ配下に配置します。
![スクリーンショット 2020-08-26 0.03.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/dde7fdfa-518e-f1fd-880a-a94a7576ee05.png)

Githubにpushして、Amplify Consoleを確認してみましょう。

```
$ git add .
$ git commit -m "add cypress settings"
$ git push
```

「テスト」が追加されています！
![スクリーンショット 2020-08-26 1.28.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/e44b8894-e3ad-76da-7833-559f5020beb3.png)

「Download artifacts」をクリックすると、テストの動画も取得できます。
![スクリーンショット 2020-08-26 1.30.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/63e0d22b-c058-63e3-1140-ef6f11fa65f1.png)
![スクリーンショット 2020-08-26 1.31.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/5b7b66c2-2ca0-2412-f0ca-bdf2e7607fe0.png)

[authenticator_spec.js.mp4をgifに変換したもの]

![authenticator_spec.js.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/c825c15b-21e8-99d7-6b8e-1658e5470296.gif)

# 後始末
amplifyで作成したアプリケーションを削除する際は、`$amplify delete`を実行して削除をします。
これを実行するとCloudFormationで作成した成果物やAmplifyアプリなど全てが消去されます。