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).


Top comments (0)