Breaking Bad Practices with CDK Aspects

·

10 min read

In the ever-evolving landscape of cloud infrastructure, AWS Cloud Development Kit (CDK) continues to stand as a groundbreaking tool, simplifying the process of defining cloud resources. Within this universe, Aspects—a feature within CDK—hold a distinctive position. Aspects act as autonomous agents within your CDK constructs, systematically traversing and applying consistent modifications.

This article aims to dissect the intricacies of CDK Aspects, shedding light on their fundamental purpose, operation, and advanced use cases. Aspects, in many ways, embody the balance between consistency and flexibility in cloud infrastructure development, a concept that's growing increasingly important in this era of complex, scalable applications.

Whether you are a seasoned AWS CDK user or a newcomer looking to expand your cloud development toolkit, this deep dive into CDK Aspects will provide valuable insights into this powerful feature. As we peel back the layers, you'll discover how Aspects can enhance resource management, improve security protocols, and promote code efficiency. Let's 'cook' up some knowledge on CDK Aspects.

The example code for this repository is located here: github.com/martzcodes/blog-aspects

This article by @JannikWempe is another great resource: https://aws.hashnode.com/the-power-of-aws-cdk-aspects

Understanding CDK Aspects - The Basics

At its core, the AWS Cloud Development Kit (CDK) is a software development framework that allows developers to define cloud infrastructure in code. This is where CDK Aspects come into play. Aspects are a feature within CDK that act like intelligent filters, traversing your code, identifying specific constructs, and applying modifications to them.

Consider this simple TypeScript code snippet of an AWS S3 bucket defined using CDK:

import { Stack, StackProps } from 'aws-cdk-lib';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

export class MyCDKStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    new Bucket(this, 'MyBucket', {
      versioned: false,
    });
  }
}

Now, let's say you want to ensure that every S3 bucket in your CDK application has versioning enabled. With CDK Aspects you can do this in two ways.

  1. Validation - Create a CDK Aspect to validate all Buckets in the Stack and throw an Error if it is not set

  2. Modification - Create a CDK Aspect to automatically set the versioned property on all buckets

In this context, your CDK code is like a complex chemistry experiment that needs careful management to prevent unwanted reactions.

  1. Validation (the Chemistry Professor): In the validation role, the Chemistry Professor is like a vigilant observer, watching you perform your experiment. If they notice you're about to mix incompatible substances or your measurements are incorrect, they intervene immediately (throw an error) to prevent a potential disaster and ensure the effectiveness of your experiment.

  2. Modification (the Chemistry Assistant): In the lab, an Assistant stands by to help with the experiment, offering a slight adjustment to ensure the reactions go as planned. They don't conduct the experiment for you but provide just enough assistance to keep you on track. This is akin to the modification role of Aspects, which scan your constructs and make slight but important tweaks to ensure consistency and conformity to standards.

To draw the parallel back to our CDK Aspects, the Aspect could, like a Chemistry Assistant, automatically adjust certain aspects of your resources (like enabling versioning for all S3 buckets), ensuring your infrastructure maintains the proper 'formula' throughout its configuration.

Here's how you might define that Aspect:

// For validation
export class ValidateVersioningAspect implements IAspect {
  public visit(node: IConstruct): void {
    if (node instanceof CfnBucket) {
      if (!node.versioningConfiguration
        || (!Tokenization.isResolvable(node.versioningConfiguration)
            && node.versioningConfiguration.status !== 'Enabled')) {
              Annotations.of(node).addError('Bucket versioning is not enabled');
      }
    }
  }
}

const app = new App();
const stack = new MyCDKStack(app, 'MyStack');
Aspects.of(stack).add(new ValidateVersioningAspect());

In this code, the ValidateVersioningAspect Aspect will add an error Annotation if it finds an S3 bucket with versioning disabled, ensuring that all S3 buckets comply with the requirement for versioning to be enabled. On synth, the error would look like this:

[Error at /MyTestStack/MyBucket/Resource] Bucket versioning is not enabled

Found errors

Annotations can also be tested and have support from CDK's built-in assertions: docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk..

// For modification
export class EnableVersioningAspect implements IAspect {
  public visit(node: IConstruct): void {
    if (node instanceof CfnBucket) {
      if (!node.versioningConfiguration
        || (!Tokenization.isResolvable(node.versioningConfiguration)
            && node.versioningConfiguration.status !== 'Enabled')) {
              node.versioningConfiguration = {
                status: 'Enabled'
              };
      }
    }
  }
}

const app = new App();
const stack = new MyCDKStack(app, 'MyStack');
Aspects.of(stack).add(new EnableVersioningAspect());

In this code, the EnableVersioningAspect class defines an Aspect that will "visit" every construct in the stack. If the construct is an instance of the Bucket class, the Aspect will set its versioned property to true, effectively enabling versioning for every bucket in the stack.

Deep Dive into CDK Aspects

In this section, we'll delve deeper into the inner workings of CDK Aspects and uncover the magic behind the scenes. We'll explain the crucial role of the visit method, discuss the Aspects.of(scope).add(aspect) pattern, and illustrate how Aspects interact with the CDK's synthesis process. When using Aspects, it's crucial to be aware of how the AWS CDK uses tokenization to manage resources' properties.

The visit Method

At the heart of any CDK Aspect is the visit method. As an implementer of the IAspect interface, this method is called when the Aspect is applied to a construct. The visit method takes a single argument—IConstruct—which is the construct the Aspect is visiting. What you do inside this method is the meat of your Aspect: whether you choose to throw an error for validation, or modify a property of the construct.

The Intricacies of Tokenization in AWS CDK

Tokens are placeholders used by the CDK to represent values that are not known until deployment time. For example, if you create an S3 bucket without specifying a bucket name, CDK generates a unique name and represents it with a token in your code.

When you inspect the bucketName property during the visit method, you might expect to see an actual bucket name. However, you'll instead see a token, something like ${Token[TOKEN.12]}.

The tokenization system can lead to unexpected results when using Aspects. For instance, if you attempt to modify a property that uses a token, your Aspect might not behave as expected. This is because tokens aren't resolved until the CDK synthesizes your app into a CloudFormation template.

Here's an example:

export class TokenAwareAspect implements IAspect {
  visit(node: IConstruct): void {
    if (node instanceof Bucket) {
      console.log(`Bucket name is ${node.bucketName}`);
    }
  }
}

const app = new App();
const stack = new Stack(app, 'MyStack');
new Bucket(stack, 'MyBucket');
Aspects.of(stack).add(new TokenAwareAspect());

In the console output, you'll see a token as the bucket name, not a real bucket name. Keep this in mind when designing your Aspects!

Applying Aspects

The Aspects.of(scope).add(aspect) pattern is the standard way to apply an Aspect to a construct. In this pattern, Aspects.of(scope) returns an Aspects object associated with a construct, and add(aspect) adds an Aspect to this object. The scope here could be any construct to which you want to apply the Aspect—typically an instance of a Stack or an App.

Aspects and the Synthesis Process

CDK Aspects play a crucial role during the CDK's synthesis process. The synthesis process is a multi-stage operation where CDK translates your code into a CloudFormation template, which AWS can understand. During this process, Aspects are invoked after the construct tree has been fully initialized, but before synthesis. This allows Aspects to validate or modify constructs right before the CloudFormation templates are generated.

Just as Skyler White had to understand the sequence of money laundering, let's delve deeper into the sequence of CDK Aspects with a diagram.

sequenceDiagram
    participant User
    participant CDK App
    participant Aspect
    participant CloudFormation
    User->>CDK App: Runs CDK Synth
    CDK App->>CDK App: Initializes Construct Tree
    CDK App->>Aspect: Invokes Aspects
    Aspect-->>CDK App: Validates/Modifies Constructs
    CDK App->>CloudFormation: Generates CloudFormation Template

CDK Aspects in Action: An Architecture Diagram Generator

In this section, we'll explore a concrete example of using CDK Aspects in a real-world scenario. We'll delve into the internals of a recently published npm library, @aws-community/arch-dia, which uses a CDK Aspect to generate a pseudo-architecture diagram of a project. Not only does it visualize your AWS infrastructure, but it also tracks changes between synthesis stages, providing a visual diff.

The Architecture Diagram Aspect

The key component in @aws-community/arch-dia is the ArchitectureDiagramAspect, an implementation of the IAspect interface. This Aspect traverses the constructs in a given Stack to generate a Mermaid diagram representing the architecture of your AWS resources. Here's an overview of the code:

export class ArchitectureDiagramAspect implements IAspect {
  private readonly mermaidDiagram: string[];
  private stackName = '';

  constructor () {
    this.mermaidDiagram = [];
  }

  visit (node: IConstruct): void {
    if (node instanceof Stack) {
      this.stackName = node.stackName;
      this.traverseConstruct(node, '');
    }
  }
  ...
}

This Aspect, like all Aspects, has a visit method. It checks if the visited construct is an instance of the Stack class. If it is, it initiates a traversal of the constructs in that stack.

The traverseConstruct method iteratively visits all children of a given construct, building up a Mermaid diagram string in the process:

private traverseConstruct (construct: IConstruct, parentPath: string): void {
  ...
  construct.node.children.forEach((child) => {
    this.traverseConstruct(child, currentPath);
  });
}

Generating and Comparing Diagrams

Once all constructs have been visited, the Aspect can generate a Mermaid diagram of the entire Stack using the generateDiagram method. This method also handles comparing the newly generated diagram with the previous one, if it exists, to create a visual diff:

generateDiagram (): string {
  ...
  const addedElements = [...newElements].filter((e) => !oldElements.has(e));
  const removedElements = [...oldElements].filter((e) => !newElements.has(e));
  const added = this.mermaidDiagram.filter((line) => !old.includes(line));
  const removed = old.filter((line) => !this.mermaidDiagram.includes(line));
  ...
}

This visual diff highlights the changes between the old and new architectures, providing a clear visualization of how your resources have evolved.

By traversing the constructs in a Stack, @aws-community/arch-dia can generate a visual representation of your AWS resources and track changes over time. This not only aids in understanding and documenting your infrastructure but can also serve as a powerful tool for communicating changes to stakeholders.

Best Practices and Tips: Using CDK Aspects Effectively

Once you've got a handle on the basics of CDK Aspects, here are a few additional best practices and tips to help you use them more effectively.

1. Use Aspects for Cross-Cutting Concerns

CDK Aspects are ideal for applying changes or enforcing rules that cut across different layers or types of resources in your infrastructure. Consider using Aspects when you want to apply a consistent policy or setting across multiple resources, especially when they are of different types.

2. Be Cognizant of the Construct Tree Traversal

CDK Aspects traverse the construct tree using a depth-first approach. This means that the visit method is invoked on a construct only after it has been invoked on all of its children. In certain scenarios, you may need to be aware of this order of traversal to achieve the desired results.

3. Account for CDK Tokenization

As we discussed earlier, the CDK uses tokenization to handle values that aren't known until deployment time. Be aware of this while designing your Aspects, especially when inspecting or modifying properties that might be tokenized. If necessary, consider using the Token.isUnresolved method to check if a value is a token.

4. Avoid Making Changes Outside the Visit Method

The visit method is the only place where you should make changes to constructs when using Aspects. While it might be technically possible to modify constructs outside this method, doing so can lead to unexpected behavior and hard-to-debug issues.

5. Test Your Aspects

As with any code, you should thoroughly test your Aspects. Given that Aspects can modify constructs across your app, a small error in an Aspect can have a broad impact. Consider using the AWS CDK's built-in testing tools, like the aws-cdk-lib/assertions library, to write unit tests for your Aspects.

My example code includes tests for the aspects: https://github.com/martzcodes/blog-aspects/tree/main/test as does the @aws-community/arch-dia library

CDK Aspects offer a powerful way to enforce consistency and automate modifications across your cloud infrastructure code. With these best practices and tips, you'll be well-equipped to use Aspects effectively in your AWS CDK applications. 🚀

Wrap Up

To wrap it up, here are some final thoughts:

  1. Harnessing the Power of Aspects: CDK Aspects are a powerful tool for AWS developers, offering a way to automate cross-cutting concerns and enforce consistency across an entire application. While they may seem complex at first, understanding how they work and how to use them effectively can greatly enhance your AWS CDK toolkit.

  2. Exploration and Experimentation: Don't be afraid to explore and experiment with Aspects. Whether you're trying to create an architecture diagram generator or a recursive Aspect, there's a lot of potential for creative and effective solutions.

  3. Caution and Diligence: Despite their power, it's important to be cautious when working with Aspects. Be aware of potential pitfalls, such as tokenization and the performance implications of using Aspects. Always test your Aspects thoroughly to avoid introducing broad-reaching errors into your application.

In the end, CDK Aspects can be your 'Heisenberg' in managing cloud infrastructure - they have the potential to be influential, powerful, and transforming. They provide a way to simplify and automate many tasks that would otherwise require manual, error-prone work.