Skip to content

Commit

Permalink
feat(synthetics): support canary environment variables (#15082)
Browse files Browse the repository at this point in the history
Add support for canary environment variables that will be threaded to the underlying Lambda function. This allows multiple canaries to use the same source code by extracting configuration to the resource specification.

Also makes the README snippets compile since it was hard to tell whether my changes were correct.

closes #10515
refer #9300

Co-authored-by: Florian Chazal <[email protected]>

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
BenChaimberg authored Jun 16, 2021
1 parent dc3cf13 commit df9f13f
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 42 deletions.
44 changes: 23 additions & 21 deletions packages/@aws-cdk/aws-synthetics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,40 +36,39 @@ The Hitchhikers Guide to the Galaxy
The below code defines a canary that will hit the `books/topbook` endpoint every 5 minutes:

```ts
import * as synthetics from '@aws-cdk/aws-synthetics';

const canary = new synthetics.Canary(this, 'MyCanary', {
schedule: synthetics.Schedule.rate(Duration.minutes(5)),
test: Test.custom({
test: synthetics.Test.custom({
code: synthetics.Code.fromAsset(path.join(__dirname, 'canary')),
handler: 'index.handler',
}),
runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_3_1,
environmentVariables: {
stage: 'prod',
},
});
```

The following is an example of an `index.js` file which exports the `handler` function:

```js
var synthetics = require('Synthetics');
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/`;

// INSERT URL here
const URL = "https://api.example.com/user/books/topbook/";

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.
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
// This will take a screenshot that will be included in test output artifacts.
await synthetics.takeScreenshot('loaded', 'loaded');
let pageTitle = await page.title();
const pageTitle = await page.title();
log.info('Page title: ' + pageTitle);
if (response.status() !== 200) {
throw "Failed to load page!";
throw 'Failed to load page!';
}
};

Expand Down Expand Up @@ -102,26 +101,28 @@ Using the `Code` class static initializers:

```ts
// To supply the code inline:
const canary = new Canary(this, 'MyCanary', {
test: Test.custom({
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_3_1,
});

// To supply the code from your local filesystem:
const canary = new Canary(this, 'MyCanary', {
test: Test.custom({
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_3_1,
});

// To supply the code from a S3 bucket:
const canary = new Canary(this, 'MyCanary', {
test: Test.custom({
import * as s3 from '@aws-cdk/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'
}),
Expand Down Expand Up @@ -150,7 +151,8 @@ You can configure a CloudWatch Alarm on a canary metric. Metrics are emitted by

Create an alarm that tracks the canary metric:

```ts
```ts fixture=canary
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
new cloudwatch.Alarm(this, 'CanaryAlarm', {
metric: canary.metricSuccessPercent(),
evaluationPeriods: 2,
Expand Down
18 changes: 18 additions & 0 deletions packages/@aws-cdk/aws-synthetics/lib/canary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,14 @@ export interface CanaryProps {
*/
readonly test: Test;

/**
* Key-value pairs that the Synthetics caches and makes available for your canary scripts. Use environment variables
* to apply configuration changes, such as test and production environment configurations, without changing your
* Canary script source code.
*
* @default - No environment variables.
*/
readonly environmentVariables?: { [key: string]: string };
}

/**
Expand Down Expand Up @@ -306,6 +314,7 @@ export class Canary extends cdk.Resource {
failureRetentionPeriod: props.failureRetentionPeriod?.toDays(),
successRetentionPeriod: props.successRetentionPeriod?.toDays(),
code: this.createCode(props),
runConfig: this.createRunConfig(props),
});

this.canaryId = resource.attrId;
Expand Down Expand Up @@ -410,6 +419,15 @@ export class Canary extends cdk.Resource {
};
}

private createRunConfig(props: CanaryProps): CfnCanary.RunConfigProperty | undefined {
if (!props.environmentVariables) {
return undefined;
}
return {
environmentVariables: props.environmentVariables,
};
}

/**
* Creates a unique name for the canary. The generated name is the physical ID of the canary.
*/
Expand Down
21 changes: 21 additions & 0 deletions packages/@aws-cdk/aws-synthetics/rosetta/canary.ts-fixture
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Fixture with a canary already created, named `canary`
import { Construct, Duration, Stack } from '@aws-cdk/core';
import * as synthetics from '@aws-cdk/aws-synthetics';
import * as path from 'path';

class Fixture extends Stack {
constructor(scope: Construct, id: string) {
super(scope, id);

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_3_1,
});

/// here
}
}
12 changes: 12 additions & 0 deletions packages/@aws-cdk/aws-synthetics/rosetta/default.ts-fixture
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Fixture with packages imported, but nothing else
import { Construct, Duration, Stack } from '@aws-cdk/core';
import * as synthetics from '@aws-cdk/aws-synthetics';
import * as path from 'path';

class Fixture extends Stack {
constructor(scope: Construct, id: string) {
super(scope, id);

/// here
}
}
87 changes: 66 additions & 21 deletions packages/@aws-cdk/aws-synthetics/test/canary.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import '@aws-cdk/assert-internal/jest';
import { objectLike } from '@aws-cdk/assert-internal';
import { ABSENT, objectLike } from '@aws-cdk/assert-internal';
import * as iam from '@aws-cdk/aws-iam';
import * as s3 from '@aws-cdk/aws-s3';
import { App, Duration, Lazy, Stack } from '@aws-cdk/core';
import { Duration, Lazy, Stack } from '@aws-cdk/core';
import * as synthetics from '../lib';

test('Basic canary properties work', () => {
// GIVEN
const stack = new Stack(new App(), 'canaries');
const stack = new Stack();

// WHEN
new synthetics.Canary(stack, 'Canary', {
Expand Down Expand Up @@ -36,7 +36,7 @@ test('Basic canary properties work', () => {

test('Canary can have generated name', () => {
// GIVEN
const stack = new Stack(new App(), 'canaries');
const stack = new Stack();

// WHEN
new synthetics.Canary(stack, 'Canary', {
Expand All @@ -49,13 +49,13 @@ test('Canary can have generated name', () => {

// THEN
expect(stack).toHaveResourceLike('AWS::Synthetics::Canary', {
Name: 'canariescanary8dfb794',
Name: 'canary',
});
});

test('Name validation does not fail when using Tokens', () => {
// GIVEN
const stack = new Stack(new App(), 'canaries');
const stack = new Stack();

// WHEN
new synthetics.Canary(stack, 'Canary', {
Expand All @@ -73,7 +73,7 @@ test('Name validation does not fail when using Tokens', () => {

test('Throws when name is specified incorrectly', () => {
// GIVEN
const stack = new Stack(new App(), 'canaries');
const stack = new Stack();

// THEN
expect(() => new synthetics.Canary(stack, 'Canary', {
Expand All @@ -89,7 +89,7 @@ test('Throws when name is specified incorrectly', () => {

test('Throws when name has more than 21 characters', () => {
// GIVEN
const stack = new Stack(new App(), 'canaries');
const stack = new Stack();

// THEN
expect(() => new synthetics.Canary(stack, 'Canary', {
Expand All @@ -105,7 +105,7 @@ test('Throws when name has more than 21 characters', () => {

test('An existing role can be specified instead of auto-created', () => {
// GIVEN
const stack = new Stack(new App(), 'canaries');
const stack = new Stack();

const role = new iam.Role(stack, 'role', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
Expand All @@ -131,7 +131,7 @@ test('An existing role can be specified instead of auto-created', () => {

test('An existing bucket and prefix can be specified instead of auto-created', () => {
// GIVEN
const stack = new Stack(new App(), 'canaries');
const stack = new Stack();
const bucket = new s3.Bucket(stack, 'mytestbucket');
const prefix = 'canary';

Expand All @@ -153,7 +153,7 @@ test('An existing bucket and prefix can be specified instead of auto-created', (

test('Runtime can be specified', () => {
// GIVEN
const stack = new Stack(new App(), 'canaries');
const stack = new Stack();

// WHEN
new synthetics.Canary(stack, 'Canary', {
Expand All @@ -170,9 +170,54 @@ test('Runtime can be specified', () => {
});
});

test('environment variables can be specified', () => {
// GIVEN
const stack = new Stack();
const environmentVariables = {
TEST_KEY_1: 'TEST_VALUE_1',
TEST_KEY_2: 'TEST_VALUE_2',
};

// WHEN
new synthetics.Canary(stack, 'Canary', {
runtime: synthetics.Runtime.SYNTHETICS_1_0,
test: synthetics.Test.custom({
handler: 'index.handler',
code: synthetics.Code.fromInline('/* Synthetics handler code */'),
}),
environmentVariables: environmentVariables,
});

// THEN
expect(stack).toHaveResourceLike('AWS::Synthetics::Canary', {
RunConfig: {
EnvironmentVariables: environmentVariables,
},
});
});

test('environment variables are skipped if not provided', () => {
// GIVEN
const stack = new Stack();

// WHEN
new synthetics.Canary(stack, 'Canary', {
runtime: synthetics.Runtime.SYNTHETICS_1_0,
test: synthetics.Test.custom({
handler: 'index.handler',
code: synthetics.Code.fromInline('/* Synthetics handler code */'),
}),
});

// THEN
expect(stack).toHaveResourceLike('AWS::Synthetics::Canary', {
RunConfig: ABSENT,
});
});

test('Runtime can be customized', () => {
// GIVEN
const stack = new Stack(new App(), 'canaries');
const stack = new Stack();

// WHEN
new synthetics.Canary(stack, 'Canary', {
Expand All @@ -191,7 +236,7 @@ test('Runtime can be customized', () => {

test('Schedule can be set with Rate', () => {
// GIVEN
const stack = new Stack(new App(), 'canaries');
const stack = new Stack();

// WHEN
new synthetics.Canary(stack, 'Canary', {
Expand All @@ -211,7 +256,7 @@ test('Schedule can be set with Rate', () => {

test('Schedule can be set to 1 minute', () => {
// GIVEN
const stack = new Stack(new App(), 'canaries');
const stack = new Stack();

// WHEN
new synthetics.Canary(stack, 'Canary', {
Expand All @@ -231,7 +276,7 @@ test('Schedule can be set to 1 minute', () => {

test('Schedule can be set with Expression', () => {
// GIVEN
const stack = new Stack(new App(), 'canaries');
const stack = new Stack();

// WHEN
new synthetics.Canary(stack, 'Canary', {
Expand All @@ -251,7 +296,7 @@ test('Schedule can be set with Expression', () => {

test('Schedule can be set to run once', () => {
// GIVEN
const stack = new Stack(new App(), 'canaries');
const stack = new Stack();

// WHEN
new synthetics.Canary(stack, 'Canary', {
Expand All @@ -271,7 +316,7 @@ test('Schedule can be set to run once', () => {

test('Throws when rate above 60 minutes', () => {
// GIVEN
const stack = new Stack(new App(), 'canaries');
const stack = new Stack();

// THEN
expect(() => new synthetics.Canary(stack, 'Canary', {
Expand All @@ -287,7 +332,7 @@ test('Throws when rate above 60 minutes', () => {

test('Throws when rate above is not a whole number of minutes', () => {
// GIVEN
const stack = new Stack(new App(), 'canaries');
const stack = new Stack();

// THEN
expect(() => new synthetics.Canary(stack, 'Canary', {
Expand All @@ -303,7 +348,7 @@ test('Throws when rate above is not a whole number of minutes', () => {

test('Can share artifacts bucket between canaries', () => {
// GIVEN
const stack = new Stack(new App(), 'canaries');
const stack = new Stack();

// WHEN
const canary1 = new synthetics.Canary(stack, 'Canary1', {
Expand Down Expand Up @@ -331,7 +376,7 @@ test('Can share artifacts bucket between canaries', () => {

test('can specify custom test', () => {
// GIVEN
const stack = new Stack(new App(), 'canaries');
const stack = new Stack();

// WHEN
new synthetics.Canary(stack, 'Canary', {
Expand All @@ -355,4 +400,4 @@ test('can specify custom test', () => {
};`,
},
});
});
});

0 comments on commit df9f13f

Please sign in to comment.