AWS初心者（一ヶ月）がサーバレス開発　～ CloudWatch と サービス間の接続周り 編 ～

[前前回](https://qiita.com/k-mishima/items/94a46d81951d0a4c3f3e)と[前回](https://qiita.com/k-mishima/private/458bf25a8641e3f72cef)に引き続き、AWS を触り始めて一ヶ月強の人間がサーバレス開発します。

# 脳内作戦会議、三度

既にフロント側とバック側は作り終えたわけで。
CloudWatch は、各サービスでログを仕込めば良いわけで。
あとは、フロント側とバック側を繋いであげればオッケーなわけで。
AWS を触り始めて一ヶ月強の僕でも、なんとかなりそうな希望が持てる気がしてくるわけで。

|![capture_000022.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/5cf19a63-7b64-8053-9374-51834f7ce531.png)|
|:--|

- 作業a : [S3 & CloudFront](https://qiita.com/k-mishima/items/94a46d81951d0a4c3f3e)
- 作業b : [API Gateway & Lambda & DynamoDB](https://qiita.com/k-mishima/private/458bf25a8641e3f72cef)
- 作業c : CloudWatch と サービス間の接続周り(本記事)

# CloudWatch と サービス間の接続周り

希望が持てる気がしたところで、やりたいことはコレだ。

- 各サービスにログを仕込んで CloudWatch でログ出力を確認
- フロント側とバック側を接続
- 最終的な動作確認

## CloudWatch

各サービスにログを仕込むべく公式をトレースするだけのお仕事です。

### S3 サーバーアクセスログ

CloudWatch でログを確認するわけではないけど、
S3 のアクセスログも出力できるならしておきたいよね、ってことで。
まずログ保存先となるバケットを作成する。

|![cf1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/c53a6584-d10d-b0bc-0089-3d1107e406c5.png)|
|:--|

HTML 格納中のバケットでログを有効化する。

|![cf2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/68e1feb0-e765-90c4-9b4a-4f715d48a9de.png)|
|:--|

プレフィックスを付けるのがオススメらしいので、事前に作成したディレクトリを指定する。
設定は即時反映ではないとのことで、小一時間経過後に確認する。

|![capture_000037.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/5c49219c-e65e-9092-fd59-07f4f7ee1f14.png)|
|:--|

なんかでた。
あぁぁ、ダウンロードからの参照なのね、確認が面倒くさい。

### CloudFront 標準ログ(アクセスログ) 

CloudWatch でログを確認するわけではないけど、
CloudFront のアクセスログも出力できるならしておきたいよね、ってことで。
保存先に、先程作成したバケットのプレフィックスとして用意したディレクトリを指定する。

|<img width="400" alt="capture_000036.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/866dc967-b106-01fa-1cb5-25d7ca0091b9.png">|
|:--|

確認してみる。

|![capture_000038.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/54df76c0-4ad5-5f0e-da86-27144118443c.png)|
|:--|

なんかでた。
うぇぇ、ダウンロードからの参照なのね、確認が面倒くさい。

### Lambda 実行ログ

いつもお世話になっております。
Lambda 関数作成時に自動的に組み込まれるみたい。
当然、ログ出力を設定した記憶はないのであります。

|![capture_000040.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/83d19f27-1c63-26ce-61ef-487d9315cb73.png)|
|:--|

### API Gateway アクセスログ

ログ出力用のロールが必要らしいので IAMコンソールに寄り道。

|![capture_000041.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/2ed7949b-5e16-f061-c48f-73468c518a44.png)|
|:--|

設定から作成したロールを指定して、

|![capture_000042.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/7319a32f-69ff-2324-35e4-cbdb709f1156.png)|
|:--|

ステージのログ/トレースでチェックを入れて、

|![capture_000044.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/81e9817b-c2b0-1225-a4aa-12f3551c9bee.png)|
|:--|

確認してみる。

|![capture_000045.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/1772ae74-b7ac-8c00-19d9-6e82ec71ae62.png)|
|:--|

なにやらでた。
INFO レベルにしただけあって量が多い。
満足。


## サービス間の接続周り

フロント側とバック側を繋ぐだけ。
しかも、前回、先達のパッチワークにより、リクエストパラメータは既にマッピング済みなわけで。
残るはコレだ。

- API Gateway でレスポンスパラメータのマッピングを仕込む
- HTML に POST 先となる API Gateway の URL を仕込む
- HTML に API Gateway からのレスポンスに応じた画面制御を仕込む

### API Gateway でレスポンスパラメータのマッピング

レスポンスは Lambda の処理結果に応じて３種類を用意する。

1. DynamoDB にデータ登録できた
1. キー重複で登録できなかった
1. キー重複以外の理由で登録できなかった

加えて、次を理由にリダイレクト URL を Lambda で仕込むことに。

- Lambda から HTML を出力したくない
- S3 に配置した当該ページを表示したい

そして、前回のコードを修正すること約半日、次のコードが完成した。

```Node.js
const AWS = require('aws-sdk');
const documentClient = new AWS.DynamoDB.DocumentClient();
const util = require('util');

exports.handler = function(event, context, callback) {

    console.log(util.inspect(event, false, null)); // 為念
    
    // リダイレクト先はリファラから取得
    var input_referer = (event.headers.referer) ?
        event.headers.referer :
        ((event.headers.Referer) ?
            event.headers.Referer :
            process.env.baseUrl);
    
    var input_email = (event.body.email) ? event.body.email : "*";
    var input_name  = (event.body.name)  ? event.body.name  : "*";
    var input_phone = (event.body.phone) ? event.body.phone : "*";

    var params = {
        TableName: 'masklottery_entries',
        Item: {
            'email': input_email,
            'name' : input_name,
            'phone': input_phone
        },
        ConditionExpression: 'attribute_not_exists(email)'
    };

    var response = {
        "headers": {
            "Content-Type": "application/json",
            "Location": ""  // Redirect URL
        },
        "body": ""
    };

    documentClient.put(params, function(err, data) {
        if (err) {
            console.log("***** failure *****");
            console.log(err);
            if (err.code == "ConditionalCheckFailedException") {
                // キー重複で登録できなかった
                response.headers.Location = input_referer.replace(/\/$/, "") + process.env.dupErrUrl;
            } else {
                // キー重複以外の理由で登録できなかった
                response.headers.Location = input_referer.replace(/\/$/, "") + process.env.failureUrl;
            }
            response.body = JSON.stringify(err);
            // Error 返却の方が良いらしいがリダイレクト制御が面倒なので
            callback(null, response);
        } else {
            // データ登録できた
            console.log("***** success *****");
            console.log(data);
            response.headers.Location = input_referer.replace(/\/$/, "") + process.env.successUrl;
            response.body = JSON.stringify(data);
            callback(null, response);
        }
    });
};
```

ようやくのことで、マッピングに取り掛かる。

まず、リダイレクトURL はリクエストのリファラから取得することにしたので、
メソッドリクエストに `Referer` を準備。

|![capture_000051.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/62c18bdf-fb8f-ac66-f585-449118e7aa50.png)|
|:--|

次に、メソッドレスポンスに 301 を定義して、リダイレクトURL を差し込む `Location` を準備。

|![capture_000047.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/e10553dd-26f9-532f-3aa8-e40518e5d423.png)|
|:--|

Lambda からのレスポンスに仕込んだ Location をレスポンスヘッダの Location にマッピング。

|![capture_000048.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/450f0e57-83c8-7ca0-29fe-fa8a77f4645c.png)|
|:--|

これで Lambda の結果に応じたリダイレクト先 URL を HTML へのレスポンスヘッダに仕込むことができた。

### HTML 修正

まずは API Gateway の URL を `<form>` に仕込んであげればおっけー。

次に、キー重複で登録できなかった場合のエラー表示を仕込む。

```HTML
<div id="errors" class="errors">
    <p>既に同一メールアドレスによる応募が完了しています。</p>
    <p>別のメールアドレスを入力ください。</p>
</div>
```

Lambda のレスポンスで表示を切り替えるようにして

```HTML
<script>
    window.onload = function () {
        var params = (new URL(document.location)).searchParams;
        var target = document.getElementById("errors");
        if (params.get("errFlg") != null) {
            target.style.display = "block";
        } else {
            target.style.display = "none";
        }
    }
</script>
```

S3 にアップロードしたら、CloudFront に以前のキャッシュが残っているので、Invalidation でキャッシュクリア。

|![capture_000049.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/417fa01d-2899-e7a1-b3dc-f37f2d36225a.png)|
|:--|

テストデータにはアンダースローな彼に登場してもらって、さて？

|![check1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/fc8aa861-759e-f303-5d86-acb2d161c3d8.png)|
|:--|

よし。

|![check2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/ca65e583-16c2-70d9-1c8b-76fb58368ec9.png)|
|:--|

再度エントリーで想定通りエラー。

|![check3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/7eaf25d9-f8a0-1a50-1eb2-805c40a7919f.png)|
|:--|

DynamoDB も確認。

|![capture_000050.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/f074cb8e-0f2d-f4fa-247e-42f0f5563b43.png)|
|:--|

感無量。
ここに明訓高校の四天王が！

## 最終的な動作確認

既に動作確認は終わっているわけで。
しかし、要件の「1 日で 500 万件程度の申し込み」の件、忘れてたわけで。
最終的な、とはそういうことなわけで。

### 念の為、計算

気を取り直して計算してみましょー。

> 500 万件 ÷ 86400 秒(=24 時間) = 57.87 件/秒

秒間 60 件の申込リクエストをさばければ問題なさそうですねっ。
ただし、現実には単位時間あたりのリクエストが均等にやってくることはないわけで。
わかってますよ、ですが、ココに至っては均等になる、そういうことにしておきませんか。
どっちにしても現在の DynamoDB の設定はコレですので、そもそも秒間 60 件を捌くのは無理なわけで。

|<img width="400" alt="capture_000070.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/eee42440-c0ac-a7f6-1ad0-2df08072bdd6.png">|
|:--|

時間耐久な限界を見極めるつもりもないので、5 分程度の実行時間で勘弁してください、本当に。

つまり、こうなります。

> 5 分で 1500 件 (5 件/秒 × 300 秒(=3 分) = 1500 件)

コレを達成できればおっけーとする、します、するの！
とはいえ、手作業でのテスト実施は無理ですな。

### JMeter

テスト実行は僕ではなく JMeter に頑張ってもらいましょー。

POST パラメータ中のメールアドレスをランダム文字列で与えるようにして、

|![capture_000072.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/f1cbbefe-b5dd-2936-fdc0-d975f0f0bd3e.png)|
|:--|

JMeter の動作確認用に秒間 5 件のリクエストを 15 秒間、で設定して、

|![capture_000073.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/c620e0c4-5cf7-3192-14b0-e7873fe35abb.png)|
|:--|

はい、実行。
リダイレクトしていることが確認できる。

|![capture_000074.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/6eaf6f1d-6ead-51f2-5937-736e76a77c3e.png)|
|:--|

CloudFront のキャッシュにもヒット。

|![capture_000075.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/421f6fca-8329-526f-8f52-20fae647f8c8.png)|
|:--|

気になるデータ量は約 `1600 Byte` ね。

|![capture_000076.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/cc52a9fd-adc4-4de4-4ed2-d934ae403937.png)|
|:--|

さて、公式のテストポリシーを確認すると、次に該当する場合はテスト申請が必要とのこと。

> 全体として 1 分を越えて継続する、1 Gbps (10 億ビット/秒) または 1 Gpps (10 億パケット/秒) を超えるもの。
> 不正なまたは悪意のあると見られるトラフィックが生成されるもの。
> 予想されるテストのターゲット以外のエンティティ (ルーティングや共有サービスインフラストラクチャ) に対して影響を与える可能性が存在するもの。

計算してみる。

> 0.000715 Gbps (1600 Byte/件 ×  8 bit/Byte × 60 件/秒 = 768000 bit/秒)

てんで話にならないわけで。

申請不要、と安心できたトコロで、秒間 5 件のリクエストを 300 秒間、で設定。

|![capture_000077.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/fa60eae6-5561-fe37-cf33-f4481b203366.png)|
|:--|

実行結果を確認してみる。

|![capture_000078.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1164835/9adf4f8f-3b82-c044-a297-1d355da14bb1.png)|
|:--|

スループットは `5.0 /sec` で想定通り。
最遅でも `1416` ミリ秒、リクエストの99%は `609` ミリ秒以下という結果になったわけで。 

つまり、先に挙げたコチラを達成した、と言って良い気がしてきませんか？

> 5 分で 1500 件 (5 件/秒 × 300 秒(=3 分) = 1500 件)


ということで今回の全ての目的を達した。


株式会社メソドロジック
三嶋　圭　@k-mishima


# 参考
[Amazon S3 サーバーアクセスログ - Amazon Simple Storage Service](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/ServerLogs.html)
[標準ログ (アクセスログ) の設定および使用 - Amazon CloudFront](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html) 
[Amazon CloudWatch の AWS Lambda ログへのアクセス - AWS Lambda](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/monitoring-cloudwatchlogs.html)
[API Gateway での CloudWatch による REST API のログの設定 - Amazon API Gateway](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/set-up-logging.html)
[AWSサービスのログまとめ - Qiita](https://qiita.com/hssh2_bin/items/6d2d7cfe7c45412f1634)
[[AWS] Lambda + API Gatewayでサーバレスを始める ２ - Qiita](https://qiita.com/naoki_koreeda/items/5351f8ab803db655b0b4)
[テストポリシー - Amazon EC2 | AWS](https://aws.amazon.com/jp/ec2/testing/)