Skip to content
Home
Blog
Notes
Back to Blog

AWS Application Signals for Node.js Lambda with CDK

January 2, 2025•3 min read

How to configure AWS CloudWatch Application Signals centrally for an AWS account and for individual Node.js Lambdas using the AWS CDK.

awsaws application signalscdkcloudwatch

Table of Contents

  • Update 2025-06-18: Provisioning with CloudFormation
  • Use case
  • Setup
  • Account-wide setup
  • Each Lambda function
  • Result

Update 2025-06-18: Provisioning with CloudFormation

It can now also be configured using CloudFormation. https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-applicationsignals-discovery.html

import { CfnDiscovery } from "aws-cdk-lib/aws-applicationsignals"; new CfnDiscovery(this, "ApplicationSignalsDiscovery");
NOTE

With the CfnDiscovery resource now available, the following sections detailing the custom resource implementation are for informational purposes only. They show the workaround that was needed before native CloudFormation support was added.

Use case

This guide shows how to use AWS CloudWatch Application Signals for your Node.js Lambda functions when using the AWS CDK for Infrastructure as Code (IaC).

Setup

In a new AWS account, the Management Console shows two steps to set up 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

To start 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. The AWS CDK does not have a high-level construct for this. However, we can use the AWS SDK via a custom resource to achieve this.

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

Each Lambda function

As described here, each Lambda needs the environment variable AWS_LAMBDA_EXEC_WRAPPER with the value /opt/otel-instrument and the AWSOpenTelemetryDistroJs layer 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);

The Lambda function implementation can then 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 };

⚠️ The documentation currently recommends using CommonJS (CJS) instead of ECMAScript Modules (ESM).

For CommonJS, some details need to be considered, such as how the handler is exported: https://github.com/aws-observability/aws-otel-lambda/issues/284#issuecomment-1465465790

Result

After the setup and a few invocations of the Lambda function, the CloudWatch Application Signals will be visible in the Management Console. This might take a few minutes.

application signals result

Published on January 2, 2025

Share:Bluesky Icon
Categories: aws

Related Posts

Jan 7, 2025

Configure AWS CloudWatch Application Signals Transaction Search with CDK

Dec 13, 2025

Monitor multiple resources using a single CloudWatch Alarm (with CDK)

Feb 2, 2026

TanStack AI with AWS Bedrock on TanStack Start (simple example)

RSS|

© 2026 Johannes Konings. All rights reserved.