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.