AWS CloudWatch Synthetics Monitoring を CloudFormation で設定する

参考) [Using Synthetic Monitoring - Amazon CloudWatch Documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries.html)

## リソース

管理コンソールから CloudWatch Synthetics の Canary を作成すると、以下のリソースが展開される。
これを参考にして CloudFormation で各リソースを作成する。

![構成](https://hirose-public.s3-ap-northeast-1.amazonaws.com/qiita/2020/06/25/9a6cbfbd-25f8-49f7-8252-b2c8ad602926.svg)

## CloudFormation テンプレート

### Parameters

以下をパラメタ化した。

- CanaryName : Canary の名前。配下にいろいろリソースを作る都合上からか、Canary の名前は `[a-z\-_]{,21}` と厳しい制約になっている。
- BucketName : 結果の保存先とするバケット名
- TargetURL : 監視対象の URL

参考) [Parameters - AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html)

```yaml
Parameters:
  CanaryName:
    Description: Synthetics Canary name to create
    Type: String
  BucketName:
    Description: Bucket name to save Synthetics results
    Type: String
  TargetURL:
    Description: Target URL to monitor
    Type: String
```

### Service Role

自動で作られる Role を参考に作成した。必要な permissions は Canary リソースに指定する [ExecutionRoleArn](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-synthetics-canary.html#cfn-synthetics-canary-executionrolearn) の説明に書かれている。

- S3 バケットに結果を書き込むための s3:PutObject と s3::GetBucketLocation、あと s3:ListAllMyBuckets
- Lambda から実行ログを出力するための logs:CreateLogStream, logs:PutLogEvents, logs:CreateLogGroup
- CloudWatch Metrics に送信するための cloudwatch:PutMetricData

参考) [AWS::IAM::Role - AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html)

```yaml
Resources:
  SyntheticsIamRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: "sts:AssumeRole"
      Policies:
        - PolicyName: "SyntheticsPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetBucketLocation
                Resource:
                  - !Sub "${SyntheticsResultsBucket.Arn}/*"
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - logs:CreateLogGroup
                Resource:
                  - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/cwsyn-${CanaryName}-*"
              - Effect: Allow
                Action:
                  - s3:ListAllMyBuckets
                Resource: "*"
              - Effect: Allow
                Resource: "*"
                Action: cloudwatch:PutMetricData
                Condition:
                  StringEquals:
                    cloudwatch:namespace: CloudWatchSynthetics
```

### S3 Bucket

参考) [AWS::S3::Bucket - AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html)

```yaml
  SyntheticsResultsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
```

### Synthetics Canary

参考) [AWS::Synthetics::Canary - AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-synthetics-canary.html)

上で生成した IAM Role、保存先バケット名を指定して作る。
Canary を生成すると Lambda 関数も自動的に生成される。

ここで設定しているスクリプトは Blueprint として用意されている Heatbeat monitoring そのまま持ってきたもの。スクリプトは `Code` に直接記述するほか、S3 に保存しておいて場所を指定することができる。[SAM](https://aws.amazon.com/serverless/sam/) で管理できると良いと思うのだけど...

```yaml
  Canary:
    Type: AWS::Synthetics::Canary
    Properties:
      Name: !Ref CanaryName
      ExecutionRoleArn: !GetAtt SyntheticsIamRole.Arn
      Code:
        Handler: pageLoadBlueprint.handler
        Script: !Sub >
          var synthetics = require('Synthetics');
          const log = require('SyntheticsLogger');
          const pageLoadBlueprint = async function () {
            const URL = "${TargetURL}";
            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();
          };
      ArtifactS3Location: !Sub "s3://${SyntheticsResultsBucket}"
      RuntimeVersion: syn-1.0
      Schedule:
        DurationInSeconds: 0
        Expression: "rate(5 minutes)"
      RunConfig:
        TimeoutInSeconds: 60
      FailureRetentionPeriod: 31
      SuccessRetentionPeriod: 31
      StartCanaryAfterCreation: true
      DeleteLambdaResourcesOnCanaryDeletion: true
```

### SNS Topic

Alarm の通知先トピック

参考) [AWS::SNS::Topic - AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sns-topic.html)

```yaml
  AlarmTopic:
    Type: AWS::SNS::Topic
```

### CloudWatch Alarm

Canary の実行結果は固定名の Metrics に送信されるので、それに対して Alarm を設定する。

参考) [AWS::CloudWatch::Alarm - AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cw-alarm.html)

```yaml
  CanaryAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      MetricName: "SuccessPercent"
      Namespace: "CloudWatchSynthetics"
      Dimensions:
        - Name: CanaryName
          Value: !Ref CanaryName
      Statistic: Average
      Period: 300
      EvaluationPeriods: 1
      Threshold: 90
      AlarmActions:
        - Ref: AlarmTopic
      OKActions:
        - Ref: AlarmTopic
      InsufficientDataActions:
        - Ref: AlarmTopic
      TreatMissingData: breaching
      ComparisonOperator: LessThanThreshold
```

## 注意点

- ~~Canary を生成すると `cwsyn-(Canary 名)-xxxx` という Lambda が生成されるが、スタックを削除しても Lambda までは削除されないようなので、手動で削除する必要がある。~~
  →`DeleteLambdaResourcesOnCanaryDeletion` を設定することで削除されるようになりました。