Amplifyを試してみた

# 背景・目的
Webアプリケーションの調査および検証をする機会があり、簡単に作成できる仕組みがAWSにあるか調べてみたところAmplifyがあったので試してみます。

# まとめ
Amplifyは、フロントエンド開発を構築するツールです。
利用することで、簡単にバックエンドと通信でき素早くデプロイできる機能を有しています。
また、SPAフレームワークや、SSGをサポートすることで、最新のWebアプリケーション開発にも対応されています。

# 概要
## Amplifyとは?
フロントエンジニアがAWSでフルスタックアプリケーションを素早く構築する専用ツールと機能です。
Amplifyでは、以下のサービスを提供しています。
- Amplify Hosting
- Amplify Studio

Amplify Hostingでは、CI/CDでフルスタックサーバレスウェブアプリをホストするためのgitベースのワークフローを提供しているとのことです。

## Amplify Hostingの機能
以下の機能を有します。
- SPAフレームワークのサポート。以下に例を記載します。
    - React
    - Angular
    - Vue.js
    - lonic
    - Ember
- 静的サイトジェネレータのサポート。以下に例を記載します。
    - Gatsby
    - Eleventy
    - Hugo
    - VuePress
    - Jekyll
- 新しいブランチで、フロントエンドとバックエンドの本番環境と、ステージング環境を管理する
- アプリケーションをカスタムドメインに接続する
- Next.jsを使用して作成されたSSR Webアプリケーションをデプロイし、ホストする。
- エンドツーエンドのテストで、アプリの品質を向上させる。
- Webアプリをパスワード保護し、新しい機能を一般公開せずに作業可能にする。
- リライトとリダイレクトにより、SEOランキングを維持し、クライアントアプリの要件に基づいてトラフィックをルーティングする。
- インスタントキャッシュの無効化により、コードがコミットされる都度アプリが即時に更新される。
- アトミックデプロイは、デプロイ全体が終了した後にのみWebアプリが更新されるようにすることで、メンテンナンスウィンドウを排除する。これによりファイルが正しくアップロードされないシナリオが排除される。
- 様々なモバイルデバイスでレンダリングされたアプリのスクショをとり、レイアウトの問題を特定する。

私が理解していないSPAと、静的サイトジェネレータについては、簡単に調べたので以降に記載します。

### SPAとは?
- Single Page Application（SPA）のこと。
Wikipediaの「[シングルページアプリケーション](https://ja.wikipedia.org/wiki/%E3%82%B7%E3%83%B3%E3%82%B0%E3%83%AB%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3)」の説明によると、以下の記載がありました。

> シングルページアプリケーション（英: single-page application、SPA）とは、単一のWebページのみから構成することで、デスクトップアプリケーションのようなユーザ体験を提供するWebアプリケーションまたはWebサイトである。必要なコード（HTML、JavaScript、CSS）は最初にまとめて読み込むか[1]、ユーザの操作などに応じて動的にサーバと通信し、必要なものだけ読み込みを行う。

単一のWebページから構成され、デスクトップアプリのようなUXを提供する。とのことです。

また、AWSのページにも記載がありました。
> Modern SPA web applications
This user guide is intended for customers who have a basic understanding of modern single-page web applications (SPA). Modern web applications are constructed as SPAs that package all application components into static files. Traditional client-server web architectures led to poor experiences; every button click or search required a round trip to the server, re-rendering the entire application. Modern web apps offer a native app-like user experience by serving the app frontend, or user interface, efficiently to browsers as prebuilt HTML/JavaScript files that can then invoke backend functionality without reloading the page.

最新のWebアプリケーションは、全てのアプリケーションコンポーネントを静的ファイルにパッケージ化するSPAとして構築されています。
従来のクライアントサーバWebアーキテクチャでは、ボタンのクリックや検索する度にサーバへのリクエストが発生し、アプリケーション全体が読み込まれていた。
最新のWebアプリケーションでは、ページをリロードせずにバックエンドを呼び出すことができるビルド済みHTML/JSファイルにより、ネイティブアプリのようなUXを提供しています。

### 静的サイトジェネレータとは?
- Static Site Generator（SSG）のこと。

[こちら](https://www.science.co.jp/document_jamstack_blog/27767/)の記事によると、以下の記載がありました。

> 静的サイトジェネレータ (Static Site Generator = SSG) は、入力ファイルから静的ページを構築するためのツールです。コンテンツ（ヘッドレスCMSなど）を取り込み、選択したテンプレートを適用して、静的なHTMLページを生成します。

コンテンツを用意しておいて、予め用意されたテンプレートを元にHTMLを生成するツールとのことです。

また、対局にあるものとして、動的WebサイトのWordPressがあり、WordPressの場合は、ユーザリクエストの際にページを構築するようです。SSGの場合、既に構築したページがCDNに保存されていて、ユーザリクエスト時にダウンロードするので、レイテンシに違いが出るとのことです。

# 実践
- AWSの「[基本的なウェブアプリケーションを構築する](https://aws.amazon.com/jp/getting-started/hands-on/build-web-app-s3-lambda-api-gateway-dynamodb/)」を参考に環境を構築したいと思います。

## 1.Webアプリケーション 
### コードの準備
- 以下のコードを,index.htmlファイルとして保存し、zip圧縮します。
```
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello World</title>
</head>

<body>
    Hello World
</body>
</html>
```

### Amplifyトップページ
- Amplifyのマネコンで「使用を開始する」をクリックします。
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/042f8e56-2dee-eca3-3ff9-a28f9f8f71c0.png">

- ウェブアプリケーションをホストで「使用を開始する」をクリックします。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/61d8b3f4-993b-d333-9e59-d536eb37cc9a.png">


### デプロイ
- 「Gitプロバイダーなしでデプロイ」を選択し、「続行」をクリックします。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/5f250e0d-82ae-437f-2e92-11fa0bfae29b.png">

- 以下を入力し、保存してデプロイをクリックします。
    - アプリケーションの名前：GettingStarted
    - 環境名：dev
    - 方法：ドラッグアンドドロップ
    - ファイルは、index.html.zipを選択。（コードの準備で作成し、zip圧縮したもの。）
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/5a82deed-7182-be8d-d490-27996db211b6.png">

- しばらくするとデプロイが完了します。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/6ea119f5-106b-0092-3095-055b66aeabda.png">


### 確認
- リンクをクリックすると確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/a138a8f7-2e2a-e813-1c61-c2852eca013f.png)

## 2.サーバレス関数を作成する
### Lambdaトップページ
- Lambdaのマネコンで「関数の作成」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/d80c2192-ab98-71c2-1bf0-9e9c65658f51.png)

- 以下を入力し、関数の作成をクリックします。
    - 関数名：HelloWorldFunction
    - ランタイム：Python3.8
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/7d99c1c2-a4f4-f559-bae7-04962257d5ab.png)

### コード
- 以下のコードを貼り付けて、「Deploy」をクリックします。
```
# import the JSON utility package since we will be working with a JSON object
import json
# define the handler function that the Lambda service will use an entry point
def lambda_handler(event, context):
# extract values from the event object we got from the Lambda service
    name = event['firstName'] +' '+ event['lastName']
# return a properly formatted JSON object
    return {
    'statusCode': 200,
    'body': json.dumps('Hello from Lambda, ' + name)
    }

```

### 確認
- Testボタンをクリックします。
- 以下を入力し、「保存」をクリックします。
    - イベント名：HelloWorldTestEvent
    - イベントJSONに以下を入力
```
{
"firstName": "Ada",
"lastName": "Lovelace"
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/cf1d56fe-585e-8218-b6cd-76b462fb1c34.png)

- Testボタンをクリックし、以下が表示されました。
    - 上記で作成したLambdaは、firstNameとlastNameから値を取得し、JSONフォーマットのbody属性に埋め込むコードでした。
    - 結果を見ると、HelloWorldTestEventで引数に渡した、AdaとLovelaceが表示されてるので成功ですね。
```
Test Event Name
HelloWorldTestEvent

Response
{
  "statusCode": 200,
  "body": "\"Hello from Lambda, Ada Lovelace\""
}

Function Logs
START RequestId: 614a7680-a599-475a-9379-7f7ba35e702f Version: $LATEST
END RequestId: 614a7680-a599-475a-9379-7f7ba35e702f
REPORT RequestId: 614a7680-a599-475a-9379-7f7ba35e702f	Duration: 1.38 ms	Billed Duration: 2 ms	Memory Size: 128 MB	Max Memory Used: 37 MB	Init Duration: 138.99 ms

Request ID
614a7680-a599-475a-9379-7f7ba35e702f
```

## 3.サーバーレス関数をウェブアプリケーションにリンクする
- API Gatewayを作成し、Lambda関数とつなぎます。

### 新しいREST APIを作成
- API GatewayのマネコンのREST APIで「構築」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/4f335459-5a77-dbbc-30fd-e1c605509f1f.png)

- 以下を入力し、「APIの作成」をクリックします。
    - プロトコル：REST
    - 新しいAPI
    - API名：HelloWorldAPI
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/2ecda6aa-9d09-803b-8371-71bc94c116a2.png)

### 新しいリソースとメソッドを作成
-  メソッドの作成をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/f6178f40-b26d-89bc-a2fc-75c1b6bd6db7.png)

- POSTを選択し、チェックを入れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/09bb1156-4907-4b81-5ef6-c221e314f3a5.png)

- 以下を入力し、保存をクリックします。
    - 統合タイプ：Lambda関数
    - Lambda関数：HelloWorldFunction（2.で作成したLambda関数）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/d958cfc1-71eb-23d5-46a7-86f69d679ae4.png)

- 「API Gateway に、Lambda 関数を呼び出す権限を与えようとしています:」と表示されるので、OKをクリックします。
- POSTメソッドを選択した状態で、アクション＞CORSの有効化をクリックします。
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/ed8a20e6-89dd-8627-91f0-2fd16cc73738.png">

- メソッドにPOSTが選択されていることを確認し、「CORSを有効にして既存のCORSヘッダーを置換」をクリックします。
:::note 
Access-Control-Allow-Originについて
今回のWebアプリケーションでは、Amplifyにデプロイした静的コンテンツから、ボタンをクリックした際にJavaScriptが実行され、API Gatewayにリクエストされます。
デフォルトのままで動かすと、AmplifyにデプロイしたドメインとAPI Gatewayのドメインが異なるため、ブラウザの制限で動作しません。
そのため、CORS(Cross-origin Resource Sharing)を有効にし、動作させます。
以下のキャプチャでは、**Access-Control-Allow-OriginがXXXXXXX**となっていますが、実際にはAmplifyのオリジンを指定しています。
:::

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/6dfd2d9e-61e3-ee54-13be-ceb39e97e056.png">

- メソッド変更の確認で「はい、既存の値を置き換えます」をクリックします。

### APIのデプロイ
- アクション＞APIのデプロイをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/920e6159-58fe-a3df-5096-eab5a40c468d.png)
- 以下を入力し、デプロイをクリックします。
    - デプロイされるステージ：[新しいステージ]
    - ステージ名:dev

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/0b46b726-3d8b-d57d-6bb8-c42e0ee7cbe4.png">

- 「変更を保存」をクリックします。

### APIの検証
- ナビゲーションペインの「リソース」をクリックします。
<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/b60f6807-8e03-9ca3-ad35-91c22bbb861f.png">

- POSTをクリックし、テストをクリックします。
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/4b97f687-dd35-a10c-0b83-953d7e704e61.png">

- 以下をリクエスト本文フィールドに貼り付けて、テストをクリックします。
```
{
    "firstName":"Grace",
    "lastName":"Hopper"
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/5e569240-910f-7a1d-98fb-e61c2d2a000f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/0a1a84a6-4b6a-3355-4a06-158a5221f632.png)

- リクエスト本文に設定したfirstNameとlastNameがレスポンスに表示されました。
- レスポンスヘッダーにも設定した値が反映されていました。

- Lambdaのログにも反映されていました。（分かりづらいですが。。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/1b5bfbc3-aacc-b85d-510c-d7df4230a9fb.png)

## 4.データテーブルを作成
:::note info
今回は、Amplifyの挙動を確認するだけなので、DynamoDBの作成は省略しています。
:::

## 5.ウェブアプリケーションに対話性を追加
- Amplifyでデプロイしたhtmlから、API Gatewayをつなぎます。

#### コードの修正
- 以下のコードをindex.htmlに貼り付け、41行目のYOUR-API-INVOKE-URLをAPI Gatewayのオリジンに置き換えます。また、同様にZip圧縮します。
```
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello World</title>
    <!-- Add some CSS to change client UI -->
    <style>
    body {
        background-color: #232F3E;
        }
    label, button {
        color: #FF9900;
        font-family: Arial, Helvetica, sans-serif;
        font-size: 20px;
        margin-left: 40px;
        }
     input {
        color: #232F3E;
        font-family: Arial, Helvetica, sans-serif;
        font-size: 20px;
        margin-left: 20px;
        }
    </style>
    <script>
        // define the callAPI function that takes a first name and last name as parameters
        var callAPI = (firstName,lastName)=>{
            // instantiate a headers object
            var myHeaders = new Headers();
            // add content type header to object
            myHeaders.append("Content-Type", "application/json");
            // using built in JSON utility package turn object to string and store in a variable
            var raw = JSON.stringify({"firstName":firstName,"lastName":lastName});
            // create a JSON object with parameters for API call and store in a variable
            var requestOptions = {
                method: 'POST',
                headers: myHeaders,
                body: raw,
                redirect: 'follow'
            };
            // make API call with parameters and use promises to get response
            fetch("YOUR-API-INVOKE-URL", requestOptions)
            .then(response => response.text())
            .then(result => alert(JSON.parse(result).body))
            .catch(error => console.log('error', error));
        }
    </script>
</head>
<body>
    <form>
        <label>First Name :</label>
        <input type="text" id="fName">
        <label>Last Name :</label>
        <input type="text" id="lName">
        <!-- set button onClick method to call function we defined passing input values as parameters -->
        <button type="button" onclick="callAPI(document.getElementById('fName').value,document.getElementById('lName').value)">Call API</button>
    </form>
</body>
</html>
```

- Amplifyのマネコンで、先ほど作成したウェブアプリケーションを選択して、zipファイルを置き換えます。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/b9a84c99-0764-caa0-3b5e-f86d52e75e88.png">

### ウェブアプリケーションをテスト
- リクエストし、画面を表示する。FirstNameとLastNameに適当に文字列を入れて、Call APIをクリックする。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/e0048d72-8e2f-451d-8fd0-2570b15b4dca.png">

- Hello from Lambda ${入力値}が表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/c4252f63-a031-cded-0986-e55b4be8fe3e.png)

# 考察
- Amplifyにより、簡単に静的コンテンツがデプロイできバックエンド(API Gateway、Lambda)と通信ができました。他にも多くの機能やGithubと連携したデプロイなど試せてないので今後、確認したいと考えています。

# 参考
https://docs.aws.amazon.com/ja_jp/amplify/latest/userguide/welcome.html

https://aws.amazon.com/jp/getting-started/hands-on/build-web-app-s3-lambda-api-gateway-dynamodb/

https://www.science.co.jp/document_jamstack_blog/27767/