簡単Webサイトホスティング③特定のURLを定期的にモニタリングするCloudFormationテンプレート

# はじめに

Amazon Web Services（AWS）が提供する、``Amazon CloudFront`` や ``Amazon S3`` と呼ばれるサービスを組み合わせることで、 **HTMLやJavaScript、画像、ビデオなどで構成される静的Webサイトの配信基盤** を[**安価に構築**](https://aws.amazon.com/jp/websites/)することができます。本記事では、リソースのセットアップを自動で行うことのできる、``AWS CloudFormation`` を用いることで、これらの配信基盤を **ミスなく迅速に構築** する手順をご説明します。なお、今回使用する CloudFormation テンプレートは、以下の GitHub リポジトリで公開しています。

+ [AWSCloudFormationTemplates/static-website-hosting-with-ssl](https://github.com/eijikominami/aws-cloudformation-templates/blob/master/static-website-hosting-with-ssl/README_JP.md)

# TL;DR

以下の ``CloudFormation`` テンプレートを実行することで、 **静的Webサイトのホスティング基盤を迅速かつお手軽** に実現します。下にあるボタンをクリックすると、自身のAWSアカウント（Asia Pacific Tokyo - ap-northeast-1）で、この ``CloudFormation`` テンプレートを実行することが可能となります。

[![cloudformation-launch-stack](https://raw.githubusercontent.com/eijikominami/aws-cloudformation-templates/master/images/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-northeast-1#/stacks/create/review?stackName=StaticWebsiteHosting&templateURL=https://eijikominami.s3-ap-northeast-1.amazonaws.com/aws-cloudformation-templates/static-website-hosting-with-ssl/template.yaml) 

作成されるAWSリソース全体のアーキテクチャ図は、[**過去の記事**](https://qiita.com/eijikominami/items/13ebbb2c44a62c71aee7)をご覧ください。このうち本記事では、以下のリソースに焦点を当ててご説明します。

![architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51599/fef0b414-8dee-fe2e-25a6-5171eb886477.png)


# 特定のURLのモニタリング

システムを運用するにあたって **モニタリングは不可欠** です。モニタリングを行うことで、 **システムの健全性と可用性を追求することが可能** となります。このうち、「ブラックボックスモニタリング」や「フロントエンド監視」「外形監視」と呼ばれる、 **ユーザが目にする外部的な振る舞いを確認することが特に重要である** とされています。なぜなら、個別のコンポーネントの正常性ではなく、アプリケーションが正常に動いているかどうかが、ユーザにとって最も重要となるからです。

これを実現させるために、AWSには **Amazon CloudWatch Synthetics** というサービスが用意されており、 ``Canary`` と呼ばれるスクリプトを用いて特定のURLを定期的に監視することが可能です。 ``Canary`` は、Lambda上で実行される Node.isスクリプトで、[**指定したURLの可用性やレイテンシーの確認して、 UI のスクリーンショットを保存**](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries.html)することができます。また、CloudWatch カスタムメトリクスを生成するので、可用性やレイテンシーなどの数値を継続して追跡することが可能です。

## IAM Role

まず、 ``Amazon CloudWatch Synthetics`` に付与する IAM Role を設定します。このIAM Roleは、 **S3バケットに対する書き込み権限** や **CloudWatch Logsに対するログの書き込み権限** 、 **CloudWatch に対するメトリクスの送信権限** などをCloudWatch Syntheticsに付与します。

```yaml
Parameters:
  CanaryName:
    Type: String
    AllowedPattern: .+
    Description: CloudWatch Synthetics Canary Name [required]

Resources:
  IAMRoleForSynthetics:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: 'sts:AssumeRole'
      Description: A role required for CloudWatch Synthetics to access S3 and CloudWatch Logs.
      Policies:
        - PolicyName: !Sub 'CloudWatchLogsSynthetics-${CanaryName}-${AWS::Region}'
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 's3:PutObject'
                  - 's3:GetBucketLocation'
                Resource: !Sub arn:aws:s3:::${S3ForSynthetics}/*
              - Effect: Allow
                Action:
                  - 'logs:CreateLogGroup'
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/cwsyn-${CanaryName}-*
              - Effect: Allow
                Action:
                  - 's3:ListAllMyBuckets'
                Resource: '*'
              - Effect: Allow
                Action:
                  - 'cloudwatch:PutMetricData'
                Resource: '*'
                Condition:
                  StringEquals:
                    cloudwatch:namespace: CloudWatchSynthetics
      RoleName: !Sub 'SyntheticsRole-${CanaryName}-${AWS::Region}'
```

## Amazon S3

次に、 ``Amazon CloudWatch Synthetics`` が取得/作成したログやスクリーンショットを保存するS3バケットを作成します。

```yaml
Parameters:
  CanaryName:
    Type: String
    AllowedPattern: .+
    Description: CloudWatch Synthetics Canary Name [required]

Resources:
  S3ForSynthetics:
    Type: 'AWS::S3::Bucket'
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
    Properties:
      BucketName: !Sub synthetics-${CanaryName}-${AWS::Region}-${AWS::AccountId}
      LifecycleConfiguration:
        Rules:
          - Id: ExpirationInDays
            ExpirationInDays: 60
            Status: Enabled
      PublicAccessBlockConfiguration: 
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
```

## Amazon CloudWatch Synthetics

CloudWatch Synthetics の Canary を作成します。Canaryの作成自体は CloudWatch Synthetics 上で行われますが、 通常のLambda関数と同様に **Lambdaサービス上で新規のLambda関数が作成** されます。下記のテンプレートでは、 **指定したドメインおよびパスに対してリクエストを送信し、スクリーンショットを取得する関数** がLambda上に作成され、 **5分に1回の頻度でこの関数が自動的に実行** されます。また、先ほど作成したIAM RoleおよびS3バケットの紐づけも行なっているため、**Canaryの実行結果が S3バケット、CloudWatch Logs、CloudWatch メトリクスなどに書き込まれます** 。

なおCanaryを作成する際には、その名前を指定する必要がありますが、 **数字と小文字、一部の記号のみを使用して21文字以内でなければならない** と文字制限が厳しいため、テンプレート上で **Canaryの命名規則をを他のリソース名と共通化する場合は注意が必要** です。

```yaml
Parameters:
  CanaryName:
    Type: String
    AllowedPattern: .+
    Description: CloudWatch Synthetics Canary Name [required]
  DomainName:
    Type: String
    AllowedPattern: .+
    Description: The watched domain name [required]
  WatchedPagePath:
    Type: String
    Default: /index.html
    AllowedPattern: .+
    Description: The watched page path [required]

Resources:
  Synthetics:
    Type: 'AWS::Synthetics::Canary'
    Properties: 
      ArtifactS3Location: !Sub s3://${S3ForSynthetics}
      Code: 
        Handler: pageLoadBlueprint.handler
        Script: !Sub >
          var synthetics = require('Synthetics');
          const log = require('SyntheticsLogger');

          const pageLoadBlueprint = async function () {

              // INSERT URL here
              const URL = "https://${DomainName}${WatchedPagePath}";

              let page = await synthetics.getPage();
              const response = await page.goto(URL, {waitUntil: 'domcontentloaded', timeout: 30000});
              //Wait for page to render.
              //Increase or decrease wait time based on endpoint being monitored.
              await page.waitFor(15000);
              await synthetics.takeScreenshot('loaded', 'loaded');
              let pageTitle = await page.title();
              log.info('Page title: ' + pageTitle);
              if (response.status() !== 200) {
                  throw "Failed to load page!";
              }
          };
          
          exports.handler = async () => {
            return await pageLoadBlueprint();
          };
      ExecutionRoleArn: !GetAtt IAMRoleForSynthetics.Arn
      FailureRetentionPeriod: 31
      Name: !Ref CanaryName
      RuntimeVersion: syn-nodejs-2.0
      Schedule: 
        DurationInSeconds: '0'
        Expression: rate(5 minutes) 
      StartCanaryAfterCreation: true
      SuccessRetentionPeriod: 31
```

## CloudWatch Alarm

上記のテンプレートのみで **CloudWatch Synthetics の設定は全て完了** し、5分に1回の頻度でモニタリングを開始します。この外形監視がなんらかの理由で失敗した際に通知を受け取ることができるようにするために、最後に CloudWatch Alarm を作成します。このアラームは、 **Canaryの成功率が90%を下回った場合にAmazon SNSに対して通知を発行** します。

```yaml
Resources:
  CloudWatchAlarmSynthetics:
    Type: 'AWS::CloudWatch::Alarm'
    Properties:
      ActionsEnabled: true
      AlarmActions:
        - !Ref SNSForAlertArn
      AlarmDescription: !Sub 'CloudWatch Synthetics による定期モニタリングで、${DomainName}${WatchedPagePath} への GET の *失敗率が上昇* しています。このエラーが継続する場合は、 *トラフィックの増大* もしくは *内部処理に異常が発生* している可能性があります。'
      AlarmName: !Sub 'Notice-${CanaryName}-Synthetics-AccessError'
      ComparisonOperator: LessThanThreshold
      DatapointsToAlarm: 1
      Dimensions:
        - Name: CanaryName
          Value: !Ref CanaryName
      EvaluationPeriods: 1
      MetricName: SuccessPercent
      Namespace: CloudWatchSynthetics
      OKActions:
        - !Ref SNSForAlertArn
      Period: 300
      Statistic: Average
      Threshold: 90
      TreatMissingData: notBreaching
```

以上で、特定のURLに対して定期的にアクセスを行い、このエンドポイントの正常性を確認することのできる、CloudWatch Synthetics の Canary を設定することができました。これまでの記事で作成した **CloudFront の URL をこの Canary の監視先に指定することで、 CloudFront が正常に動作しているかどうかを継続的に監視することが可能** となります。

# 関連リンク
1. [**ワンクリックで配信基盤を構築** - CloudFormation を用いて簡単Webサイトホスティング](https://qiita.com/eijikominami/items/13ebbb2c44a62c71aee7)
2. [**CloudFrontにWAFをアタッチ** - CloudFormation を用いて簡単Webサイトホスティング](https://qiita.com/eijikominami/items/dffa11d456be3d6a88d2)
3. **特定のURLを定期的にモニタリングする** - CloudFormation を用いて簡単Webサイトホスティング
4. [**CloudFrontのリアルタイムログをKibanaで可視化する** - CloudFormation を用いて簡単Webサイトホスティング](https://qiita.com/eijikominami/items/1bcde04897b5136e4122)