CoverTree Secures $13 Million in Series A Funding to Revolutionize Manufactured Home Insurance Read More
 

Usage of AWS CDK Aspects and Lambda Powertools for improved observability

Published on Monday February 10, 2025

AWS Cloud Development Kit (CDK) is a great tool to incorporate Infrastructure as Code (IaC) pattern, which makes management of cloud resources really simple. CoverTree’s infrastructure is built with Serverless technology stack, meaning we heavily rely on AWS Lambdas and lots of different resources. How could we quickly achieve great observability with usage of AWS Lambda Powertools and AWS CDK Aspects? The article will cover an interesting implementation of Powertools within a 200+ Lambdas code base.

Piotr Moszkowicz. Engineering Manager at CoverTree. Has been coding for more than fifteen years, loves to share knowledge and grow other people’s careers. Currently is a member of the AWS Community Builder program within the Serverless category. His main responsibilities at CoverTree are management and growth of the Polish engineering team, day to day development and creating complex serverless systems architecture on AWS Cloud.

What is observability and why is it important especially for Serverless technology stack?

Observability is the ability to understand a system’s internal state by examining its external outputs. Those outputs usually are recorded as logs, metrics and traces. Within serverless-first codebases usually there are tens of various Lambdas, which are interconnected with each other via various methods such as AWS SQS, AWS SNS, AWS EventBridge, direct invokes, HTTP calls and more. For production grade applications it is crucial to ensure the application has reasonable uptime, any bugs are quickly discovered and patched, and we properly collect all technical metrics, to make sure our infrastructure is optimal for the solution. Within the AWS offering there are tools, which are very useful for those purposes. The AWS Cloudwatch allows us to gather and search through logs and create automatic and custom metrics to measure both performance and business related factors. We can leverage AWS X-Ray for tracing capabilities, which allows us to identify which parts of our architecture or code are running slower. AWS Lambda Powertools is a great library enabling usage of those two services via one, concise API.

How to use AWS Lambda Powertools?

Within this article, we will use Node.js and TypeScript as our framework & language of choice. The usage of Logger, Tracer and Metrics components of AWS Lambda Powertools is very simple. Let’s take Logger for instance – you simply instantiate the Logger class outside of your Lambda handler and simply call logging-related methods on the instance.

import { Logger } from '@aws-lambda-powertools/logger';

const logger = new Logger({ serviceName: 'serverlessAirline' });

export const handler = async (_event, _context): Promise => {
  logger.info('Hello World');
};

(example directly grabbed from AWS Lambda Powertools installation section)

As you can see within this simple example, there’s a single argument of serviceName, which is passed to the constructor. Similar configuration is needed for the Metrics component – there we need to set the metricsNamespace parameter. Within single Lambda implementation that is not an issue, however what is we would like to have a utility file, which looks like this:

import { Logger } from "@aws-lambda-powertools/logger";

/**
 * Create logger instance with centralized configuration so that
 * all functions have consistent logging behavior.
 */
const logger = new Logger();

export { logger };

That way we could use the same logger among all of our Lambdas and also within the code, which is shared between those Lambdas, without the need of passing the logger instance reference, or adding it as a global. However, we would need a solution for passing that argument somehow on a lambda-to-lambda basis.

Fortunately within the Utility Settings section of the documentation we can clearly see that it is possible to add the POWERTOOLS_SERVICE_NAME environmental variable, which will set the parameter. We can update our Lambda configuration to simply include it. For AWS CDK it would look like this (some of the configuration parameters are missed for simplicity):

const nodeLambda = new aws_lambda_nodejs.NodejsFunction(
      this,
      "NodeLambda",
      {
        entry: "./file.ts",
        environment: {
          POWERTOOLS_SERVICE_NAME: "OurCustomService"
        },
        handler: "handler",
        tracing: aws_lambda.Tracing.ACTIVE,
      }
    )

Let’s consider the situation, when we have hundreds of Lambdas. Adding that environmental variable for all of them together is a little cumbersome. Let’s call AWS CDK Aspects for the rescue!

AWS CDK Aspects

The AWS CDK Aspects allow to apply an operation to all constructs in a given scope (for example a Stack). They can be used to verify or change all of our constructs. As per documentation Aspects are using visitor pattern, our Aspects needs to be a class that implements IAspect interface.

interface IAspect {
   visit(node: IConstruct): void;
}

We can enable certain Aspects with Aspects.of(SCOPE).add(aspectReference); call. The AWS CDK will call the visit method of the class for every single resource within the SCOPE. Usually aspects are used to verify certain configuration parts of resources (for example using cdk-nag library to maintain security standards for all of our cloud resources) and automated tagging for budgeting purposes. However here we will use AWS CDK Aspect to automatically add environmental variables for AWS Lambda Powertools.

AWS CDK Aspect for AWS Lambda Powertools configuration

Within CoverTree’s codebase we have certain rules around placement of our Lambda handlers. Within the src directory we have multiple directories – each for our CloudFormation stack, the name of the directory is the same as the stack name. Within each stack’s folder we put our lambda handlers.

src/
├─ StackOne/
│  ├─ firstLambdaHandler.ts
│  ├─ secondLambdaHandler.ts
├─ StackTwo/
│  ├─ thirdLambdaHandler.ts

We are able to use that design to our advantage for our Powertools configuration. Also we do have convention for our Lambda handler file naming – it needs to be camelCase and correspond to the purpose of Lambda. If we could simply get the Stack name and put it as our MetricsNamespace argument (used for gathering Metrics), get our Lambda handler file name and put it as our ServiceName, we could group all similar-purposed lambdas in the stacks and ensure, they have same metrics configuration and proper logging configuration. Let’s get to coding.

export class TelemetryEnvAspect implements IAspect {
  /**
   * Returns a string in PascalCase
   * @param str
   */
  static moveToPascalCase(str: string) {
    return startCase(camelCase(str)).replace(/ /g, "");
  }

  visit(node: IConstruct): void {
    if (node instanceof aws_lambda_nodejs.NodejsFunction) {
      const asset = node.node.children.find(
        (child) => child.constructor.name === "Asset"
      ) as aws_s3_assets.Asset | undefined;

      if (!asset) {
        Annotations.of(node).addError("Couldn't find Asset for NodejsFunction");
        return;
      }

      const assetStaging = asset.node.children[0] as AssetStaging | undefined;

      if (!assetStaging) {
        Annotations.of(node).addError("Couldn't find AssetStaging for Asset");
        return;
      }

      // @ts-ignore
      const entry = assetStaging.fingerprintOptions.bundling.props
        .entry as string;

      const [lambdaFileNameWithExtension, mainFolder] = entry
        .split("/")
        .reverse();

      const finalMainFolder = TelemetryEnvAspect.moveToPascalCase(
        mainFolder
      );
      const finalLambdaFileName = TelemetryEnvAspect.moveToPascalCase(
        lambdaFileNameWithExtension.split(".")[0]
      );
      node.addEnvironment("POWERTOOLS_METRICS_NAMESPACE", finalMainFolder);
      node.addEnvironment("POWERTOOLS_SERVICE_NAME", finalLambdaFileName);
    }
  }
}

As you can see, first within the implementation of the visit method we are ensuring that only aws_lambda_nodejs.Nodejsfunction resources are taken into consideration. Next lines are gathering sub-resource of our AWS Lambda – we need to get to the details of AssetStaging to know the entry of the AWS Lambda. After we get it, we simply move it to the PascalCase with the help of moveToPascalCase method. Finally we are using .addEnvironment to add the environmental variable to the AWS Lambda configuration.

The final part of our solution is to add the Aspect to our scope. Within bin/cloud.ts file we are doing those amendments:

const telemetryEnvAspect = new TelemetryEnvAspect();

const ourCustomWhichIncludesLambdasStack = new OurCustomStack(
  app,
  `OutCustomStack`,
);
cdk.Aspects.of(ourCustomWhichIncludesLambdasStack).add(telemetryEnvAspect);

That’s all you need! You can easily use it for your whole codebase allowing quick setup of AWS Lambda Powertools across all the Lambdas thanks to AWS CDK Aspects!

More Articles

October 30, 2024

How CoverTree moved rating engine to AWS Lambda in 3 weeks?

May 8, 2024

Software Architectural Choices & Trade-offs at CoverTree

CoverTree Inc. (CoverTree) is a Program Administrator for CoverTree’s Manufactured Home Program, underwritten by Markel American Insurance Company (Markel), located at 4521 Highwoods Parkway, Glen Allen, VA 23060. CoverTree is acting as the agent of Markel in selling insurance policies. CoverTree receives compensation based on the premiums for the insurance policies sold. Further information is available upon request. Subject to underwriting guidelines, review, and approval. Use of Covertree is subject to our Terms of Use, Privacy Policy, and Licenses.

CoverTree operates in the state of California (CA) as MHTree Insurance Services with CA license# 6009070.

Products and discounts not available to all persons in all states. All decisions regarding any insurance products, including approval for coverage, premium, commissions and fees, will be made solely by the insurer underwriting the insurance under the insurer’s then-current criteria. All insurance products are governed by the terms, conditions, limitations and exclusions set forth in the applicable insurance policy. Please see a copy of your policy for the full terms, conditions and exclusions. Any information on the Site does not in any way alter, supplement, or amend the terms, conditions, limitations, or exclusions of the applicable insurance policy and is intended only as a brief summary of such insurance products. Policy obligations are the sole responsibility of the issuing insurance carrier.

Rating as of March 1, 2022. AM Best ratings are under continuous review and subject to change. Please refer to Markel’s website for the most current information. The rating represents the overall financial status of Markel American Insurance Company, and is not a recommendation of the specific policy provisions, rates or practices of the issuing insurance company.

Copyright © 2022 CoverTree Inc. All rights reserved

×