Writing your own AWS CDK constructs

What are CDK Constructs?

CDK Constructs are components that represent individual or a collection of AWS cloud physical resources. They are code constructs and are building blocks of CDK apps

Why write your own CDK Construct?

  • A CDK L2 construct does not exist for a given AWS resource
    • There are quite a few resources that do not have a L2 construct defined by AWS, yet. For example, AWS Budget is one such resource.https://docs.aws.amazon.com/cdk/api/v1/docs/aws-budgets-readme.html
  • Combine individual AWS resources in order to create a more logical construct
    • For example, you can create a Budget and an Alarm on SNS topic as part of a single construct so that a logical “BudgetAlarm” construct can be created and used across your organization
  • Create a AWS resource as per best practices or resource that complies to policies of your company
    • For example, instead of using the default S3 Bucket construct, you may want to create a custom one which is always encrypted and let developers use this EncryptedBucket construct whenever they want to create new S3 Bucket resources.
  • Build a custom construct library to be reused across all projects within your company
    • You can create a collection of constructs as a library and distribute this on a central repository so that developers across your organization can re(use) them

Initialize your first construct library project

At Fourco, we use typescript as the programming language of choice for CDK. There are various reasons for this:

  • Typescript is better suited for declarative code. Typescript static typing provides hinting that can be very useful during development
  • It is the first supported language by AWS for developing with CDK. So, a lot of example code from AWS is written in Typescript
  • A lot of custom constructs on construct library are in Typescript

The first step is to create an empty directory and initialize a CDK construct project under this directory

mkdir my-cdk-construct
cdk init lib --language=typescript

Running the above command creates a CDK TypeScript Construct Library project that includes

  • A construct named MyCdkConstruct (notice how this is based on the directory name you used to create the project)
  • An interface named MyCdkConstructProps to configure properties of the construct
  • An initialized git repository

Initialize your first construct library project

Write your code

The first thing to modify is the package.json file (the first 2 lines on this file being significant for the setup)

"name": "@myorg/construct-library",
"version": "0.1.0"

The name of the construct library contains 2 parts – the @myorg represents the scope of the project and the second part, construct-library after the / representing the actual name of the construct library. The version number represents the version of the library and needs to be updated every time you make an update to the library and want to publish a newer version. In the next section, we will see how to do this.

Lets first write our construct code (index.ts)

import { Construct } from 'constructs';
import * as budgets from 'aws-cdk-lib/aws-budgets'


export interface BudgetAlarmProps {
  
  // Threshold (defined in percent). 
  // For example, you may want to alert when the usage hits 80% of your budget
  // In this case this value should be 80 
  readonly monthlyThreshold: number;

  // List of emails where budget alarm notifications will be sent
  readonly emails: Array<string>;
  readonly budgetLimit: number;
  readonly notificationType: NotificationType;
}


export class BudgetAlarm extends Construct {
  public readonly topic: sns.Topic;
  public costFilters: any;
  public plannedBudgetLimits: any;  

  constructor(scope: Construct, id: string, props: BudgetAlarmProps) {
    super(scope, id);
    this.topic = new sns.Topic(this, 'BudgetAlarmNotifyTopic');


    interface subscriber {
      address : string
      subscriptionType : string
    }

    var subscriptions:subscriber[] = [];
    props.emails.forEach(email => 
      subscriptions.push(
        {
          address: email,
          subscriptionType: 'EMAIL',
        }        
      )
    )

    const cfnBudget = new budgets.CfnBudget(this, `MonthlyBudget_${id}`, {
      budget: {
        budgetType: 'COST',
        timeUnit: 'MONTHLY',
    
        budgetLimit: {
          amount: props.budgetLimit,
          unit: 'USD',
        },

        plannedBudgetLimits: this.plannedBudgetLimits,
      },
    
      // the properties below are optional
      notificationsWithSubscribers: [{
        notification: {
          comparisonOperator: 'GREATER_THAN',
          notificationType: props.notificationType ? props.notificationType : 'ACTUAL',
          threshold: props.monthlyThreshold ? props.monthlyThreshold : 150,
    
          // the properties below are optional
          thresholdType: 'PERCENTAGE',
        },
        subscribers: subscriptions,
      }],
    });    
  }
}

The main things to remember when writing the construct code are:

  • Create your construct as a Class extending it from Construct
  • Create the actual Cfn constructs as part of your constructor code
  • Default the values that are required or you always want defaulted to a certain value (in the above example, we always create a Monthly budget and in USD and hence we have these value defaulted
  • Your construct properties should be created as an Interface and be passed to the constructor as shown
  • If you have multiple custom constructs as part of this library, put them in separate files and then include those files in the lib/index.ts file

Build, version and publish your library

You can install the dependencies of your project and build the code like you do with any other CDK project

npm install
npm run build

Before you package your library and publish it, you need to version your library

You can do this by either manually updating the version of the project in your package.json

Or use a convenient npm command instead (the below command will update the package version to 0.1.1. Always update the version in order to publish an updated version of your library to the repository

npm version 0.1.1

At this point, you must be wondering about where and how to publish and distribute the library. You will need a private npm registry to host the construct library. We use gitlab registry to publish this but you can look up your own source control documentation as most providers offer a similar registry for publishing your artefacts.

In order to connect to your private registry you need to create a .npmrc file in the root directory of your project and add the configuration details relating to your gitlab projectId. You also need to provide an access token to authenticate to this registry. Please note that the access key your create need to have write permissions to the registry

First, lets look at the contents of your .npmrc file. The value 99999999 represents your Gitlab projectId and needs to be replaced by an actual projectId of your project. The value myorg represents the scope. This can be any unique value that represents the scope of the library, usually representing an organization or a department name. You may have multiple libraries published under a single scope.

@myorg:registry=https://gitlab.com/api/v4/projects/99999999/packages/npm/
//gitlab.com/api/v4/projects/99999999/packages/npm/:_authToken="${NPM_TOKEN}"

Export the access token you created (with write permissions) and this will be used to authenticate npm registry on gitlab before performing any publish operations

export NPM_TOKEN=<access_token_from_gitlab>

We can then simply publish this library by using the command

npm publish

The published library would appear as an entry on your npm registry (in this case on gitlab) with the name format as shown below

@scope/contruct-library@0.1.1

Using custom constructs in your CDK code

Using a custom construct in your CDK code is like using any other L2 construct in your CDK code

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as shared from '@myorg/construct-library'
import * as sns from 'aws-cdk-lib/aws-sns';

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

    const TestBudgetAlarm = new shared.BudgetAlarm(this, "MonthlyBudgetAlarm", {
      budgetLimit: 100,
      monthlyThreshold: 80,
      notificationType: shared.NotificationType.ACTUAL,
      emails: ['alarms@fourco.nl', "alerts@fourco.nl"]
    });

In order for the private library to be resolved by the code, you need to first include this as a dependency and then install dependencies from the private npm registry

In your package.json, mention the dependency to be resolved

  "dependencies": {
    "aws-cdk-lib": "2.53.0",
    "@myorg/construct-library": "^0.1.1",
    "constructs": "^10.0.0",
    "source-map-support": "^0.5.21"
  }

And before you perform an npm install to pull down the dependencies, also let npm know where to pull these dependencies by using the .npmrc file from before

@myorg:registry=https://gitlab.com/api/v4/projects/99999999/packages/npm/
//gitlab.com/api/v4/projects/99999999/packages/npm/:_authToken="${NPM_TOKEN}"

Do not forget to export access token again (this time you only need one with read only access)

 export NPM_TOKEN=<access_token_from_gitlab>

And then you should be able to install and use the construct in your code

npm install @myorg/construct-library

That’s all folks. Do try this at home and let me know if you find this interesting and if you come across issues while trying this out.

Author

Niranjan Manjunath