Custom compliance implementation in AWS CDK

AWS CDK accelerates cloud development using common programming languages to model your applications. I had a series of posts using CDK to demonstrate Building serverless web applications with AWS Serverless. Because CDK uses a programming language to model your application, you can encapsulate your library via Constructs, and then reuse it crossing the entire application.

Meanwhile, you can create your own constructs to encapsulate the compliance requirements to simplify the code. For example, in our solution, I used the construct SolutionFunction to force using the same Node.js version(18.x), architecture(ARM64), Lambda logging configuration(JSON log), environment variables for Powertools Logger and so on crossing all NodejsFunction. In addition, using Aspects and escape hatches to make sure the application meets the compliance requirements.

Let's deep dive into how to make all Nodejs Lambda functions compliant with the above requirements.

Firstly, define the SolutionFunction for making a generic configuration of solutions's Nodejs Lambda,

 1export class SolutionNodejsFunction extends NodejsFunction {
 2
 3  constructor(scope: Construct, id: string, props?: NodejsFunctionProps) {
 4    super(scope, id, {
 5      ...props,
 6      bundling: props?.bundling ? {
 7        ...props.bundling,
 8        externalModules: props.bundling.externalModules?.filter(p => p === '@aws-sdk/*') ?? [],
 9      } : {
10        externalModules: [],
11      },
12      runtime: Runtime.NODEJS_18_X,
13      architecture: Architecture.ARM_64,
14      environment: {
15        ...POWERTOOLS_ENVS,
16        ...(props?.environment ?? {}),
17      },
18      logRetention: props?.logRetention ?? RetentionDays.ONE_MONTH,
19      logFormat: 'JSON',
20      applicationLogLevel: props?.applicationLogLevel ?? 'INFO',
21    });
22  }
23}

Then, add an Aspect to the application to make sure the NodejsFunction functions are an instance of SolutionFunction.

 1class NodejsFunctionSanityAspect implements IAspect {
 2
 3  public visit(node: IConstruct): void {
 4    if (node instanceof NodejsFunction) {
 5      if (!(node instanceof SolutionNodejsFunction)) {
 6        Annotations.of(node).addError('Directly using NodejsFunction is not allowed in the solution. Use SolutionNodejsFunction instead.');
 7      }
 8      if (node.runtime != Runtime.NODEJS_18_X) {
 9        Annotations.of(node).addError('You must use Nodejs 18.x runtime for Lambda with javascript in this solution.');
10      }
11    }
12  }
13}
14Aspects.of(app).add(new NodejsFunctionSanityAspect());

The above code snippets help us to archive the compliance of Nodejs Lambda functions without modifying tens or hundreds of occurrences one by one.

However, due to service availability, the ARM64 architect and JSON log Lambda function are not available in the AWS China partition. Also, using another Aspect with escape hatches to override the attributes with conditional values.

 1class CNLambdaFunctionAspect implements IAspect {
 2
 3  private conditionCache: { [key: string]: CfnCondition } = {};
 4
 5  public visit(node: IConstruct): void {
 6    if (node instanceof Function) {
 7      const func = node.node.defaultChild as CfnFunction;
 8      if (func.loggingConfig) {
 9        func.addPropertyOverride('LoggingConfig',
10          Fn.conditionIf(this.awsChinaCondition(Stack.of(node)).logicalId,
11            Fn.ref('AWS::NoValue'), {
12              LogFormat: (func.loggingConfig as CfnFunction.LoggingConfigProperty).logFormat,
13              ApplicationLogLevel: (func.loggingConfig as CfnFunction.LoggingConfigProperty).applicationLogLevel,
14              LogGroup: (func.loggingConfig as CfnFunction.LoggingConfigProperty).logGroup,
15              SystemLogLevel: (func.loggingConfig as CfnFunction.LoggingConfigProperty).systemLogLevel,
16            }));
17      }
18      if (func.architectures && func.architectures[0] == Architecture.arm64) {
19        func.addPropertyOverride('Architectures',
20          Fn.conditionIf(this.awsChinaCondition(Stack.of(node)).logicalId,
21            Fn.ref('AWS::NoValue'), func.architectures));
22      }
23    }
24  }
25
26  private awsChinaCondition(stack: Stack): CfnCondition {
27    const conditionName = 'AWSCNCondition';
28    // Check if the resource already exists
29    const existingResource = this.conditionCache[stack.artifactId];
30
31    if (existingResource) {
32      return existingResource;
33    } else {
34      const awsCNCondition = new CfnCondition(stack, conditionName, {
35        expression: Fn.conditionEquals('aws-cn', stack.partition),
36      });
37      this.conditionCache[stack.artifactId] = awsCNCondition;
38      return awsCNCondition;
39    }
40  }
41}
42Aspects.of(app).add(new CNLambdaFunctionAspect());

Alright, using the above two aspects forces the solution to meet the compliance requirements of Lambda functions with the same runtime version, architecture, and logger configuration. 🤩 😄 🤩