OVERVIEW

In the world of cloud computing, AWS CDK, short for the ‘Cloud Development Kit’, is a game-changer. It’s a tool provided by Amazon Web Services (AWS) that simplifies the process of creating and managing AWS infrastructure. With AWS CDK, you can define cloud resources using familiar programming languages, like TypeScript, instead of manually configuring them using templates or scripts. This makes it easier to work with AWS services and build complex cloud applications.

When it comes to AWS CDK development, leveraging patterns becomes incredibly beneficial. Much like design patterns in software development, AWS CDK patterns act as ready-made solutions to recurring challenges but in cloud development. By using patterns, you can avoid reinventing the wheel and streamline your AWS CDK projects. They help you apply proven solutions to common problems, making your AWS CDK development journey more efficient and manageable. Whether you’re building a simple application or a complex cloud infrastructure, patterns can simplify the process and save you time and effort.

BEFORE WE START

In this article, we are going to consider an in-depth view of cases and best practices from real projects. These examples will illustrate the significance of patterns and offer actionable insights you can apply in your AWS CDK development journey. We’ll explore key areas, including:

  • Lambda Templates: Explore how prebuilt templates can expedite your CDK projects for diverse Lambda applications, allowing you to commence with a solid foundation.
  • Custom AWS CDK App: Explore the concept of a customized CDK App, a universal container for CloudFormation stacks, and how it can enhance organization and deployment management.
  • Stack Cleaner: Learn about a smart solution for stack management, beneficial for the development process, that automatically handles stack clean-up.

LAMBDA TEMPLATE: implementation of the “Template Method” design pattern

In the context of collaborative software development involving multiple teams, the importance of a unified approach becomes evident, and it is applicable to CDK development as well.

Imagine you’re developing a cloud application that utilizes various AWS Lambda functions for different purposes. Initially, it all works smoothly, but as your application grows, managing these functions effectively can become more challenging.

To simplify the development and maintenance of your AWS Lambda functions, it’s better to have a common approach. The `BaseLambda` template might be a good solution. This template serves as a blueprint for creating new AWS Lambda function handlers, providing a standardized foundation that encapsulates shared logic for error handling, logging, and common event processing.

export class BaseLambda {
  static get handler() {
    this.onCreate();
    return this.customHandler.bind(this); 
  }

  protected static onCreate() {...}

  protected static async onRequest(event, context, callback) {...}

  protected static async onError(error) {...}

  protected static async onResponse(response){...}

  protected static async customHandler(event, context, callback) {
    try{
       const requestResult = await this.onRequest(event, context, callback);
       const response = await this.onResponse(requestResult);
	    
       return response;
     } catch(error) {
       return this.onError(error);
     }
   }
 }

As a Template Method pattern, the `BaseLambda` template offers a structured approach. Each AWS Lambda handler should inherit this template and can then selectively override its methods. This approach eliminates code duplication while allowing you to adapt the functionality to meet the unique requirements of each AWS Lambda function. With the implementation of the `BaseLambda` template, you’ve taken significant steps toward simplifying AWS Lambda development. This template ensures that your cloud application runs smoothly, regardless of the different roles your AWS Lambda functions play. Let’s examine a scenario where we not only use but also extend the capabilities of the `BaseLambda` class.

import { BaseLambda } from "./BaseLambda";

export class RestApiLambda extends BaseLambda {
  protected static override async onRequest(event, context) {
      return { statusCode: 200, body: "Request succeeded" };
    }
 
 protected static override async onError(error) {
      return { statusCode: 400, body: `Request failed: ${error?.message}` };
  }
}

The `RestApiLambda` class inherits from `BaseLambda` and configures two main methods: `onRequest` and `onError`. Thus, we create a specialized AWS Lambda template designed to efficiently process REST API requests when working with an Amazon API Gateway.

CUSTOM AWS CDK APP

In AWS CDK, the process of creating a stack involves several steps. One key step is initializing an AWS CDK app, which serves as the foundation for defining and deploying your infrastructure. 

import { App } from 'aws-cdk-lib';
import { CustomCdkStack } from './stacks/CustomCdkStack';

const app = new App();
new CustomCdkStack(app, CustomCdkStack, {});

However, AWS CDK development is a dynamic process that often requires managing complex configurations and context parameters. To address these challenges effectively,  I suggest considering a strategic approach: creating a custom `App`. Having a custom and configurable app gives you the opportunity to create an application that perfectly fits the unique needs of your project. Let’s consider an example of implementing a custom app:

import { App, AppProps, Environment, Tags, Stage } from 'aws-cdk-lib';
import * as os from 'os';

export interface CustomAppProps {
  appName: string;
  appProps?: AppProps;
}

export class CustomApp extends App {
  private _stage: Stage;
  private _nameSpace: string;
  // Define environments for different stages
  get availableEnvironments(): Record<string, Environment> {
    return {
      dev: {
        account: 'dev-account',
        region: 'dev-region'
      },
      prod: {
        account: 'prod-account',
        region: 'prod-region'
      }
    };
  }
  
  constructor(props: CustomAppProps) {
    super(props.appProps);
    this.getConfig();
    this.applyTags(props.appName);
  }
  
  private getConfig() {
    let environment: Environment;
    const stage = this.node.tryGetContext('stage');
    
     if (stage) {
      environment = this.availableEnvironments[stage];
      if (!environment) throw new Error(`The specified stage '${stage}' is not supported.`);
      this._nameSpace = stage;
    } else {
      this._nameSpace = this.node.tryGetContext('namespace') ||    os.userInfo().username;
      if (!this._nameSpace) throw new Error(`No 'stage' was specified & no alternative 'namespace' was provided.`);
      // if we don't have a specified stage but have a name, we can use the default developers environment based on their machine's settings
      environment = {
        account: process.env.CDK_DEFAULT_ACCOUNT,
        region: process.env.CDK_DEFAULT_REGION,
      };
    }
    this._stage = new Stage(this, this._nameSpace, { env: environment });
  }
  
  private applyTags(appName: string) {
    Tags.of(this._stage).add('App', appName);
    Tags.of(this._stage).add('Namespace', this._nameSpace || '');
  }
}

In this code, you see the `CustomApp`, an extension of the standard AWS CDK App. This enhanced class provides several benefits for AWS CDK development:

  1.  With the help of `CustomAppProps` you can take additional parameters that allow you to customize your app as needed.
  2. The `getConfig()` method manages the configuration of your app. It checks if a specific deployment stage is provided in the app’s context. If not, it uses a default or user-provided namespace and sets the environment accordingly. This ensures flexibility and adaptability for different deployment scenarios.
  3. By specifying the custom stage, you have more control over the naming of your stacks, which can be especially useful when managing multiple stacks in different environments or contexts.

    `this._stage = new Stage(this, this._nameSpace, { env: environment });`

    The `Stage` is a construct provided by the AWS CDK that represents an environment where you can deploy your AWS resources. By creating a `Stage` with a specific name and environment settings, you are effectively customizing the context, including the naming of the CloudFormation stacks associated with that environment in which your AWS resources will be deployed.
  4. The `applyTags()` method adds essential tags, such as ‘App’ and ‘Namespace’, to the created stage. These tags help identify resources, making resource management and tracking more straightforward.

By implementing the `CustomApp`, you enhance your AWS CDK development process, making it more adaptable, user-friendly, and efficient for a variety of use cases and deployment scenarios, plus, you can easily expand it as needed, as we’ll discuss next.

STACK CLEANER: as a part of the Custom Application

In a collaborative development environment, especially when multiple team members are actively working on AWS CDK projects, it’s not uncommon to have various stacks deployed for testing, debugging, or development purposes. These stacks are essential for ensuring the reliability and functionality of your cloud infrastructure, however, it can become challenging to manage them effectively.

Imagine having multiple stacks created by different team members, each serving a specific purpose during development. While these stacks are crucial for iterative improvements, they can accumulate over time, potentially incurring unnecessary costs if not properly managed.

This is where a Stack Cleaner comes into play. It’s a proactive solution designed to automatically clean up stacks that were deployed for development purposes after a predefined time frame. By implementing this stack management strategy, you maintain a cleaner and more cost-effective AWS environment. Let’s explore how Stack Cleaner works and how you can incorporate it into your AWS CDK projects for enhanced resource management.

We already have the  `CustomApp`: let’s add to this app the necessary functionality for automatic cleaning. First, let’s create a new method in the `CustomApp` class and call it inside the `getConfig()` method:

import { Aspects } from 'aws-cdk-lib';

export class CustomApp extends App {
  ...
  private getConfig() {
    ...
    const stage = this.node.tryGetContext('stage');
    if (stage) {
      ...
    } else {
      ...
    }
    // Create a stage with the chosen namespace and environment
    this._stage = new Stage(this, this._nameSpace, { env: environment });
  
  // If a stage is not specified it means that it is a local development and we should attach stack destroyer
    if (!stage) {
      this.addStackDestroyer();
    }
  }
  
  private addStackDestroyer() {
    const ttl = this.node.tryGetContext('ttl') || TTL_DEFAULT_VALUE;
    Aspects.of(this._stage).add(
      new StackSearcherAspect((stack: Stack) => {
        // For each stack found, create a StackDestroyer with the specified TTL
        new StackDestroyer(stack, ttl);
      }),
    );
  }
  ...
}

With the help of Amazon CDK Aspects, we can add a destroyer functionality to our stacks. Aspects are a way to attach cross-cutting behavior to constructs or resources within your AWS CDK app. Aspects allow you to apply common functionality or behavior to multiple constructs without modifying their individual code. They are often used for tasks like tagging resources, enforcing security policies, or in this case, managing stacks.

The `StackSearcherAspect` is a custom Aspect, and it is responsible for visiting each construct within your AWS CDK app and identifying if a construct is a Stack. If it finds a Stack, it calls a provided callback function (`StackDestroyer`), which is responsible for destroying this stack.

import { Stack, IAspect } from 'aws-cdk-lib';
import { IConstruct } from 'constructs';

export class StackSearcherAspect implements IAspect {
  constructor(private callback: (stack: Stack) => void) {}
  
  visit(node: IConstruct): void {
    if (Stack.isStack(node)) {
      this.callback(node);
    }
  }
}

The callback function, provided to the `StackSearcherAspect`, creates the `StackDestroyer` for each stack and sets up logic to destroy the stack if its lifetime has expired. 

`StackDestroyer` is a class that creates a specialized AWS Lambda function that’s added to the AWS CDK stack, which we passed as the first parameter (`new StackDestroyer(stack, ttl)`). It is triggered automatically on a schedule using an Amazon CloudWatch Events Rule. The schedule is determined by the `ttl` (time to live) parameter provided during initialization. Stack removal occurs inside the AWS Lambda handler and looks like this:

import * as AWS from 'aws-sdk';

const cloudFormation = new AWS.CloudFormation();
await cloudFormation.deleteStack({ StackName: 'provided_name'}).promise();

Using the AWS SDK for TypeScript, the AWS Lambda function sends a delete request to AWS CloudFormation. The request includes the StackName parameter, specifying the name of the stack to be deleted.

The automated stack cleaner process ensures that development or testing stacks are cleaned up promptly according to the specified `ttl` parameter, preventing them from incurring unnecessary costs and resources after they’ve served their purpose.

CONCLUSION

Developing with the AWS Cloud Development Kit offers excellent solutions but has its challenges. That’s why, as you venture into the world of infrastructure as code, it’s essential to have the right tools and best practices at your disposal. By understanding the core principles and patterns of CDK development, you can unlock the full potential of AWS services and create solutions that are efficient, maintainable, and adaptable. The strategies that are discussed in this article will not only help you simplify your workflow but also empower you to build robust and scalable cloud applications more deliberately.

By Artyom Misyukevich, Software Developer at Klika Tech, Inc.