From 9a55d96f7439c746daa48a81ab2fdd6ff8cb6be7 Mon Sep 17 00:00:00 2001 From: Alexander Melnyk <amelnyk@amazon.com> Date: Mon, 10 Jul 2023 16:20:33 +0200 Subject: [PATCH 1/2] chore(ci): add canary to layer deployment --- .github/scripts/setup_tmp_layer_files.sh | 4 +- .../workflows/reusable_deploy_layer_stack.yml | 2 + layers/bin/layers.ts | 7 ++ layers/src/canary-stack.ts | 99 +++++++++++++++++++ layers/src/canary/app.ts | 20 ++++ 5 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 layers/src/canary-stack.ts create mode 100644 layers/src/canary/app.ts diff --git a/.github/scripts/setup_tmp_layer_files.sh b/.github/scripts/setup_tmp_layer_files.sh index 77c5875734..977479267c 100644 --- a/.github/scripts/setup_tmp_layer_files.sh +++ b/.github/scripts/setup_tmp_layer_files.sh @@ -6,7 +6,9 @@ npm init -y npm i \ @aws-lambda-powertools/logger@$VERSION \ @aws-lambda-powertools/metrics@$VERSION \ - @aws-lambda-powertools/tracer@$VERSION + @aws-lambda-powertools/tracer@$VERSION \ + @aws-lambda-powertools/commons@$VERSION \ + @aws-lambda-powertools/parameters@$VERSION rm -rf node_modules/@types \ package.json \ package-lock.json diff --git a/.github/workflows/reusable_deploy_layer_stack.yml b/.github/workflows/reusable_deploy_layer_stack.yml index 86b0f5afb6..e044615f1c 100644 --- a/.github/workflows/reusable_deploy_layer_stack.yml +++ b/.github/workflows/reusable_deploy_layer_stack.yml @@ -94,6 +94,8 @@ jobs: path: ./cdk-layer-stack/* # NOTE: upload-artifact does not inherit working-directory setting. if-no-files-found: error retention-days: 1 + - name: CDK deploy canary + run: npm run cdk -w layer -- deploy --app cdk.out --context region=${{ matrix.region }} 'CanaryStack' --require-approval never --verbose --outputs-file cdk-outputs.json update_layer_arn_docs: needs: deploy-cdk-stack permissions: diff --git a/layers/bin/layers.ts b/layers/bin/layers.ts index 902bd6de5f..e945a14aba 100644 --- a/layers/bin/layers.ts +++ b/layers/bin/layers.ts @@ -2,6 +2,7 @@ import 'source-map-support/register'; import { App } from 'aws-cdk-lib'; import { LayerPublisherStack } from '../src/layer-publisher-stack'; +import { CanaryStack } from 'layers/src/canary-stack'; const SSM_PARAM_LAYER_ARN = '/layers/powertools-layer-arn'; @@ -12,3 +13,9 @@ new LayerPublisherStack(app, 'LayerPublisherStack', { layerName: 'AWSLambdaPowertoolsTypeScript', ssmParameterLayerArn: SSM_PARAM_LAYER_ARN, }); + +new CanaryStack(app, 'CanaryStack', { + powertoolsPackageVersion: app.node.tryGetContext('PowertoolsPackageVersion'), + ssmParameterLayerArn: SSM_PARAM_LAYER_ARN, + layerName: 'AWSLambdaPowertoolsCanaryTypeScript', +}); diff --git a/layers/src/canary-stack.ts b/layers/src/canary-stack.ts new file mode 100644 index 0000000000..1f310526b0 --- /dev/null +++ b/layers/src/canary-stack.ts @@ -0,0 +1,99 @@ +import { CustomResource, Duration, Stack, StackProps } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { LayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { v4 } from 'uuid'; +import { + Effect, + ManagedPolicy, + PolicyStatement, + Role, + ServicePrincipal, +} from 'aws-cdk-lib/aws-iam'; +import { Provider } from 'aws-cdk-lib/custom-resources'; +import { StringParameter } from 'aws-cdk-lib/aws-ssm'; +import path from 'path'; +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; + +export interface CanaryStackProps extends StackProps { + readonly layerName: string; + readonly powertoolsPackageVersion: string; + readonly ssmParameterLayerArn: string; +} + +export class CanaryStack extends Stack { + public constructor(scope: Construct, id: string, props: CanaryStackProps) { + super(scope, id, props); + const { layerName, powertoolsPackageVersion } = props; + + const suffix = v4().substring(0, 5); + + const layerArn = StringParameter.fromStringParameterAttributes( + this, + 'LayerArn', + { + parameterName: props.ssmParameterLayerArn, + } + ).stringValue; + + // lambda function + const layer = [ + LayerVersion.fromLayerVersionArn(this, 'powertools-layer', layerArn), + ]; + + const executionRole = new Role(this, 'LambdaExecutionRole', { + assumedBy: new ServicePrincipal('lambda.amazonaws.com'), + managedPolicies: [ + ManagedPolicy.fromAwsManagedPolicyName( + 'service-role/AWSLambdaBasicExecutionRole' + ), + ], + }); + + executionRole.addToPolicy( + new PolicyStatement({ + actions: ['lambda:GetFunction'], + resources: ['*'], + effect: Effect.ALLOW, + }) + ); + + const canaryFunction = new NodejsFunction(this, 'CanaryFunction', { + entry: path.join(__dirname, './canary/app.ts'), + handler: 'handler', + runtime: Runtime.NODEJS_18_X, + functionName: `canary-${suffix}`, + timeout: Duration.seconds(30), + bundling: { + externalModules: [ + // don't package these modules, we want to pull them from the layer + 'aws-sdk', + '@aws-lambda-powertools/logger', + '@aws-lambda-powertools/metrics', + '@aws-lambda-powertools/tracer', + '@aws-lambda-powertools/parameters', + '@aws-lambda-powertools/commons', + ], + }, + role: executionRole, + environment: { + POWERTOOLS_SERVICE_NAME: 'canary', + POWERTOOLS_VERSION: powertoolsPackageVersion, + POWERTOOLS_LAYER_NAME: layerName, + }, + layers: layer, + logRetention: RetentionDays.ONE_DAY, + }); + + // use custom resource to trigger the lambda function during the CFN deployment + const provider = new Provider(this, 'CanaryCustomResourceProvider', { + onEventHandler: canaryFunction, + logRetention: RetentionDays.ONE_DAY, + }); + + // random suffix forces recreation of the custom resource otherwise the custom resource will be reused from prevous deployment + new CustomResource(this, `CanaryCustomResource${suffix}`, { + serviceToken: provider.serviceToken, + }); + } +} diff --git a/layers/src/canary/app.ts b/layers/src/canary/app.ts new file mode 100644 index 0000000000..07ca9421b6 --- /dev/null +++ b/layers/src/canary/app.ts @@ -0,0 +1,20 @@ +import { Context } from 'aws-lambda'; +import { Logger } from '@aws-lambda-powertools/logger'; +import { Tracer } from '@aws-lambda-powertools/tracer'; +import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; +import { SSMProvider } from '@aws-lambda-powertools/parameters/ssm'; + +const logger = new Logger(); +const tracer = new Tracer(); +const metrics = new Metrics({}); +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const ssmProvider = new SSMProvider(); + +export const handler = async ( + _event: unknown, + _context: Context +): Promise<void> => { + logger.info('Hello, world!'); + metrics.addMetric('MyMetric', MetricUnits.Count, 1); + tracer.annotateColdStart(); +}; From 847b3bf907297c12257a27e166050f23b5e14105 Mon Sep 17 00:00:00 2001 From: Alexander Melnyk <amelnyk@amazon.com> Date: Mon, 10 Jul 2023 18:03:27 +0200 Subject: [PATCH 2/2] reuse e2e function for canary --- .github/scripts/setup_tmp_layer_files.sh | 1 - layers/src/canary-stack.ts | 42 +++++++------------ layers/src/canary/app.ts | 20 --------- .../layerPublisher.class.test.functionCode.ts | 32 +++++++------- 4 files changed, 30 insertions(+), 65 deletions(-) delete mode 100644 layers/src/canary/app.ts diff --git a/.github/scripts/setup_tmp_layer_files.sh b/.github/scripts/setup_tmp_layer_files.sh index 977479267c..9d6da9635a 100644 --- a/.github/scripts/setup_tmp_layer_files.sh +++ b/.github/scripts/setup_tmp_layer_files.sh @@ -7,7 +7,6 @@ npm i \ @aws-lambda-powertools/logger@$VERSION \ @aws-lambda-powertools/metrics@$VERSION \ @aws-lambda-powertools/tracer@$VERSION \ - @aws-lambda-powertools/commons@$VERSION \ @aws-lambda-powertools/parameters@$VERSION rm -rf node_modules/@types \ package.json \ diff --git a/layers/src/canary-stack.ts b/layers/src/canary-stack.ts index 1f310526b0..4847af21d3 100644 --- a/layers/src/canary-stack.ts +++ b/layers/src/canary-stack.ts @@ -3,13 +3,7 @@ import { Construct } from 'constructs'; import { LayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; import { RetentionDays } from 'aws-cdk-lib/aws-logs'; import { v4 } from 'uuid'; -import { - Effect, - ManagedPolicy, - PolicyStatement, - Role, - ServicePrincipal, -} from 'aws-cdk-lib/aws-iam'; +import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { Provider } from 'aws-cdk-lib/custom-resources'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import path from 'path'; @@ -41,25 +35,11 @@ export class CanaryStack extends Stack { LayerVersion.fromLayerVersionArn(this, 'powertools-layer', layerArn), ]; - const executionRole = new Role(this, 'LambdaExecutionRole', { - assumedBy: new ServicePrincipal('lambda.amazonaws.com'), - managedPolicies: [ - ManagedPolicy.fromAwsManagedPolicyName( - 'service-role/AWSLambdaBasicExecutionRole' - ), - ], - }); - - executionRole.addToPolicy( - new PolicyStatement({ - actions: ['lambda:GetFunction'], - resources: ['*'], - effect: Effect.ALLOW, - }) - ); - const canaryFunction = new NodejsFunction(this, 'CanaryFunction', { - entry: path.join(__dirname, './canary/app.ts'), + entry: path.join( + __dirname, + '../tests/e2e/layerPublisher.class.test.functionCode.ts' + ), handler: 'handler', runtime: Runtime.NODEJS_18_X, functionName: `canary-${suffix}`, @@ -75,16 +55,24 @@ export class CanaryStack extends Stack { '@aws-lambda-powertools/commons', ], }, - role: executionRole, environment: { POWERTOOLS_SERVICE_NAME: 'canary', - POWERTOOLS_VERSION: powertoolsPackageVersion, + POWERTOOLS_PACKAGE_VERSION: powertoolsPackageVersion, POWERTOOLS_LAYER_NAME: layerName, + SSM_PARAMETER_LAYER_ARN: props.ssmParameterLayerArn, }, layers: layer, logRetention: RetentionDays.ONE_DAY, }); + canaryFunction.addToRolePolicy( + new PolicyStatement({ + actions: ['ssm:GetParameter'], + resources: ['*'], + effect: Effect.ALLOW, + }) + ); + // use custom resource to trigger the lambda function during the CFN deployment const provider = new Provider(this, 'CanaryCustomResourceProvider', { onEventHandler: canaryFunction, diff --git a/layers/src/canary/app.ts b/layers/src/canary/app.ts deleted file mode 100644 index 07ca9421b6..0000000000 --- a/layers/src/canary/app.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Context } from 'aws-lambda'; -import { Logger } from '@aws-lambda-powertools/logger'; -import { Tracer } from '@aws-lambda-powertools/tracer'; -import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics'; -import { SSMProvider } from '@aws-lambda-powertools/parameters/ssm'; - -const logger = new Logger(); -const tracer = new Tracer(); -const metrics = new Metrics({}); -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const ssmProvider = new SSMProvider(); - -export const handler = async ( - _event: unknown, - _context: Context -): Promise<void> => { - logger.info('Hello, world!'); - metrics.addMetric('MyMetric', MetricUnits.Count, 1); - tracer.annotateColdStart(); -}; diff --git a/layers/tests/e2e/layerPublisher.class.test.functionCode.ts b/layers/tests/e2e/layerPublisher.class.test.functionCode.ts index 6606efffbc..28e60424e8 100644 --- a/layers/tests/e2e/layerPublisher.class.test.functionCode.ts +++ b/layers/tests/e2e/layerPublisher.class.test.functionCode.ts @@ -2,33 +2,31 @@ import { readFileSync } from 'node:fs'; import { Logger } from '@aws-lambda-powertools/logger'; import { Metrics } from '@aws-lambda-powertools/metrics'; import { Tracer } from '@aws-lambda-powertools/tracer'; +import { SSMProvider } from '@aws-lambda-powertools/parameters/ssm'; const logger = new Logger({ logLevel: 'DEBUG', }); const metrics = new Metrics(); const tracer = new Tracer(); +new SSMProvider(); export const handler = (): void => { // Check that the packages version matches the expected one - try { - const packageJSON = JSON.parse( - readFileSync( - '/opt/nodejs/node_modules/@aws-lambda-powertools/logger/package.json', - { - encoding: 'utf8', - flag: 'r', - } - ) - ); + const packageJSON = JSON.parse( + readFileSync( + '/opt/nodejs/node_modules/@aws-lambda-powertools/logger/package.json', + { + encoding: 'utf8', + flag: 'r', + } + ) + ); - if (packageJSON.version != process.env.POWERTOOLS_PACKAGE_VERSION) { - throw new Error( - `Package version mismatch: ${packageJSON.version} != ${process.env.POWERTOOLS_PACKAGE_VERSION}` - ); - } - } catch (error) { - console.error(error); + if (packageJSON.version != process.env.POWERTOOLS_PACKAGE_VERSION) { + throw new Error( + `Package version mismatch: ${packageJSON.version} != ${process.env.POWERTOOLS_PACKAGE_VERSION}` + ); } // Check that the logger is working