diff --git a/packages/@aws-cdk/aws-synthetics/README.md b/packages/@aws-cdk/aws-synthetics/README.md index 80a6defd34848..878272f31b57d 100644 --- a/packages/@aws-cdk/aws-synthetics/README.md +++ b/packages/@aws-cdk/aws-synthetics/README.md @@ -45,6 +45,9 @@ const canary = new synthetics.Canary(this, 'MyCanary', { handler: 'index.handler', }), runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_3_0, + environment: { + URL: 'https://api.example.com/user/books/topbook/' + } }); ``` @@ -57,7 +60,7 @@ const log = require('SyntheticsLogger'); const pageLoadBlueprint = async function () { // INSERT URL here - const URL = "https://api.example.com/user/books/topbook/"; + const URL =process.env.URL; let page = await synthetics.getPage(); const response = await page.goto(URL, {waitUntil: 'domcontentloaded', timeout: 30000}); diff --git a/packages/@aws-cdk/aws-synthetics/lib/canary.ts b/packages/@aws-cdk/aws-synthetics/lib/canary.ts index 72d4b311ce6e3..1f7cccbac6ae4 100644 --- a/packages/@aws-cdk/aws-synthetics/lib/canary.ts +++ b/packages/@aws-cdk/aws-synthetics/lib/canary.ts @@ -235,6 +235,46 @@ 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 environment?: { [key: string]: string }; + + /** + * How long the canary is allowed to run before it must stop. + * You can't set this time to be longer than the frequency of the runs of this canary. + * If you omit this field, the frequency of the canary is used as this value, up to a maximum of 900 seconds. + * + * @default cdk.Duration.seconds(840) + */ + readonly timeout?: cdk.Duration; + + /** + * The maximum amount of memory that the canary can use while running. This value + * must be a multiple of 64. The range is 960 to 3008. + * + * @default 960 + */ + readonly memorySize?: number; + + /** + * Specifies whether this canary is to use active AWS X-Ray tracing when it runs. + * Active tracing enables this canary run to be displayed in the ServiceLens and X-Ray service maps + * even if the canary does not hit an endpoint that has X-ray tracing enabled. + * + * You can enable active tracing only for canaries that use version syn-nodejs-2.0 or later for their canary runtime. + * + * Enabling tracing increases canary run time by 2.5% to 7%. + * + * @default false + */ + readonly tracing?: boolean; + } /** @@ -284,7 +324,9 @@ export class Canary extends cdk.Resource { encryption: s3.BucketEncryption.KMS_MANAGED, }); - this.role = props.role ?? this.createDefaultRole(props.artifactsBucketLocation?.prefix); + this.role = props.role ?? this.createDefaultRole(props.artifactsBucketLocation?.prefix, props.tracing); + + const schedule = this.createSchedule(props); const resource: CfnCanary = new CfnCanary(this, 'Resource', { artifactS3Location: this.artifactsBucket.s3UrlForObject(props.artifactsBucketLocation?.prefix), @@ -292,10 +334,11 @@ export class Canary extends cdk.Resource { startCanaryAfterCreation: props.startAfterCreation ?? true, runtimeVersion: props.runtime.name, name: this.physicalName, - schedule: this.createSchedule(props), + schedule: schedule, failureRetentionPeriod: props.failureRetentionPeriod?.toDays(), successRetentionPeriod: props.successRetentionPeriod?.toDays(), code: this.createCode(props), + runConfig: this.createRunConfig(props, schedule), }); this.canaryId = resource.attrId; @@ -339,7 +382,7 @@ export class Canary extends cdk.Resource { /** * Returns a default role for the canary */ - private createDefaultRole(prefix?: string): iam.IRole { + private createDefaultRole(prefix?: string, tracingEnabled?: boolean): iam.IRole { const { partition } = cdk.Stack.of(this); // Created role will need these policies to run the Canary. // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-synthetics-canary.html#cfn-synthetics-canary-executionrolearn @@ -365,6 +408,15 @@ export class Canary extends cdk.Resource { ], }); + if (tracingEnabled) { + policy.addStatements( + new iam.PolicyStatement({ + resources: ['*'], + actions: ['xray:PutTraceSegments'], + }), + ); + } + return new iam.Role(this, 'ServiceRole', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), inlinePolicies: { @@ -400,6 +452,27 @@ export class Canary extends cdk.Resource { }; } + /** + * Retruns a runConfig object + */ + private createRunConfig(props:CanaryProps, schedule: CfnCanary.ScheduleProperty): CfnCanary.RunConfigProperty { + // Cloudformation implementation made TimeoutInSeconds a required field where it should not (see links below). + // So here is a workaround to fix https://github.com/aws/aws-cdk/issues/9300 and still follow documented behavior. + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-synthetics-canary-runconfig.html#cfn-synthetics-canary-runconfig-timeoutinseconds + // https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-synthetics/issues/31 + + const MAX_CANARY_TIMEOUT = 840; + const RATE_IN_SECONDS = Schedule.expressionToRateInSeconds(schedule.expression); + const DEFAULT_CANARY_TIMEOUT_IN_SECONDS = RATE_IN_SECONDS <= MAX_CANARY_TIMEOUT ? RATE_IN_SECONDS : MAX_CANARY_TIMEOUT; + + return { + timeoutInSeconds: props.timeout?.toSeconds() ?? DEFAULT_CANARY_TIMEOUT_IN_SECONDS, + activeTracing: props.tracing, + environmentVariables: props.environment, + memoryInMb: props.memorySize, + }; + } + /** * Creates a unique name for the canary. The generated name is the physical ID of the canary. */ diff --git a/packages/@aws-cdk/aws-synthetics/lib/schedule.ts b/packages/@aws-cdk/aws-synthetics/lib/schedule.ts index 3bd92c81b4d0b..5e50214fc8d37 100644 --- a/packages/@aws-cdk/aws-synthetics/lib/schedule.ts +++ b/packages/@aws-cdk/aws-synthetics/lib/schedule.ts @@ -42,6 +42,33 @@ export class Schedule { return new Schedule(`rate(${minutes} minutes)`); } + /** + * Convert a Schedule expression in to a number of seconds + * + * @param expression Schedule expression such as 'rate(2 minutes)' + */ + public static expressionToRateInSeconds(expression: string): number { + // extract data isolating the number and units of the rate set + const regularExpression = /^rate\(([0-9]*) ([a-z]*)\)/; + const split = regularExpression.exec(expression); + const number = Number(split ? split[1] : '0'); + const unit = split ? split[2] : 'minutes'; + + switch (unit) { + case 'second': + case 'seconds': + return number; + case 'minute': + case 'minutes': + return 60 * number; + case 'hour': + case 'hours': + return 3600 * number; + default: + throw new Error('Unit not supported'); + } + } + private constructor( /** * The Schedule expression diff --git a/packages/@aws-cdk/aws-synthetics/test/canary.test.ts b/packages/@aws-cdk/aws-synthetics/test/canary.test.ts index bb5e479e7f7e9..83d4678c1f8de 100644 --- a/packages/@aws-cdk/aws-synthetics/test/canary.test.ts +++ b/packages/@aws-cdk/aws-synthetics/test/canary.test.ts @@ -1,5 +1,5 @@ import '@aws-cdk/assert/jest'; -import { objectLike } from '@aws-cdk/assert'; +import { objectLike, Capture } from '@aws-cdk/assert'; 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'; @@ -170,6 +170,130 @@ test('Runtime can be specified', () => { }); }); +test('RunConfig attributes can be specified', () => { + // GIVEN + const stack = new Stack(new App(), 'canaries'); + const environmentVariables = { + test_key_1: 'TEST_VALUE_1', + test_key_2: 'TEST_VALUE_2', + }; + const timeout = 10; + const memorySize = 256; + const activateTracing = true; + + // 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 */'), + }), + environment: environmentVariables, + timeout: Duration.seconds(timeout), + memorySize: memorySize, + tracing: activateTracing, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Synthetics::Canary', { + RunConfig: { + EnvironmentVariables: environmentVariables, + TimeoutInSeconds: timeout, + MemoryInMB: memorySize, + ActiveTracing: activateTracing, + }, + }); +}); + +test('If timeout not provided it default to schedule set with rate', () => { + // GIVEN + const stack = new Stack(new App(), 'canaries'); + const scheduledRate = Duration.minutes(3); + // 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 */'), + }), + schedule: synthetics.Schedule.rate(scheduledRate), + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Synthetics::Canary', { + RunConfig: { + TimeoutInSeconds: scheduledRate.toSeconds(), + }, + }); +}); + +test('If timeout not provided it default to schedule set with expressionString', () => { + // GIVEN + const stack = new Stack(new App(), 'canaries'); + // 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 */'), + }), + schedule: { + expressionString: 'rate(2 minutes)', + }, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Synthetics::Canary', { + RunConfig: { + TimeoutInSeconds: 120, + }, + }); +}); + + +test('If timeout not provided it default to default schedule if schedule is not set', () => { + // GIVEN + const stack = new Stack(new App(), 'canaries'); + // 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: { + TimeoutInSeconds: 300, + }, + }); +}); + +test('If timeout not provided it default to MAX timeout if schedule is higher than max', () => { + // GIVEN + const stack = new Stack(new App(), 'canaries'); + const scheduledRate = Duration.hours(1); + + // 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 */'), + }), + schedule: synthetics.Schedule.rate(scheduledRate), + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Synthetics::Canary', { + RunConfig: { + TimeoutInSeconds: 840, + }, + }); +}); + test('Runtime can be customized', () => { // GIVEN const stack = new Stack(new App(), 'canaries'); @@ -269,6 +393,45 @@ test('Schedule can be set to run once', () => { }); }); +test('On tracing enabled, the generated role will have xray PutTraceSegments permission', () => { + // GIVEN + const stack = new Stack(new App(), 'canaries'); + + + // WHEN + new synthetics.Canary(stack, 'Canary', { + test: synthetics.Test.custom({ + handler: 'index.handler', + code: synthetics.Code.fromInline('/* Synthetics handler code */'), + }), + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_0, + tracing: true, + }); + + // THEN + const policyStatements = Capture.anyType(); + expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Policies: [ + { + PolicyDocument: { + Statement: policyStatements.capture(), + }, + }, + ], + }); + + expect(policyStatements.capturedValue).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + Action: 'xray:PutTraceSegments', + Effect: 'Allow', + Resource: '*', + }), + ]), + ); + +}); + test('Throws when rate above 60 minutes', () => { // GIVEN const stack = new Stack(new App(), 'canaries'); diff --git a/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json b/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json index 58412fee9bfbb..c201a7602f4f4 100644 --- a/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json +++ b/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json @@ -119,7 +119,10 @@ "DurationInSeconds": "0", "Expression": "rate(1 minute)" }, - "StartCanaryAfterCreation": true + "StartCanaryAfterCreation": true, + "RunConfig": { + "TimeoutInSeconds":60 + } } }, "MyCanaryOneArtifactsBucketDF4A487D": { @@ -286,7 +289,10 @@ "DurationInSeconds": "0", "Expression": "rate(5 minutes)" }, - "StartCanaryAfterCreation": true + "StartCanaryAfterCreation": true, + "RunConfig": { + "TimeoutInSeconds":300 + } } }, "MyCanaryTwoArtifactsBucket79B179B6": { @@ -453,7 +459,10 @@ "DurationInSeconds": "0", "Expression": "rate(5 minutes)" }, - "StartCanaryAfterCreation": true + "StartCanaryAfterCreation": true, + "RunConfig": { + "TimeoutInSeconds":300 + } } } },