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.
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 ,
);
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);
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 };
⚠️ 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).