constructor()

in source/lib/deployment-helper/canary_alarm.ts [45:416]


  constructor(scope: cdk.Construct, id: string, props: iCanaryProps) {
    super(scope, id, props);
    //=========================================================================
    // PARAMETERS
    //=========================================================================
    /**
     * @description name of the CodeCommit repository for the application to be
     * monitored
     * @type {cdk.CfnParameter}
     */
    const paramRepoName = new cdk.CfnParameter(this, 'RepoName', {
      description: "Name of CodeCommit repository for the application.",
      type: "String"
    });

    /**
     * @description name of the application
     * @type {cdk.CfnParameter}
     */
    const paramAppName = new cdk.CfnParameter(this, 'AppName', {
      description: "Name of the application that canary monitors.",
      type: "String"
    });

    /**
     * @description url to monitor via a synthetic transaction
     * @type {cdk.CfnParameter}
     */
    const paramCanaryUrl = new cdk.CfnParameter(this, 'URL', {
      description: "Application or endpoint URL you want to monitor with the canary (for example, https://www.example.com). The canary will check the site every 5 minutes.",
      type: "String"
    });

    /**
     * @description name of the canary (synthetic transaction)
     * @type {cdk.CfnParameter}
     */
    const paramCanaryName = new cdk.CfnParameter(this, 'CanaryName', {
      description: "Name of your Canary (new or existing) so users can easily understand what it is in the console.",
      type: "String",
      default: "mycanary",
      allowedPattern: "^[0-9a-z_\-]+$"
    });

    /**
     * @description milliseconds to wait for a response
     * @type {cdk.CfnParameter}
     */
    const paramResponseThresh = new cdk.CfnParameter(this, 'ResponseThreshold', {
      description: "Number of milliseconds to wait for a url response before considering the canary failed.",
      type: "String",
      default: "15000",
      allowedPattern: "^[0-9]+$"
    });

    /**
     * @description canary interval, in minutes
     * @type {cdk.CfnParameter}
     */
    const paramCanaryInterval = new cdk.CfnParameter(this, 'Interval', {
      description: "Interval, in minutes.",
      type: "String",
      default: "5"
    });

    /**
     * @description threshold for SuccessPercent
     * @type {cdk.CfnParameter}
     */
    const paramThreshold = new cdk.CfnParameter(this, 'PercentThreshold', {
      description: "Threshold for Success (percentage). Any value less than this value will result in an alarm being triggered.",
      type: "Number",
      default: 100
    });

    /**
     * @description number of eval periods to compare to threshold
     * @type {cdk.CfnParameter}
     */
    const paramEvalPeriods = new cdk.CfnParameter(this, 'EvalPeriods', {
      description: "Number of periods to compare to the threshold.",
      type: "Number",
      default: 1
    });

    /**
     * @description number of periods before alarming when below threshold
     * @type {cdk.CfnParameter}
     */
    const paramAlarmPeriods = new cdk.CfnParameter(this, 'AlarmPeriods', {
      description: "Number of collection periods over which the threshold is exceeded before alarming. \
        This value must be less than or equal to Evaluation Periods",
      type: "Number",
      default: 1
    });

    /**
     * @description should artifact bucket be created?
     * @type {cdk.CfnParameter}
     */
    const paramCreateBucket = new cdk.CfnParameter(this, 'CreateBucket', {
      description: "Canaries store artifacts in an S3 bucket. Should this \
        canary create a new bucket? Enter the bucket name (new or existing) below.",
      type: "String",
      default: "No",
      allowedValues: [
        "No",
        "Yes",
      ]
    });

    /**
     * @description should canary be created?
     * @type {cdk.CfnParameter}
     */
    const paramCreateCanary = new cdk.CfnParameter(this, 'CreateCanary', {
      description: "Should a new canary be created? If yes, a canary with the name specified above will be created. If no, just skip the rest of Canary Configuration and move on to Application Monitoring.",
      type: "String",
      default: "Yes",
      allowedValues: [
        "No",
        "Yes",
      ]
    });

    /**
     * @description bucket to store canary artifacts. Can be an existing bucket.
     * @type {cdk.CfnParameter}
     */
    const paramBucketName = new cdk.CfnParameter(this, 'BucketName', {
      description: "Name of the bucket (new or existing) for logging \
        Canary artifacts. Each Canary will log to this bucket to a different \
        prefix.",
      type: "String"
    });

    const parameterMetaData = {
      "AWS::CloudFormation::Interface": {
        "ParameterGroups": [
          {
            "Label": {
              "default": "Canary Configuration"
            },
            "Parameters": [
              "CanaryName",
              "CreateCanary",
              "URL",
              "Interval",
              "ResponseThreshold",
              "CreateBucket",
              "BucketName"
            ]
          },
          {
            "Label": {
              "default": "Application Monitoring"
            },
            "Parameters": [
              "AppName",
              "RepoName",
            ]
          },
          {
            "Label": {
              "default": "Alarm Configuration"
            },
            "Parameters": [
              "PercentThreshold",
              "EvalPeriods",
              "AlarmPeriods"
            ]
          }
        ],
        "ParameterLabels": {
          "URL": {
            "default": "URL"
          },
          "CanaryName": {
            "default": "Canary Name"
          },
          "CreateCanary": {
            "default": "Create New Canary?"
          },
          "CanaryInterval": {
            "default": "Canary Interval"
          },
          "CreateBucket": {
            "default": "Create Artifact Bucket?"
          },
          "BucketName": {
            "default": "Artifact Bucket Name"
          },
          "PercentThreshold": {
            "default": "% Success Threshold (<)"
          },
          "ResponseThreshold": {
            "default": "Response Threshold (ms)"
          },
          "EvalPeriods": {
            "default": "Evaluation Periods"
          },
          "AlarmPeriods": {
            "default": "Alarm Periods"
          },
          "AppName": {
            "default": "Application Name"
          },
          "RepoName": {
            "default": "Repository Name"
          }
        }
      }
    };
    this.templateOptions.metadata = parameterMetaData;

    //=========================================================================
    // CONDITIONS
    //=========================================================================
    const cndCanaryBucket = new cdk.CfnCondition(this, "CanaryBucketCondition", {
      expression: cdk.Fn.conditionEquals(paramCreateBucket, "Yes")
    });

    const cndCreateCanary = new cdk.CfnCondition(this, "CreateCanaryCondition", {
      expression: cdk.Fn.conditionEquals(paramCreateCanary, "Yes")
    });

    const cndCreateBucket = new cdk.CfnCondition(this, "CreateBucketCondition", {
      expression: cdk.Fn.conditionAnd(cndCanaryBucket, cndCreateCanary)
    });

    //=========================================================================
    // RESOURCES
    //=========================================================================
    // ArtifactBucket
    // --------------
    let ArtifactBucketCfg = {
      encryption: s3.BucketEncryption.S3_MANAGED,
      versioned: false,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
      accessControl: s3.BucketAccessControl.LOG_DELIVERY_WRITE,
      bucketName: paramBucketName.valueAsString
    } as s3.BucketProps;

    const bktArtifact = new s3.Bucket(this, "ArtifactBucket", ArtifactBucketCfg);

    applySecureBucketPolicy(bktArtifact);
    cdk.Tags.of(bktArtifact).add('Name', props.solutionName + ' Canary Artifacts')
    {
      let childToMod = bktArtifact.node.defaultChild as s3.CfnBucket;
      childToMod.addPropertyDeletionOverride('LifecycleConfiguration.Rules')
      childToMod.cfnOptions.condition = cndCreateBucket;
    }
    {
      let intermediate = bktArtifact.node.findChild('Policy') as s3.BucketPolicy;
      let childToMod = intermediate.node.defaultChild as s3.CfnBucketPolicy;
      childToMod.cfnOptions.condition = cndCreateBucket;
    }

    const bktArtifactResource = bktArtifact.node.findChild('Resource') as s3.CfnBucket;

    bktArtifactResource.cfnOptions.metadata = {
         cfn_nag: {
            rules_to_suppress: [{
                id: 'W35',
                reason: 'This bucket is used by canary to store artifacts and no access loging bucket is needed.'
            }]
        }
    };

    // Get iBucket reference - works whether bucket was created or not
    //
    let artifactBucket: s3.IBucket = s3.Bucket.fromBucketAttributes(this, "ArtifactiBucket", {
        bucketName: paramBucketName.valueAsString
    });

    const canaryCode = [
      "var synthetics = require('Synthetics');",
      "const log = require('SyntheticsLogger');",
      "",
      "const pageLoadBlueprint = async function () {",
      "",
      "  const URL = \"" + paramCanaryUrl.valueAsString + "\";",
      "",
      "  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(" + paramResponseThresh.valueAsString + ");",
      "  // This will take a screenshot that will be included in test output artifacts",
      "  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();",
      "};"
    ]

    // Note: undate SYNTHETICS_1_0 to SYNTHETICS_2_0 as soon as CDK supports it
    const canary = new synth.Canary(this, 'HttpCanary', {
      canaryName: paramCanaryName.valueAsString,
      schedule: synth.Schedule.rate(cdk.Duration.minutes(5)),
      test: synth.Test.custom({
        code: synth.Code.fromInline(canaryCode.join("\n")),
        handler: 'index.handler'
      }),
      artifactsBucketLocation: {
        bucket: artifactBucket,
        prefix: cdk.Aws.STACK_NAME
      },
      runtime: synth.Runtime.SYNTHETICS_1_0
    });
    (canary.node.defaultChild as synth.CfnCanary).addPropertyOverride(
      "Schedule", {
          "DurationInSeconds": "0",
          "Expression": "rate(" + paramCanaryInterval.valueAsString + " minutes)"
      }
    );
    (canary.node.defaultChild as synth.CfnCanary).addPropertyOverride(
      "RuntimeVersion", "syn-nodejs-puppeteer-3.1"
    );

    const canaryServiceRoleResource = canary.node.findChild('ServiceRole').node.findChild('Resource') as iam.CfnRole;

    canaryServiceRoleResource.cfnOptions.metadata = {
         cfn_nag: {
            rules_to_suppress: [{
                id: 'W11',
                reason: 'Resource * is required by the canary service role.'
            }]
        }
    };

    let refCanary = canary.node.defaultChild as synth.CfnCanary
    refCanary.cfnOptions.condition = cndCreateCanary;

    // Alarm State - the canary is "failed" when...
    // --------------------------------------------
    const alarmName = props.solutionId + '-[' + paramAppName.valueAsString + ']-[' + paramRepoName.valueAsString + ']-MTTR'
    new AlarmConstruct(this, "Alarm", {
      canaryName: paramCanaryName.valueAsString,
      alarmName: alarmName,
      evalPeriods: paramEvalPeriods.valueAsNumber,
      alarmPeriods: paramAlarmPeriods.valueAsNumber,
      threshold: paramThreshold.valueAsNumber
    });


    //=========================================================================
    // OUTPUTS
    //=========================================================================
    new CfnOutput(this, 'SolutionVersion', {
      value: props.solutionVersion?props.solutionVersion:'',
      description: 'Version for AWS DevOps Monitoring Dashboard Solution'
    })

    new CfnOutput(this, 'Canary Name', {
      value: paramCanaryName.valueAsString,
      description: 'Name of the Canary'
    })

    new CfnOutput(this, 'Alarm Name', {
      value: alarmName,
      description: 'Name of the Canary Alarm'
    })

  }