AWS モダンウェブアプリケーションを構築するチュートリアルをやってみた

# 初めに

以下のチュートリアルをやってみました。このチュートリアルでは S3 を利用した静的ウェブサイトを Fargate や Network Load Balancer、API Gateway 利用して動的ウェブサイトにする過程を学ぶことができました。さらに CI/CD 環境の構築、クリックイベントに対するストリーミング処理の方法も学ぶことができます。このチュートリアルではほとんど CLI、時折 CloudFormation を使用してアプリケーションを構築していきます。

https://aws.amazon.com/jp/getting-started/hands-on/build-modern-app-fargate-lambda-dynamodb-python/

# 1. 静的 Web サイトの作成

開発環境として Cloud9 を用います。リージョンは us-east-1 を選択しました。以下の手順で開発環境を作成します。

開発環境の名前を入力します。

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/fe3d5a33-8300-c94f-e5c2-3af0175f9a4f.png)

すべてデフォルトのまま進めます。

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/13e4b40d-8d61-4669-ae98-d899fc279157.png)

「Next step」をクリックします。

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/bc22baf6-819c-cc03-7ddc-65360ab09c45.png)

「Create environment」をクリックします。

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/dbc36dcd-5d4c-7fb3-220a-0f8ec3d4f557.png)

チュートリアルに使用するコードをクローンし、そのルートディレクトリに移動します。

```
git clone -b python https://github.com/aws-samples/aws-modern-application-workshop.git
cd aws-modern-application-workshop
```

ウェブサイトをホスティングするバケットを作成します。`mythical-mysfits-python-tutorial-bucket` はバケット名です。バケット名はグローバルで一意である必要があります。

```
aws s3 mb s3://mythical-mysfits-python-tutorial-bucket
```

次に静的 Web ホスティングを有効にします。静的 Web ホスティングを有効化することで以下のようなウェブサイトエンドポイントを利用して、リクエスト先を index.html に指定することができます。

- `http://bucket-name.s3-website-Region.amazonaws.com`
- `http://bucket-name.s3-website.Region.amazonaws.com`

上記 2 つのうちどちらを選択するかどうかはリージョンごとに決まっています。以下のリンクにすべて記載があります。

https://docs.aws.amazon.com/general/latest/gr/s3.html#s3_website_region_endpoints

ユーザーガイドに記載がある通り、これらのエンドポイントには https ではアクセスできません。

> Amazon S3 ウェブサイトエンドポイントは HTTPS をサポートしていません。

静的 Web ホスティングを有効にするために以下のコマンドを実行します。

```
aws s3 website s3://mythical-mysfits-python-tutorial-bucket --index-document index.html
```

なお上記の CLI は以下のコンソール画面の操作を行うのと同じことです。「S3 コンソール画面 → 設定したいバケット名を選択 → プロパティ → 静的ウェブサイトホスティング」 の手順で設定できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/ae55b1d3-8aa9-0dcc-d1c9-2d1481a4814b.png)

/aws-modern-application-workshop/module-1/aws-cli/website-bucket-policy.json を編集します。

```/aws-modern-application-workshop/module-1/aws-cli/website-bucket-policy.json
{
  "Id": "MyPolicy",
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadForGetBucketObjects",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::REPLACE_ME_BUCKET_NAME/*"
    }
  ]
}
```

上記の `REPLACE_ME_BUCKET_NAME` を作成したバケット名で置き換えます。このチュートリアルではこのような置き換えが何度も出てきます。チュートリアル通りに進めているのにエラーが発生した場合は、この置き換えがされていないことが原因かもしれません。以下はバケット名を置き換えた後のポリシーです。

```/aws-modern-application-workshop/module-1/aws-cli/website-bucket-policy.json
{
    "Version": "2012-10-17",
    "Id": "MyPolicy",
    "Statement": [
        {
            "Sid": "PublicReadForGetBucketObjects",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::mythical-mysfits-python-tutorial-bucket/*"
        }
    ]
}
```

編集を保存した後、以下のコマンドを実行しバケットポリシーを設定します。

```
aws s3api put-bucket-policy --bucket mythical-mysfits-python-tutorial-bucket --policy file://~/environment/aws-modern-application-workshop/module-1/aws-cli/website-bucket-policy.json
```

S3 コンソール画面で確認し、設定が反映されていることが確認してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/5e309c6d-abf5-94fe-01c3-de82704ba97f.png)

/aws-modern-application-workshop/module-1/web/index.html をバケットにアップロードします。

```
aws s3 cp ~/environment/aws-modern-application-workshop/module-1/web/index.html s3://mythical-mysfits-python-tutorial-bucket/index.html
```

ウェブサイトエンドポイントの
`http://mythical-mysfits-python-tutorial-bucket.s3-website-us-east-1.amazonaws.com`
にアクセスし、ホスティングが正常に行われていることを確認します。

# 2. 動的ウェブサイトの構築

この章は動的なウェブサイトを構築するためのステップです。Python を使用して、 Docker コンテナに Flask アプリケーションを作成します。

## 2-1. CloudFormation でインフラを構築する

Fargate は VPC 内に配置されるサービスであり、VPC や サブネットなどを作成する必要があるため、このチュートリアルでは CloudFormation を使用します。



| リソース                        | 用途                                                         | 個数 |
| ------------------------------- | ------------------------------------------------------------ | ---- |
| VPC                             | Fargate を配置する                                           | 1    |
| NAT Gateway 用 Elastic IP       | NAT Gateway の変換グローバル IP アドレスとして使用する       | 2    |
| NAT Gateway                     | プライベートサブネットからインターネットにアクセスする       | 2    |
| Internet Gateway                | パブリックサブネットとインターネットを接続する               | 1    |
| Fargate 用 セキュリティグループ | Fargate が NLB からのトラフィックを許可するために使用する    | 1    |
| パブリックサブネット            | NLB を配置し、インターネットと通信する                       | 2    |
| プライベートサブネット          | Fargate をセキュアに配置する                            | 2    |
| DynamoDB VPC エンドポイント     | Fargate がプライベートサブネットから DynamoDB にアクセスするために使用する | 1    |
| ECS タスクロール                | Fargate が DynamoDB にアクセスすることを許可する             | 1    |
| ECS サービスロール              | トラフィックがコンテナに到達できるように ECS がリソースを管理することを許可する。イメージのプルや、ログの書き出しを許可する。 | 1    |
| CodeBuild 用 ロール             | ビルドの実行に必要なアクションを許可する                     | 1    |
| CodePipeline 用 ロール          | パイプラインの実行に必要なアクションを許可する               | 1    |



上記の ECS タスクロールと ECS サービスロールはタスク定義内で以下のように設定されます。

![19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/b0cfffa2-e581-dd5f-8bd9-867612598c17.png)

以下のコマンドで CloudFormation のスタックを作成します。`--capabilities CAPABILITY_NAMED_IAM` はスタックリソースに IAM ロールが含まれているので必要なオプションです。

> カスタム名を持つIAMリソースがある場合は、CAPABILITY_NAMED_IAMを指定する必要があります。

```
aws cloudformation create-stack --stack-name MythicalMysfitsCoreStack --capabilities CAPABILITY_NAMED_IAM --template-body file://~/environment/aws-modern-application-workshop/module-2/cfn/core.yml
```

以下のコマンドでスタックの作成状況を確認します。

```
aws cloudformation describe-stacks --stack-name MythicalMysfitsCoreStack
```

結果は以下のように返ってきます。`StackStatus` というキーを確認し、この値が `CREATE_COMPLETE` になるまでしばらく待ちます。

```json
{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/MythicalMysfitsCoreStack/3a8d94b0-eb6a-11eb-8b9c-0e443e7db6f3", 
            "DriftInformation": {
                "StackDriftStatus": "NOT_CHECKED"
            }, 
            "Description": "This stack deploys the core network infrastructure and IAM resources to be used for a service hosted in Amazon ECS using AWS Fargate.", 
            "Tags": [], 
            "EnableTerminationProtection": false, 
            "CreationTime": "2021-07-23T03:58:24.697Z", 
            "Capabilities": [
                "CAPABILITY_NAMED_IAM"
            ], 
            "StackName": "MythicalMysfitsCoreStack", 
            "NotificationARNs": [], 
            "StackStatus": "CREATE_IN_PROGRESS", 
            "DisableRollback": false, 
            "RollbackConfiguration": {}
        }
    ]
}
```

コンソール画面からでも作成状況を確認することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/9cecbe80-dd3b-f448-a816-c1cd4ab87fe2.png)

スタックの作成が完了後、今後のアプリケーションインフラ構築を進めやすくするために以下のようにスタックの作成結果をファイルに書き出しておきます。ファイルに書き出した結果は、このスタックで作成したリソースの ARN を参照するために今後何度も使用することになります。

```
aws cloudformation describe-stacks --stack-name MythicalMysfitsCoreStack > ~/environment/cloudformation-core-output.json
```

## 2-2. Docker コンテナの準備

このコンテナで実行されるのは Flask アプリケーションです。以下のコードが実行されます。

- `/` にアクセスしたクライアントには 「'/mysfits' にアクセスしてください」というレスポンスを返す
- `/mysfits` にアクセスしたクライアントには mysfits たちの名前や年齢などの情報が記載された `mysfits-response.json` をレスポンスとして返す

```/aws-modern-application-workshop/module-2/app/service/mythicalMysfitsService.py
from flask import Flask, jsonify, json, Response, request
from flask_cors import CORS

app = Flask(__name__)
app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False
CORS(app)

@app.route("/")
def healthCheckResponse():
    return jsonify({"message" : "Nothing here, used for health check. Try /mysfits instead."})

@app.route("/mysfits")
def getMysfits():

    response = Response(open("mysfits-response.json", "rb").read())
    response.headers["Content-Type"]= "application/json"

    return response

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)
```

Fargate を開始するためのインフラ構築後、Fargate でコンテナを起動するために、まず Docker イメージを作成します。

```
cd ~/environment/aws-modern-application-workshop/module-2/app
docker build . -t 123456789012.dkr.ecr.us-east-1.amazonaws.com/mythicalmysfits/service:latest
```

ビルドが完了すると最後に以下のような出力があります。この ID はイメージの ID です。

```
Successfully built 4611ce22a1cc
Successfully tagged 123456789012.dkr.ecr.us-east-1.amazonaws.com/mythicalmysfits/service:latest
```

上記イメージ ID を渡してコンテナをローカルで起動します。

```
docker run -p 8080:8080 4611ce22a1cc
```

以下の「Preview Runnig Application」をクリックします。

![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/08f33b3d-8677-7bf8-824e-27a6d20e51f9.png)

URL の最後に「/myfits」を追加し、レスポンスとして JSON ドキュメントが確認できました。

![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/a33de20d-0288-8720-f6ba-6f493744bca9.png)

イメージを ECR リポジトリにプッシュします。以下のようにリポジトリを作成します。その後、ログインします。

```
aws ecr create-repository --repository-name mythicalmysfits/service
$(aws ecr get-login --no-include-email)
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/mythicalmysfits/service:latest
```

コンソールでもプッシュされたイメージを確認してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/f88cfd13-848d-97eb-e4f7-ac7d566a00b1.png)

次に Fargate クラスターを作成します。以下のコマンドでクラスターと CloudWatch Logs のロググループを作成します。

```
aws ecs create-cluster --cluster-name MythicalMysfits-Cluster
aws logs create-log-group --log-group-name mythicalmysfits-logs
```

/cloudformation-core-output.json を参照しながら /aws-modern-application-workshop/module-2/aws-cli/task-definition.json を編集します。編集するのは以下3点です。その後、タスク定義を登録します。

- REPLACE_ME_ECS_SERVICE_ROLE_ARN
- REPLACE_ME_ECS_TASK_ROLE_ARN
- REPLACE_ME_IMAGE_TAG_USED_IN_ECR_PUSH

```
aws ecs register-task-definition --cli-input-json file://~/environment/aws-modern-application-workshop/module-2/aws-cli/task-definition.json
```

## 2-3. NLB を作成する

Network Load Balancer を作成します。`--subnets` オプションには 2 つのパブリックサブネット ID を渡します。この実行結果を nlb-output.json というファイルに書き出しておきます。

```
aws elbv2 create-load-balancer --name mysfits-nlb --scheme internet-facing --type network --subnets subnet-0ebf9d3a8 subnet-fa7bdfa5 > ~/environment/nlb-output.json
```

ターゲットグループを作成します。この実行結果は target-group-output.json というファイルに書き出しておきます。

```
aws elbv2 create-target-group --name MythicalMysfits-TargetGroup --port 8080 --protocol TCP --target-type ip --vpc-id vpc-0ebf9d3a8fakm8fa5 --health-check-interval-seconds 10 --health-check-path / --health-check-protocol HTTP --healthy-threshold-count 3 --unhealthy-threshold-count 3 > ~/environment/target-group-output.json
```

リスナーを作成します。

```
aws elbv2 create-listener --default-actions TargetGroupArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/MythicalMysfits-TargetGroup/0249b754ae522df7,Type=forward --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/mysfits-nlb/e7795589l86fc2d7 --port 80 --protocol TCP
```

## 2-4. Fargate でタスクを実行する

/aws-modern-application-workshop/module-2/aws-cli/service-definition.json を編集します。以下を置き換えます。

- REPLACE_ME_SECURITY_GROUP_ID
- REPLACE_ME_PRIVATE_SUBNET_ONE
- REPLACE_ME_PRIVATE_SUBNET_TWO
- REPLACE_ME_NLB_TARGET_GROUP_ARN

以下のコマンドを実行しサービスを作成します。

```
aws ecs create-service --cli-input-json file://~/environment/aws-modern-application-workshop/module-2/aws-cli/service-definition.json
```

コンソール画面を確認するとタスクが実行されていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/a1f3d39f-7010-272d-aa5e-d92d0d5d39e7.png)

ブラウザを開き、http\://NLB_DNS_NAME/mysfits にアクセスします。以下の `NLB_DNS_NAME` を NLB の DNS 名で置き換えます。ただし、http でアクセスします。証明書がないため、 https ではアクセスできません。以前 Cloud9 で Docker を起動したときのレスポンスが返ってきていれば、正常に Fargate が稼働しています。

`REPLACE_ME` を `http://NLB_DNS_NAME` で置き換えます。

```/aws-modern-application-workshop/module-2/web/index.html
  <script>

    var mysfitsApiEndpoint = 'REPLACE_ME'; // example: 'http://mythi-publi-abcd12345-01234567890123.elb.us-east-1.amazonaws.com'

    var app = angular.module('mysfitsApp', []);
```

このエンドポイントは ajax 呼び出しの URL に渡されます。/aws-modern-application-workshop/module-2/web/index.html の該当ソースは以下の通りです。

```js
    function getAllMysfits(callback) {

      var mysfitsApi = mysfitsApiEndpoint + '/mysfits';

      $.ajax({
        url : mysfitsApi,
        type : 'GET',
        success : function(response) {
          callback(response.mysfits);
        },
        error : function(response) {
          console.log("could not retrieve mysfits list.");
          console.log(response.message);
        }
      });
```

変更を保存した後、バケットにアップロードします。

```
aws s3 cp ~/environment/aws-modern-application-workshop/module-2/web/index.html s3://mythical-mysfits-python-tutorial-bucket/index.html
```

## 2-5. CI/CD 環境を構築する

コードの変更を自動でデプロイするために、CodeCommit、CodeBuild、CodePipeline を使用して CI/CD 環境を構築します。

CodeBuild の出力アーティファクトを格納するバケットを作成します。

```
aws s3 mb s3://mythical-mysfits-python-tutorial-artifacts-bucket
```

/aws-modern-application-workshop/module-2/aws-cli/artifacts-bucket-policy.json を編集します。以下を置き換えます。その後バケットポリシーを設定するコマンドを実行します。

- REPLACE_ME_CODEBUILD_ROLE_ARN
- REPLACE_ME_CODEPIPELINE_ROLE_ARN

```
aws s3api put-bucket-policy --bucket mythical-mysfits-python-tutorial-artifacts-bucket --policy file://~/environment/aws-modern-application-workshop/module-2/aws-cli/artifacts-bucket-policy.json
```

CodeCommit にリポジトリを作成します。

```
aws codecommit create-repository --repository-name MythicalMysfitsService-Repository
```

ビルドプロジェクトを作成するために /aws-modern-application-workshop/module-2/aws-cli/code-build-project.json を編集します。以下を置き換えます。

- REPLACE_ME_ACCOUNT_ID
- REPLACE_ME_REGION
- REPLACE_ME_CODEBUILD_ROLE_ARN

```
aws codebuild create-project --cli-input-json file://~/environment/aws-modern-application-workshop/module-2/aws-cli/code-build-project.json
```

パイプラインを作成するために /aws-modern-application-workshop/module-2/aws-cli/code-pipeline.json を編集します。以下を置き換えます。

- REPLACE_ME_CODEPIPELINE_ROLE_ARN
- REPLACE_ME_ARTIFACTS_BUCKET_NAME

```
aws codepipeline create-pipeline --cli-input-json file://~/environment/aws-modern-application-workshop/module-2/aws-cli/code-pipeline.json
```

ECR のリポジトリポリシーを設定します。以下を置き換えます。

- REPLACE_ME_CODEBUILD_ROLE_ARN

```
aws ecr set-repository-policy --repository-name mythicalmysfits/service --policy-text file://~/environment/aws-modern-application-workshop/module-2/aws-cli/ecr-policy.json
```

コンソール画面ではリポジトリポリシーを確認できなかったので、CLI で確認してみました。以下のコマンドで確認できます。

```
aws ecr get-repository-policy --repository-name mythicalmysfits/service
```

CodeCommit のリポジトリをクローンし、ローカルリポジトリを作成します。

```
git clone https://git-codecommit.us-east-1.amazonaws.com/v1/repos/MythicalMysfitsService-Repository
```

CodeCommit のローカルリポジトリにアプリケーションコードをコピーします。

```
cp -r ~/environment/aws-modern-application-workshop/module-2/app/* ~/environment/MythicalMysfitsService-Repository/
```

パイプラインが正常に機能しているかをチェックするために Evangeline の年齢を変更します。

```
"species": "Chimera",
"age": 999,
```

変更を CodeCommit リポジトリにプッシュします。

```
git add .
git commit -m "I changed the age of one of the mysfits."
git push
```

以下はプッシュ後のリポジトリです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/f2c9c085-4b24-9e0b-4516-f1d11df06f64.png)

コンソール画面でパイプラインが実行されていることを確認してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/ed52cfe5-47b0-0054-d364-94ab2c5e99d7.png)

コンソール画面では以下のようにビルドが実行中です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/fe12ee4e-fc53-21c0-b3e3-9b305656c8b5.png)

パイプラインの実行完了後、ウェブサイトを更新し、Evangeline の年齢が変更した値に更新されていることが確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/742d8b32-eeb5-6c45-64d6-7c9dd0d82b28.png)

# 3. MYSFIT データの保存

この章では mysfit を JSON ファイルにで管理するのではなく、データベースに保存する設定を行います。データベースには DynamoDB を利用します。

## 3-1. テーブルを作成し、アイテムを追加する

テーブルを作成しアイテムを追加します。

```
aws dynamodb create-table --cli-input-json file://~/environment/aws-modern-application-workshop/module-3/aws-cli/dynamodb-table.json
aws dynamodb batch-write-item --request-items file://~/environment/aws-modern-application-workshop/module-3/aws-cli/populate-dynamodb.json
```

コンソール画面で確認してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/9dfbaf35-45be-6742-f6f8-84170c5b9365.png)

## 3-2. DynamoDB を参照するようにコードを編集する

Flask アプリケーションで DynamoDB からデータを取得するために /aws-modern-application-workshop/module-3/app/service/mythicalMysfitsService.py を用います。このモジュールは boto3 という SDK を使用して DynamoDB からデータを取得します。例えば、以下のコードはテーブルに登録されている全データを取得します。

```/aws-modern-application-workshop/module-3/app/service/mythicalMysfitsService.py
import boto3

...
...
def getAllMysfits():
    response = client.scan(
        TableName='MysfitsTable'
    )
```

リポジトリに変更を加え、プッシュします。

```
cp ~/environment/aws-modern-application-workshop/module-3/app/service/* ~/environment/MythicalMysfitsService-Repository/service/
cd ~/environment/MythicalMysfitsService-Repository
git add .
git commit -m "Add new integration to DynamoDB."
git push
```

ビルドに失敗しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/17c63464-79b7-0c02-b811-bff102b8a097.png)

「詳細」をクリックして CodeBuild のビルドログを確認します。

```
[Container] 2021/07/23 08:24:48 Running command docker build -t mythicalmysfits/service:latest .
Sending build context to Docker daemon  19.97kB

Step 1/13 : FROM ubuntu:latest
latest: Pulling from library/ubuntu
toomanyrequests: You have reached your pull rate limit. You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limit

[Container] 2021/07/23 08:24:49 Command did not exit successfully docker build -t mythicalmysfits/service:latest . exit status 1
```

公式ドキュメントに以下の記述があります。

> プルリクエストを発行し、アカウントタイプの制限を超えた429場合、マニフェストがリクエストされると、DockerHubは次の本文のレスポンスコードを返します。
>
You have reached your pull rate limit. You may increase the limit by authenticating and upgrading: https\://www\.docker.com/increase-rate-limits

https://matsuand.github.io/docs.docker.jp.onthefly/docker-hub/download-rate-limit/

チュートリアルに記載のあった Git 認証を飛ばしていたので、匿名ユーザーに対してレート制限が適用されたようです。Git 認証を追加し再度プッシュします。

```
git config --global user.name "xxxx"
git config --global user.email yyyy@example.com
git config --global credential.helper '!aws codecommit credential-helper $@'
git config --global credential.UseHttpPath true
```

なお、認証情報は以下のローカルファイルに保存されます。

```~/.gitconfig
[credential]
        helper = !aws codecommit credential-helper $@
        UseHttpPath = true
[core]
        editor = nano
[user]
        name = xxxx
        email = yyyy@example.com
```

プッシュします。

```
git add .
git commit -m "set git config.I retry PiPeline execution"
git push
```

正常にビルドが完了しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/cef1362b-55b3-84bc-7d29-38d534c35c46.png)

# 4. ユーザーの登録

Cognito を使用してユーザー管理を行います。Cognito のユーザープール・アプリクライアントを設定して認証されていないクライアントがサインアップ・サインインできるように実装します。

まずユーザープールを作成します。ユーザープールとはユーザーディレクトリのことです。

```
aws cognito-idp create-user-pool --pool-name MysfitsUserPool --auto-verified-attributes email
```

上記で作成したユーザープール ID を使用してアプリクライアントを作成します。

```
aws cognito-idp create-user-pool-client --user-pool-id us-east-1_BaCZvuQrU --client-name MysfitsUserPoolClient
```

ユーザープール ID と アプリクライアント ID は、JavaScript for SDK によって処理されます。以下は該当ソースになります。

```/aws-modern-application-workshop/module-4/web/register.html 
  <script>
  var cognitoUserPoolId = 'REPLACE_ME';  // example: 'us-east-1_abcd12345'
  var cognitoUserPoolClientId = 'REPLACE_ME'; // example: 'abcd12345abcd12345abcd12345'
...
...
    var poolData = {
      UserPoolId : cognitoUserPoolId,
      ClientId : cognitoUserPoolClientId
    };

    var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
...
...
      userPool.signUp(email, pw, attributeList, null, function(err, result){
          if (err) {
              alert(err.message);
              return;
          }
          cognitoUser = result.user;
          console.log(cognitoUser);
          localStorage.setItem('email', email);
          window.location.replace('confirm.html');
      });
```

上記のコードでの最後に `window.location.replace('confirm.html');` とあります。こちらは登録したメールアドレスに送信される認証コードを確認するためのページです。認証コードはサインアップ後、 Cognito から送信されます。認証コードを Cognito に送信してユーザー登録を完了させるための該当ソースは以下の通りです。

```/aws-modern-application-workshop/module-4/web/confirm.html 
  <script>

  var cognitoUserPoolId = 'REPLACE_ME';  // example: 'us-east-1_abcd12345'
  var cognitoUserPoolClientId = 'REPLACE_ME'; // example: 'abcd12345abcd12345abcd12345'
...
...
    var confirmCode = document.getElementById('confirmCode').value;

    var poolData = {
      UserPoolId : cognitoUserPoolId,
      ClientId : cognitoUserPoolClientId
    };

    var userName = localStorage.getItem('email');

    var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
    var userData = {
        Username : userName,
        Pool : userPool
    };

    var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
    cognitoUser.confirmRegistration(confirmCode, true, function(err, result) {
        if (err) {
            alert(err.message);
            return;
        }
        window.location.replace("index.html");
    });
```


ここからは Cognito によるオーソライザーを使用して、登録されたユーザーのみが特定の機能を使用できるようにします。そのためにはAPI Gateway を NLB に統合する必要があります。

> VPC リンクを使用すると、Application Load Balancer または Amazon ECS コンテナベースのアプリケーションなどの、HTTP API ルートを VPC 内のプライベートリソースに接続するプライベート統合を作成できます。

ただし、このチュートリアルでは NLB がパブリックであり、プライベートリソースではありません。この点について、以下の注意書きがあります。

> 実際の環境では、API Gateway がインターネット接続した API 承認向けの戦略であることがわかっているため、最初から内部用に NLB を作成する必要があります (または、新しい内部ロードバランサーを作成して、既存のものと置き換える必要があります)。ただし、時間を節約するため、既に作成した NLB をパブリックにアクセスできる状態のまま使用します。

以下のコマンドを実行し、VPC リンクを作成します。

```
aws apigateway create-vpc-link --name MysfitsApiVpcLink --target-arns arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/mysfits-nlb/e7795560f86fc2d7 > ~/environment/api-gateway-link-output.json
```

REST API を作成します。/aws-modern-application-workshop/module-4/aws-cli/api-swagger.json の `REPLACE_ME` の箇所を置き換えます。その後以下のコマンドを実行します。

```
aws apigateway import-rest-api --parameters endpointConfigurationTypes=REGIONAL --body file://~/environment/aws-modern-application-workshop/module-4/aws-cli/api-swagger.json --fail-on-warnings
```

コンソール画面で作成された REST API を確認してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/9c35bba3-c3b1-8d19-0c50-66a9b32d37d2.png)

上図のスクリーンショットが良くなかったのですが、「認可」の部分が「なし」になっています。これは、`/` にはオーソライザーが設定されていないからです。オーソライザーが設定されているのは `/mysfits/{mysfitId}/like` と `/mysfits/{mysfitId}/adopt` です。該当ソースは以下の通りです。

```/aws-modern-application-workshop/module-4/aws-cli/api-swagger.json
"/mysfits/{mysfitId}/like": {
  "post": {
      "parameters": [{
          "name": "mysfitId",
          "in": "path",
          "required": true,
          "type": "string"
      }],
      "responses": {
          "200": {
              "description": "Default response for CORS method",
              "headers": {
                  "Access-Control-Allow-Headers": {
                      "type": "string"
                  },
                  "Access-Control-Allow-Methods": {
                      "type": "string"
                  },
                  "Access-Control-Allow-Origin": {
                      "type": "string"
                  }
              }
          }
      },
      "security": [{
          "MysfitsUserPoolAuthorizer": [

          ]
      }],
```

なお、オーソライザーオブジェクトの設定方法のドキュメントは以下になります。

https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-swagger-extensions-authorizer.html


続いて API をデプロイします。

```
aws apigateway create-deployment --rest-api-id hnypyxgqve --stage-name prod
```

バックエンドを更新し、プッシュします。

```
cd ~/environment/MythicalMysfitsService-Repository/
cp -r ~/environment/aws-modern-application-workshop/module-4/app/* .
git add .
git commit -m "Update service code backend to enable additional website features."
git push
```

S3 バケットのオブジェクトも更新します。以下 3 ファイルの `REPLACE_ME` を置き換えます。

```/aws-modern-application-workshop/module-4/web/index.html
<script>
    var mysfitsApiEndpoint = 'REPLACE_ME'; // example: 'https://abcd12345.execute-api.us-east-1.amazonaws.com/prod'
    var cognitoUserPoolId = 'REPLACE_ME';  // example: 'us-east-1_abcd12345'
    var cognitoUserPoolClientId = 'REPLACE_ME'; // example: 'abcd12345abcd12345abcd12345'
    var awsRegion = 'REPLACE_ME'; // example: 'us-east-1' or 'eu-west-1' etc.
```

```/aws-modern-application-workshop/module-4/web/register.html
<script>
  var cognitoUserPoolId = 'REPLACE_ME';  // example: 'us-east-1_abcd12345'
  var cognitoUserPoolClientId = 'REPLACE_ME'; // example: 'abcd12345abcd12345abcd12345'
```

```/aws-modern-application-workshop/module-4/web/confirm.html
<script>
  var cognitoUserPoolId = 'REPLACE_ME';  // example: 'us-east-1_abcd12345'
  var cognitoUserPoolClientId = 'REPLACE_ME'; // example: 'abcd12345abcd12345abcd12345'
```

その後 S3 バケットにアップロードします。

```
aws s3 cp --recursive ~/environment/aws-modern-application-workshop/module-4/web/ s3://mythical-mysfits-python-tutorial-bucket/
```

ウェブサイトを更新すると以下のようにログイン機能が実装されていることが確認できます。

![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/687168e2-af2d-97b6-95f1-e284fb9cc40c.png)

サインアップができることを確認し、ユーザープールに登録されていることが確認できました。

![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621414/18dcfcc7-ab5a-a86a-a733-9833d4ce51c7.png)

# 5. ユーザークリックの把握

ストリーミング処理をするコードを格納するために、新たに CodeCommit にリポジトリを作成します。

```
aws codecommit create-repository --repository-name MythicalMysfitsStreamingService-Repository
```

Lambda 関数のコードを S3 にアップロードするために新たにバケットを作成します。

```
aws s3 mb s3://mythical-mysfits-python-tutorial-lambda-package-bucket
```

Kinesis Firehose から送信されるレコードを Lambda 関数は適切なフォーマットに変換します。該当ソースは以下の通りです。

```/aws-modern-application-workshop/module-5/app/streaming/streamProcessor.py
def processRecord(event, context):
    output = []

    for record in event['records']:
        print('Processing record: ' + record['recordId'])
        click = json.loads(base64.b64decode(record['data']))

        mysfitId = click['mysfitId']
        mysfit = retrieveMysfit(mysfitId)

        enrichedClick = {
                'userId': click['userId'],
                'mysfitId': mysfitId,
                'goodevil': mysfit['goodevil'],
                'lawchaos': mysfit['lawchaos'],
                'species': mysfit['species']
            }

        output_record = {
            'recordId': record['recordId'],
            'result': 'Ok',
            'data': base64.b64encode(json.dumps(enrichedClick).encode('utf-8') + b'\n').decode('utf-8')
        }
        output.append(output_record)

    print('Successfully processed {} records.'.format(len(event['records'])))

    return {'records': output}
```

SAM CLI を利用して CloudFormation テンプレートを作成します。real-time-streaming.yml が transformed-streaming.yml に変換されます。

```
sam package --template-file ./real-time-streaming.yml --output-template-file ./transformed-streaming.yml --s3-bucket mythical-mysfits-python-tutorial-lambda-package-bucket
```

SAM によって変換された CloudFormation のテンプレートを使用してスタックを作成します。

```
aws cloudformation deploy --template-file /home/ec2-user/environment/MythicalMysfitsStreamingService-Repository/transformed-streaming.yml --stack-name MythicalMysfitsStreamingStack --capabilities CAPABILITY_IAM
```

このスタックによって作成されるリソースは以下の通りです。



| リソース             | 用途                                         |
| -------------------- | -------------------------------------------- |
| API Gateway          | Kinesis Firehose と統合するREST API          |
| S3 バケット          | Kinesis のログを格納する                     |
| FirehoseDeliveryRole | Lambda、S3 へのアクションを許可する          |
| Lambda               | Kinesis Firehose のレコードを変換する        |
| Kinesis Firehose     | クリックデータを受信し Lambda、S3 に送信する |



ストリーム用のエンドポイントを追加した新しい index.html を S3 バケットにアップロードします。以下の `REPLACE_ME` を置き換えます。

```/aws-modern-application-workshop/module-5/web/index.html
  <script>

    var mysfitsApiEndpoint = 'REPLACE_ME'; // example: 'https://abcd12345.execute-api.us-east-1.amazonaws.com/prod'
    var streamingApiEndpoint = 'REPLACE_ME'; // example: 'https://abcd12345.execute-api.us-east-1.amazonaws.com/prod'

    var cognitoUserPoolId = 'REPLACE_ME';  // example: 'us-east-1_abcd12345'
    var cognitoUserPoolClientId = 'REPLACE_ME'; // example: 'abcd12345abcd12345abcd12345'
    var awsRegion = 'REPLACE_ME'; // example: 'us-east-1' or 'eu-west-1' etc.
```

以下のコマンドで編集した index.html をアップロードします。

```
aws s3 cp ~/environment/aws-modern-application-workshop/module-5/web/index.html s3://mythical-mysfits-python-tutorial-bucket/
```

S3 バケットに送信された、変換されたクリックイベントのストリーミングデータファイルをダウンロードして確認してみます。

```
{"userId": "7c25e02a-05fe-4fcc-8569-6e8688933805", "mysfitId": "a901bb08-1985-42f5-bb77-27439ac14300", "goodevil": "Good", "lawchaos": "Neutral", "species": "Haetae"}
{"userId": "7c25e02a-05fe-4fcc-8569-6e8688933805", "mysfitId": "a901bb08-1985-42f5-bb77-27439ac14300", "goodevil": "Good", "lawchaos": "Neutral", "species": "Haetae"}
{"userId": "7c25e02a-05fe-4fcc-8569-6e8688933805", "mysfitId": "b6d16e02-6aeb-413c-b457-321151bb403d", "goodevil": "Evil", "lawchaos": "Chaotic", "species": "Troll"}
{"userId": "7c25e02a-05fe-4fcc-8569-6e8688933805", "mysfitId": "a901bb08-1985-42f5-bb77-27439ac14300", "goodevil": "Good", "lawchaos": "Neutral", "species": "Haetae"}
```

上記の処理を行った Lambda が出力したログを CloudWatch Logs で確認してみます。

```
START RequestId: ba9b88c5-a552-4c6e-8df6-9ccb2ba2b10e Version: $LATEST
Processing record: 49620403830958916807586905640547610254069909630134255618000000
Processing record: 49620403830958916807586905640568161993003359356896411650000000
Processing record: 49620403830958916807586905640572997696281818079753666562000000
Processing record: 49620403830958916807586905640602011915952570485616672770000000
Successfully processed 4 records.
END RequestId: ba9b88c5-a552-4c6e-8df6-9ccb2ba2b10e
REPORT RequestId: ba9b88c5-a552-4c6e-8df6-9ccb2ba2b10e	Duration: 653.45 ms	Billed Duration: 654 ms	Memory Size: 128 MB	Max Memory Used: 53 MB	
```

正常にレコードが処理されていることが確認できました。

以上でチュートリアルは終了です。

# 参考記事

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/WebsiteEndpoints.html

https://docs.aws.amazon.com/general/latest/gr/s3.html#s3_website_region_endpoints

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/using-iam-template.html

https://docs.aws.amazon.com/cli/latest/reference/s3/website.html

https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/http-api-vpc-links.html

https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/target-group-health-checks.html

https://docs.aws.amazon.com/ja_jp/codecommit/latest/userguide/setting-up-https-windows.html#setting-up-https-windows-credential-helper

https://aws.amazon.com/jp/kinesis/data-firehose/?kinesis-blogs.sort-by=item.additionalFields.createdDate&kinesis-blogs.sort-order=desc

https://matsuand.github.io/docs.docker.jp.onthefly/docker-hub/download-rate-limit/

https://dev.classmethod.jp/articles/codebuild-has-to-use-dockerhub-login-to-avoid-ip-gacha/