Config AWS Cloudwatch Application Signals for NodeJs Lambda with CDK

Johannes Konings - Jan 4 - - Dev Community

Use case

You want to use AWS Cloudwatch Application Signals for your NodeJs Lambda functions. For IaC you use CDK.

Setup

In a new AWS account the Management Console show the two steps to set up the Cloudwatch Application Signals. Step 1 is an Account wide setup, Step 2 is necessary for each Lambda function.

application signals config steps

Account wide setup

For starting the discovery the service linked role application-signals.cloudwatch.amazonaws.com must be created: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/using-service-linked-roles.html#service-linked-role-signals. CDK itself has no direct functionality for that. The SDK can help here via a custom resource.

 const serviceLinkeRoleArnApplicationSignals = `arn:aws:iam::${Stack.of(this).account}:role/aws-service-role/application-signals.cloudwatch.amazonaws.com/AWSServiceRoleForCloudWatchApplicationSignals`;
    const applicationSignalsStartDiscovery = new AwsCustomResource(
      this,
      "ApplicationSignalsStartDiscovery",
      {
        onCreate: {
          service: "@aws-sdk/client-application-signals",
          action: "StartDiscovery",
          physicalResourceId: PhysicalResourceId.of(
            "ApplicationSignalsStartDiscovery",
          ),
        },
        // fromSdkCalls didn't work, that's why the policy is set manually
        // policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: AwsCustomResourcePolicy.ANY_RESOURCE }),
        policy: AwsCustomResourcePolicy.fromStatements([
          new PolicyStatement({
            effect: Effect.ALLOW,
            actions: ["iam:CreateServiceLinkedRole"],
            resources: [serviceLinkeRoleArnApplicationSignals],
          }),
          new PolicyStatement({
            effect: Effect.ALLOW,
            actions: ["application-signals:StartDiscovery"],
            resources: ["*"],
          }),
        ]),
      },
    );

    const customResourceId = `AWS${AwsCustomResource.PROVIDER_FUNCTION_UUID.replaceAll("-", "")}`;
    NagSuppressions.addResourceSuppressionsByPath(
      Stack.of(this),
      [
        `/${Stack.of(this).stackName}/${customResourceId}/ServiceRole/Resource`,
        `/${Stack.of(this).stackName}/${customResourceId}/Resource`,
      ],
      [
        {
          id: "AwsSolutions-L1",
          reason: "CDK managed lambda function",
        },
        {
          id: "AwsSolutions-IAM4",
          reason: "CDK managed policy",
        },
        {
          id: "AwsSolutions-IAM5",
          reason: "CDK managed policy",
        },
      ],
      true,
    );

    NagSuppressions.addResourceSuppressions(
      applicationSignalsStartDiscovery,
      [
        {
          id: "AwsSolutions-IAM5",
          reason: "CDK managed policy",
        },
      ],
      true,
    );

Enter fullscreen mode Exit fullscreen mode

Each Lambda function

As described here each lambda need the environment variable AWS_LAMBDA_EXEC_WRAPPER with the value /opt/otel-instrument and the layer AWSOpenTelemetryDistroJs with the respective ARN.

  const LAMBDA_APPLICATION_SIGNALS_LAYER_ARN =
    "arn:aws:lambda:us-east-1:615299751070:layer:AWSOpenTelemetryDistroJs:5";
  const LAMBDA_APPLICATION_SIGNALS_ENV = {
    AWS_LAMBDA_EXEC_WRAPPER: "/opt/otel-instrument",
  };

    const lambda = new NodejsFunction(this, id, {
      runtime: Runtime.NODEJS_22_X,
      timeout: Duration.seconds(10),
      environment: props.enableApplicationSignals
        ? LAMBDA_APPLICATION_SIGNALS_ENV
        : {},
    });
    lambda.role?.addManagedPolicy(
      ManagedPolicy.fromAwsManagedPolicyName(
        "CloudWatchLambdaApplicationSignalsExecutionRolePolicy",
      ),
    );
    NagSuppressions.addResourceSuppressions(
      lambda,
      [
        {
          id: "AwsSolutions-IAM4",
          reason: "CDK managed policy",
        },
      ],
      true,
    );
    const layerApplicationSignals = LayerVersion.fromLayerVersionArn(
      this,
      "LambdaApplicationSignalsLayer",
      LAMBDA_APPLICATION_SIGNALS_LAYER_ARN,
    );

    lambda.addLayers(layerApplicationSignals);

Enter fullscreen mode Exit fullscreen mode

The lambda function implementation can than look like this:

const handler = async (event: undefined, context: undefined) => {
  console.log("lambda was called...");
  return {
    statusCode: 200,
    body: JSON.stringify({
      message: "Hello from Lambda!",
    }),
  };
};
module.exports = { handler };

Enter fullscreen mode Exit fullscreen mode

⚠️ The documentation recommend to use currently CommonJS (CJS) instead of ECMAScript Modules (ESM).

Also for CommonJs some "details" are to consider like the export of the handler: https://github.com/aws-observability/aws-otel-lambda/issues/284#issuecomment-1465465790

Result

After the setup and some runs of the Lambda function the Cloudwatch Application Signals are visible in the Management Console (It takes some minutes).

application signals result

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .