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