UNPKG

aws-cdk-lib

Version:

Version 2 of the AWS Cloud Development Kit library

348 lines (273 loc) 14.3 kB
# Amazon CloudWatch Synthetics Construct Library Amazon CloudWatch Synthetics allow you to monitor your application by generating **synthetic** traffic. The traffic is produced by a **canary**: a configurable script that runs on a schedule. You configure the canary script to follow the same routes and perform the same actions as a user, which allows you to continually verify your user experience even when you don't have any traffic on your applications. ## Canary To illustrate how to use a canary, assume your application defines the following endpoint: ```console % curl "https://api.example.com/user/books/topbook/" The Hitchhikers Guide to the Galaxy ``` The below code defines a canary that will hit the `books/topbook` endpoint every 5 minutes: ```ts const canary = new synthetics.Canary(this, 'MyCanary', { schedule: synthetics.Schedule.rate(Duration.minutes(5)), test: synthetics.Test.custom({ code: synthetics.Code.fromAsset(path.join(__dirname, 'canary')), handler: 'index.handler', }), runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_6_2, environmentVariables: { stage: 'prod', }, }); ``` The following is an example of an `index.js` file which exports the `handler` function: ```js const synthetics = require('Synthetics'); const log = require('SyntheticsLogger'); const pageLoadBlueprint = async function () { // Configure the stage of the API using environment variables const url = `https://api.example.com/${process.env.stage}/user/books/topbook/`; const 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); // This will take a screenshot that will be included in test output artifacts. await synthetics.takeScreenshot('loaded', 'loaded'); const 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:** The function **must** be called `handler`. The canary will automatically produce a CloudWatch Dashboard: ![UI Screenshot](images/ui-screenshot.png) The Canary code will be executed in a lambda function created by Synthetics on your behalf. The Lambda function includes a custom [runtime](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_Library.html) provided by Synthetics. The provided runtime includes a variety of handy tools such as [Puppeteer](https://www.npmjs.com/package/puppeteer-core) (for nodejs based one) and Chromium. To learn more about Synthetics capabilities, check out the [docs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries.html). ### Canary Schedule You can specify the schedule on which a canary runs by providing a [`Schedule`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-synthetics.Schedule.html) object to the `schedule` property. Configure a run rate of up to 60 minutes with `Schedule.rate`: ```ts const schedule = synthetics.Schedule.rate(Duration.minutes(5)); // Runs every 5 minutes. ``` You can also specify a [cron expression](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_cron.html) with `Schedule.cron`: ```ts const schedule = synthetics.Schedule.cron({ hour: '0,8,16', // Run at 12am, 8am, 4pm UTC every day }); ``` If you want the canary to run just once upon deployment, you can use `Schedule.once()`. ### Active Tracing You can choose to enable active AWS X-Ray tracing on canaries that use the `syn-nodejs-2.0` or later runtime by setting `activeTracing` to `true`. With tracing enabled, traces are sent for all calls made by the canary that use the browser, the AWS SDK, or HTTP or HTTPS modules. For more information, see [Canaries and X-Ray tracing](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_tracing.html). ```ts const canary = new synthetics.Canary(this, 'MyCanary', { schedule: synthetics.Schedule.rate(Duration.minutes(5)), test: synthetics.Test.custom({ code: synthetics.Code.fromAsset(path.join(__dirname, 'canary')), handler: 'index.handler', }), runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_6_2, activeTracing: true, // active tracing enabled }); ``` ### Memory You can set the maximum amount of memory the canary can use while running with the `memory` property. ```ts import * as cdk from "aws-cdk-lib"; const canary = new synthetics.Canary(this, 'MyCanary', { schedule: synthetics.Schedule.rate(Duration.minutes(5)), test: synthetics.Test.custom({ code: synthetics.Code.fromAsset(path.join(__dirname, 'canary')), handler: 'index.handler', }), runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_6_2, memory: cdk.Size.mebibytes(1024), // 1024 MiB }); ``` ### Timeout You can set how long the canary is allowed to run before it must stop with the `timeout` property. ```ts import * as cdk from "aws-cdk-lib"; const canary = new synthetics.Canary(this, 'MyCanary', { schedule: synthetics.Schedule.rate(Duration.minutes(5)), test: synthetics.Test.custom({ code: synthetics.Code.fromAsset(path.join(__dirname, 'canary')), handler: 'index.handler', }), runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_6_2, timeout: cdk.Duration.seconds(60), // 60 seconds }); ``` ### Deleting underlying resources on canary deletion When you delete a lambda, the following underlying resources are isolated in your AWS account: - Lambda Function that runs your canary script - S3 Bucket for artifact storage - IAM roles and policies - Log Groups in CloudWatch Logs. To learn more about these underlying resources, see [Synthetics Canaries Deletion](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/synthetics_canaries_deletion.html). In the CDK, you can configure your canary to delete the underlying lambda function when the canary is deleted. This can be provisioned by setting `provisionedResourceCleanup` to `true`. ```ts const canary = new synthetics.Canary(this, 'Canary', { test: synthetics.Test.custom({ handler: 'index.handler', code: synthetics.Code.fromInline('/* Synthetics handler code'), }), provisionedResourceCleanup: true, runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_6_2, }); ``` > Note: To properly clean up your canary on deletion, you still have to manually delete other resources > like S3 buckets and CloudWatch logs. > Note: The deletion of Lambda resources can also be performed by setting the `cleanup` argument to `Cleanup.LAMBDA`. However, this is an outdated argument that uses custom resources and is currently deprecated. ### Configuring the Canary Script To configure the script the canary executes, use the `test` property. The `test` property accepts a `Test` instance that can be initialized by the `Test` class static methods. Currently, the only implemented method is `Test.custom()`, which allows you to bring your own code. In the future, other methods will be added. `Test.custom()` accepts `code` and `handler` properties -- both are required by Synthetics to create a lambda function on your behalf. The `synthetics.Code` class exposes static methods to bundle your code artifacts: - `code.fromInline(code)` - specify an inline script. - `code.fromAsset(path)` - specify a .zip file or a directory in the local filesystem which will be zipped and uploaded to S3 on deployment. See the below Note for directory structure. - `code.fromBucket(bucket, key[, objectVersion])` - specify an S3 object that contains the .zip file of your runtime code. See the below Note for directory structure. Using the `Code` class static initializers: ```ts // To supply the code inline: new synthetics.Canary(this, 'Inline Canary', { test: synthetics.Test.custom({ code: synthetics.Code.fromInline('/* Synthetics handler code */'), handler: 'index.handler', // must be 'index.handler' }), runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_6_2, }); // To supply the code from your local filesystem: new synthetics.Canary(this, 'Asset Canary', { test: synthetics.Test.custom({ code: synthetics.Code.fromAsset(path.join(__dirname, 'canary')), handler: 'index.handler', // must end with '.handler' }), runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_6_2, }); // To supply the code from a S3 bucket: import * as s3 from 'aws-cdk-lib/aws-s3'; const bucket = new s3.Bucket(this, 'Code Bucket'); new synthetics.Canary(this, 'Bucket Canary', { test: synthetics.Test.custom({ code: synthetics.Code.fromBucket(bucket, 'canary.zip'), handler: 'index.handler', // must end with '.handler' }), runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_6_2, }); ``` > **Note:** Synthetics have a specified folder structure for canaries. > For Node with puppeteer scripts supplied via `code.fromAsset()` or `code.fromBucket()`, the canary resource requires the following folder structure: > > ```plaintext > canary/ > ├── nodejs/ > ├── node_modules/ > ├── <filename>.js > ``` > > For Node with playwright scripts supplied via `code.fromAsset()` or `code.fromBucket()`, the canary resource requires the following folder structure: > > ```plaintext > canary/ > ├── <filename>.js,.mjs,.cjs > ├─some/dir/path > ├── <filename>.js,.mjs,.cjs > ``` > > If `<filename>.js` is placed in the canary directory, the handler should be specified as `filename.handler`. > However, if it is placed in the `some/dir/path` directory, the handler should be specified as `some/dir/path/filename.handler`. > For more information, see Synthetics [docs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Synthetics_WritingCanary_Nodejs_Playwright.html). > > For Python scripts supplied via `code.fromAsset()` or `code.fromBucket()`, the canary resource requires the following folder structure: > > ```plaintext > canary/ > ├── python/ > ├── <filename>.py > ``` > > See Synthetics [docs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary.html). ### Running a canary on a VPC You can specify what [VPC a canary executes in](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_VPC.html). This can allow for monitoring services that may be internal to a specific VPC. To place a canary within a VPC, you can specify the `vpc` property with the desired `VPC` to place then canary in. This will automatically attach the appropriate IAM permissions to attach to the VPC. This will also create a Security Group and attach to the default subnets for the VPC unless specified via `vpcSubnets` and `securityGroups`. ```ts import * as ec2 from 'aws-cdk-lib/aws-ec2'; declare const vpc: ec2.IVpc; new synthetics.Canary(this, 'Vpc Canary', { test: synthetics.Test.custom({ code: synthetics.Code.fromAsset(path.join(__dirname, 'canary')), handler: 'index.handler', }), runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_6_2, vpc, }); ``` > **Note:** By default, the Synthetics runtime needs access to the S3 and CloudWatch APIs, which will fail in a private subnet without internet access enabled (e.g. an isolated subnnet). > > Ensure that the Canary is placed in a VPC either with internet connectivity or with VPC Endpoints for S3 and CloudWatch enabled and configured. > > See [Synthetics VPC docs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_VPC.html). ### Alarms You can configure a CloudWatch Alarm on a canary metric. Metrics are emitted by CloudWatch automatically and can be accessed by the following APIs: - `canary.metricSuccessPercent()` - percentage of successful canary runs over a given time - `canary.metricDuration()` - how much time each canary run takes, in seconds. - `canary.metricFailed()` - number of failed canary runs over a given time Create an alarm that tracks the canary metric: ```ts import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; declare const canary: synthetics.Canary; new cloudwatch.Alarm(this, 'CanaryAlarm', { metric: canary.metricSuccessPercent(), evaluationPeriods: 2, threshold: 90, comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD, }); ``` ### Artifacts You can pass an S3 bucket to store artifacts from canary runs. If you do not, one will be auto-generated when the canary is created. You may add [lifecycle rules](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html) to the auto-generated bucket. ```ts const canary = new synthetics.Canary(this, 'MyCanary', { schedule: synthetics.Schedule.rate(Duration.minutes(5)), test: synthetics.Test.custom({ code: synthetics.Code.fromAsset(path.join(__dirname, 'canary')), handler: 'index.handler', }), runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_6_2, artifactsBucketLifecycleRules: [{ expiration: Duration.days(30), }], }); ``` Canary artifacts are encrypted at rest using an AWS-managed key by default. You can choose the encryption options SSE-S3 or SSE-KMS by setting the `artifactS3EncryptionMode` property. When you use SSE-KMS, you can also supply your own external KMS key by specifying the `kmsKey` property. If you don't, a KMS key will be automatically created and associated with the canary. ```ts import * as kms from 'aws-cdk-lib/aws-kms'; const key = new kms.Key(this, 'myKey'); const canary = new synthetics.Canary(this, 'MyCanary', { schedule: synthetics.Schedule.rate(Duration.minutes(5)), test: synthetics.Test.custom({ code: synthetics.Code.fromAsset(path.join(__dirname, 'canary')), handler: 'index.handler', }), runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_7_0, artifactsBucketLifecycleRules: [{ expiration: Duration.days(30), }], artifactS3EncryptionMode: synthetics.ArtifactsEncryptionMode.KMS, artifactS3KmsKey: key, }); ```