DEV Community

hayao-k for AWS Community Builders

Posted on • Edited on

A Beginner's Guide to Create AWS CDK Construct Library with projen

Introduction

AWS CDK allows you to create your Construct Library and publish it to npm or PyPI.

Using projen makes the development of Construct Library very comfortable.

The content of this article has been tested with the following versions

  • projen: v0.56.33
  • AWS CDK: v2.25.0

What is the projen?

projen is a tool for defining and managing increasingly complex project configurations in code.

https://github.com/projen/projen

With projen, you no longer need to manage files such as package.json by yourself.

projen does not only generate various files during project creation and continuously updates and maintains these settings.

You can quickly start a new project using the pre-defined project types.
As of May 2022, the following project types are supported.
projen can also be used to create non-CDK projects such as React apps.

Commands:
  projen new awscdk-app-java   AWS CDK app in Java.
  projen new awscdk-app-py     AWS CDK app in Python.
  projen new awscdk-app-ts     AWS CDK app in TypeScript.
  projen new awscdk-construct  AWS CDK construct library project.
  projen new cdk8s-app-ts      CDK8s app in TypeScript.
  projen new cdk8s-construct   CDK8s construct library project.
  projen new cdktf-construct   CDKTF construct library project.
  projen new java              Java project.
  projen new jsii              Multi-language jsii library project.
  projen new nextjs            Next.js project without TypeScript.
  projen new nextjs-ts         Next.js project with TypeScript.
  projen new node              Node.js project.
  projen new project           Base project.
  projen new python            Python project.
  projen new react             React project without TypeScript.
  projen new react-ts          React project with TypeScript.
  projen new typescript        TypeScript project.
  projen new typescript-app    TypeScript app.
Enter fullscreen mode Exit fullscreen mode

awscdk-construct creates an environment for building Contruct using jsii.
jsii allows you to generate libraries from TypeScript code to work in Python, Java, and .NET.

Create project

Create a Construct Library project with projen new awscdk-construct.

$ mkdir cdk-sample-lib && cd cdk-sample-lib

$ npx projen new awscdk-construct
👾 Project definition file was created at /home/ec2-user/environment/cdk-sample-lib/.projenrc.js
yarn install v1.22.18
info No lockfile found.
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...

success Saved lockfile.
Done in 38.61s.
yarn install v1.22.18
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...

success Saved lockfile.
Done in 7.21s.

> cdk-sample-lib@0.0.0 eslint
> npx projen eslint

Initialized empty Git repository in /home/ec2-user/environment/cdk-sample-lib/.git/
[main (root-commit) 924b25c] chore: project created with projen
 21 files changed, 7704 insertions(+)
 create mode 100644 .eslintrc.json
 create mode 100644 .gitattributes
 create mode 100644 .github/pull_request_template.md
 create mode 100644 .github/workflows/build.yml
 create mode 100644 .github/workflows/pull-request-lint.yml
 create mode 100644 .github/workflows/release.yml
 create mode 100644 .github/workflows/upgrade-main.yml
 create mode 100644 .gitignore
 create mode 100644 .mergify.yml
 create mode 100644 .npmignore
 create mode 100644 .projen/deps.json
 create mode 100644 .projen/files.json
 create mode 100644 .projen/tasks.json
 create mode 100644 .projenrc.js
 create mode 100644 LICENSE
 create mode 100644 README.md
 create mode 100644 package.json
 create mode 100644 src/index.ts
 create mode 100644 test/hello.test.ts
 create mode 100644 tsconfig.dev.json
 create mode 100644 yarn.lock
Enter fullscreen mode Exit fullscreen mode

Under the project directory, .projenrc.js has been created.

const { awscdk } = require('projen');
const project = new awscdk.AwsCdkConstructLibrary({
  author: 'user',
  authorAddress: 'user@example.com',
  cdkVersion: '2.1.0',
  defaultReleaseBranch: 'main',
  name: 'cdk-sample-lib',
  repositoryUrl: 'https://github.com/user/cdk-sample-lib.git',

  // deps: [],                /* Runtime dependencies of this module. */
  // description: undefined,  /* The description is just a string that helps people understand the purpose of the package. */
  // devDeps: [],             /* Build dependencies for this module. */
  // packageName: undefined,  /* The "name" in package.json. */
});
project.synth();
Enter fullscreen mode Exit fullscreen mode

You can add dependencies on AWS CDKs and other modules to be used.

  deps: [
    '@aws-cdk/aws-apigatewayv2-alpha',
    '@aws-cdk/aws-apigatewayv2-integrations-alpha',
    'other-useful-lib' 
  ]
Enter fullscreen mode Exit fullscreen mode

Add the target language if you want to cross-compile to languages other than TypeScript with jsii.

  publishToPypi: {
    distName: 'cdk-sample-lib',
    module: 'cdk_sample_lib',
  },
Enter fullscreen mode Exit fullscreen mode

See the API reference for other options that can be specified.
As an example, the modified .projenrc.js file looks like this

const { awscdk } = require('projen');

const cdkVersion = '2.25.0';

const project = new awscdk.AwsCdkConstructLibrary({
  author: 'hayao-k',
  authorAddress: '30886141+hayao-k@users.noreply.github.com',
  cdkVersion,
  defaultReleaseBranch: 'main',
  name: 'cdk-sample-lib',
  repositoryUrl: 'https://github.com/hayao-k/cdk-sample-lib.git',
  description: 'Sample AWS CDK Construct Library by projen',
  keywords: ['sample'],
  license: 'Apache-2.0',
  deps: [
    `@aws-cdk/aws-apigatewayv2-alpha@${cdkVersion}-alpha.0`,
    `@aws-cdk/aws-apigatewayv2-integrations-alpha@${cdkVersion}-alpha.0`
  ],
  publishToPypi: {
    distName: 'cdk-sample-lib',
    module: 'cdk_sample_lib',
  },
  stability: 'experimental',
});
project.synth();
Enter fullscreen mode Exit fullscreen mode

Once you have edited .projenrc.js, run the projen command to reflect the changes.

$ npx projen
👾 default | node .projenrc.js
yarn install v1.22.18
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
Done in 14.87s.
Enter fullscreen mode Exit fullscreen mode

You will see that projen automatically generates the package.json, the .gitignore, .npmignore, eslint, jsii configuration, license files, etc., and the creation and installation of the package.json.

You no longer have to copy from an existing project every time you create a new project.

You need to modify the .projenrc.js file and re-run the projen command whenever you edit these files.
If you edit them manually, the build will fail.

Image description

Development

Let's try a simple example of calling Hello World's Lambda from the API Gateway (HTTP API).
Please note that the HTTP API L2 Constructs has an Experimental status as of May 2022.

The following directory has already been created by projen.

.
├── lib/ 
├── src/
├── test/
Enter fullscreen mode Exit fullscreen mode

The code for the Lambda functions can also be inserted inline into the CDK code, but this example creates index.js in the functions directory.

  • functions/index.js
exports.handler = async (event) => {
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};
Enter fullscreen mode Exit fullscreen mode

Create the following two files in the src directory.

  • src/index.ts
import { HttpApi } from '@aws-cdk/aws-apigatewayv2-alpha';
import { HttpLambdaIntegration } from '@aws-cdk/aws-apigatewayv2-integrations-alpha';
import * as cdk from 'aws-cdk-lib';
import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';

export class CdkSampleLib extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    const handler = new Function(this, 'HelloWorld', {
      handler: 'index.handler',
      code: Code.fromAsset('functions'),
      runtime: Runtime.NODEJS_16_X,
    });

    const api = new HttpApi(this, 'API', {
      defaultIntegration: new HttpLambdaIntegration('LambdaIntegration', handler),
    });

    new cdk.CfnOutput(this, 'ApiURL', { value: api.url! });
  }
}
Enter fullscreen mode Exit fullscreen mode
  • src/integ.default.ts
import * as cdk from 'aws-cdk-lib';
import { CdkSampleLib } from './index';

const app = new cdk.App();
const stack = new cdk.Stack(app, 'MyStack');

new CdkSampleLib(stack, 'Cdk-Sample-Lib');
Enter fullscreen mode Exit fullscreen mode

Create the following file in the test directory.

  • test/cdk-sample-lib.test.ts
import { App, Stack } from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { CdkSampleLib } from '../src/index';

const mockApp = new App();
const stack = new Stack(mockApp);
new CdkSampleLib(stack, 'testing-stack');
const template = Template.fromStack(stack);

test('Lambda functions should be configured with properties and execution roles', () => {
  template.hasResourceProperties('AWS::Lambda::Function', {
    Runtime: 'nodejs16.x',
  });

  template.hasResourceProperties('AWS::IAM::Role', {
    AssumeRolePolicyDocument: {
      Statement: [
        {
          Action: 'sts:AssumeRole',
          Effect: 'Allow',
          Principal: {
            Service: 'lambda.amazonaws.com',
          },
        },
      ],
      Version: '2012-10-17',
    },
  });
});

test('HTTP API should be created', () => {
  template.hasResourceProperties('AWS::ApiGatewayV2::Api', {
    ProtocolType: 'HTTP',
  });
});

test('Lambda Integration should be created', () => {
  template.hasResourceProperties('AWS::ApiGatewayV2::Integration', {
    IntegrationType: 'AWS_PROXY',
  });
});
Enter fullscreen mode Exit fullscreen mode

Unit Test

Various scripts are predefined in the package.json generated from projen.

Run the test with yarn test (npx projen test).
yarn build (npx projen build) also run test, so omit the example output here.

Build

Run yarn build and compile TypeScript to the jsii module.

jsii-docgen generates API documentation (API.md) from comments in the code.

In addition, jsii-pacmak creates language-specific public packages in the dist directory.

$ yarn build
👾 build » default | node .projenrc.js
yarn install v1.22.18
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...

success Saved lockfile.
Done in 55.48s.
👾 build » compile | jsii --silence-warnings=reserved-word
👾 build » post-compile » docgen | jsii-docgen -o API.md
👾 build » test | jest --passWithNoTests --all --updateSnapshot
 PASS  test/cdk-sample-lib.test.ts (9.805 s)
  ✓ Lambda functions should be configured with properties and execution roles (3 ms)
  ✓ HTTP API should be created (1 ms)
  ✓ Lambda Integration should be created (1 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |     100 |      100 |     100 |     100 |                   
 index.ts |     100 |      100 |     100 |     100 |                   
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        10.121 s
Ran all test suites.
👾 build » test » eslint | eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern src test build-tools .projenrc.js
👾 build » package | if [ ! -z ${CI} ]; then mkdir -p dist && rsync -a . dist --exclude .git --exclude node_modules; else npx projen package-all; fi
👾 package-all » package:js | jsii-pacmak -v --target js
[jsii-pacmak] [INFO] Found 1 modules to package
[jsii-pacmak] [INFO] Packaging NPM bundles
[jsii-pacmak] [INFO] Loading jsii assemblies and translations
[jsii-pacmak] [INFO] Packaging 'js' for cdk-sample-lib
[jsii-pacmak] [INFO] js finished
[jsii-pacmak] [INFO] Packaged. load jsii (2.1s) | npm pack (0.4s) | js (0.0s) | cleanup (0.0s)
👾 package-all » package:python | jsii-pacmak -v --target python
[jsii-pacmak] [INFO] Found 1 modules to package
[jsii-pacmak] [INFO] Packaging NPM bundles
[jsii-pacmak] [INFO] Loading jsii assemblies and translations
[jsii-pacmak] [INFO] Packaging 'python' for cdk-sample-lib
[jsii-pacmak] [INFO] python finished
[jsii-pacmak] [INFO] Packaged. python (15.9s) | load jsii (1.9s) | npm pack (0.4s) | cleanup (0.0s)
Enter fullscreen mode Exit fullscreen mode

Once the build is successful, let's try deploying locally.

$ cdk deploy --app='./lib/integ.default.js'

✨  Synthesis time: 1.27s

current credentials could not be used to assume 'arn:aws:iam::123456789012:role/cdk-hnb659fds-lookup-role-123456789012-ap-northeast-1', but are for the right account. Proceeding anyway.
(To get rid of this warning, please upgrade to bootstrap version >= 8)
current credentials could not be used to assume 'arn:aws:iam::123456789012:role/cdk-hnb659fds-deploy-role-123456789012-ap-northeast-1', but are for the right account. Proceeding anyway.
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬────────────────────────────────────────────────────────────────────┬────────┬───────────────────────┬──────────────────────────────────┬─────────────────────────────────────────────────────────────────────┐
│   │ Resource                                                           │ Effect │ Action                │ Principal                        │ Condition                                                           │
├───┼────────────────────────────────────────────────────────────────────┼────────┼───────────────────────┼──────────────────────────────────┼─────────────────────────────────────────────────────────────────────┤
│ + │ ${Cdk-Sample-Lib/HelloWorld.Arn}                                   │ Allow  │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com │ "ArnLike": {                                                        │
│   │                                                                    │        │                       │                                  │   "AWS:SourceArn": "arn:${AWS::Partition}:execute-api:${AWS::Region │
│   │                                                                    │        │                       │                                  │ }:${AWS::AccountId}:${CdkSampleLibAPI6FD5D6E6}/*/*"                 │
│   │                                                                    │        │                       │                                  │ }                                                                   │
├───┼────────────────────────────────────────────────────────────────────┼────────┼───────────────────────┼──────────────────────────────────┼─────────────────────────────────────────────────────────────────────┤
│ + │ ${Cdk-Sample-Lib/HelloWorld/ServiceRole.Arn}                       │ Allow  │ sts:AssumeRole        │ Service:lambda.amazonaws.com     │                                                                     │
└───┴────────────────────────────────────────────────────────────────────┴────────┴───────────────────────┴──────────────────────────────────┴─────────────────────────────────────────────────────────────────────┘
IAM Policy Changes
┌───┬──────────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│   │ Resource                                 │ Managed Policy ARN                                                             │
├───┼──────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${Cdk-Sample-Lib/HelloWorld/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴──────────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)? y
MyStack: deploying...
current credentials could not be used to assume 'arn:aws:iam::123456789012:role/cdk-hnb659fds-deploy-role-123456789012-ap-northeast-1', but are for the right account. Proceeding anyway.
[0%] start: Publishing 91983f53eff8228528504631ea088fb748d797c41b9117601e9b1ed390057a51:current_account-current_region
[0%] start: Publishing 9c1606accb41e40678fb0f9503bf3fbfe23c7c6f77c1550c8421da0b84444171:current_account-current_region
current credentials could not be used to assume 'arn:aws:iam::123456789012:role/cdk-hnb659fds-file-publishing-role-123456789012-ap-northeast-1', but are for the right account. Proceeding anyway.
current credentials could not be used to assume 'arn:aws:iam::123456789012:role/cdk-hnb659fds-file-publishing-role-123456789012-ap-northeast-1', but are for the right account. Proceeding anyway.
[50%] success: Published 91983f53eff8228528504631ea088fb748d797c41b9117601e9b1ed390057a51:current_account-current_region
[100%] success: Published 9c1606accb41e40678fb0f9503bf3fbfe23c7c6f77c1550c8421da0b84444171:current_account-current_region
MyStack: creating CloudFormation changeset...

 ✅  MyStack

✨  Deployment time: 68.01s

Outputs:
MyStack.CdkSampleLibApiURL32C6192A = https://4p9zte6ny8.execute-api.ap-northeast-1.amazonaws.com/
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:123456789012:stack/MyStack/a41998b0-de9f-11ec-88d1-0afcbfc50359

✨  Total time: 69.28s
Enter fullscreen mode Exit fullscreen mode

You can check the response of the Lambda function from the output API URL.

$ curl https://4p9zte6ny8.execute-api.ap-northeast-1.amazonaws.com/
"Hello from Lambda!"
Enter fullscreen mode Exit fullscreen mode

To remove it, run the cdk destory.

$ cdk destroy --app='./lib/integ.default.js'   
Are you sure you want to delete: MyStack (y/n)? y
MyStack: destroying...
current credentials could not be used to assume 'arn:aws:iam::123456789012:role/cdk-hnb659fds-deploy-role-123456789012-ap-northeast-1', but are for the right account. Proceeding anyway.

 ✅  MyStack: destroyed
Enter fullscreen mode Exit fullscreen mode

Release

Commit the changes and push the code to GitHub.

$ git add . 
$ git commit -m "feat: initial release"
Enter fullscreen mode Exit fullscreen mode

projen automatically performs semantic versioning based on Conventional Commits.

For example

  • fix: bump PATCH version (v0.0.1)
  • feat: bump MINOR version (v0.1.0)

MAJAR version must be explicitly bumped by adding majorVersion: x to .projenrc.js to protect users from critical changes.

The Github Actions workflow definition is also generated when the projen command is executed, making it easy to automate the release to the package repository.

  • Build workflow (.github/workflows/build.yaml)
    It runs when a pull request is created.
    Builds the library and checks for tampering (i.e., manual modification).

  • Release workflow (.github/workflows/release.yaml):
    git push to the release branch triggers it.
    Builds the library and checks for tampering (i.e., manual modification).
    Bumping of Release Version by Conventional Commits.
    Create changelog.
    Automated releases to various package repositories such as GitHub Releases, npm, and PyPI.

publib is used for releases to the repository.

Image description

For Workflow to work correctly, Personal Access Token used by projen and API_KEY or Token corresponding to the repository to which it is published must be registered in Actions secrets.

  • PAT for projen: PROJEN_GITHUB_TOKEN (Scopes are repo, workflows, and packages.
  • npm: NPM_TOKEN
  • .NET: NUGET_API_KEY
  • Java: MAVEN_GPG_PRIVATE_KEY, MAVEN_GPG_PRIVATE_KEY_PASSPHRASE, MAVEN_PASSWORD, MAVEN_USERNAME, MAVEN_STAGING_PROFILE_ID
  • Python: TWINE_USERNAME, TWINE_PASSWORD

Publish to Construct Hub

Construct Hub is a registry site for discovering and sharing custom Construct Libraries from the community, AWS, and AWS partners.

GA last December on the same day as AWS CDK v2. Currently, more than 1000 Construct Libraries are listed.

https://constructs.dev/

The following conditions must be met to publish your library on Construct Hub.

  • JSII-compatible
  • Open source license
    • Apache, BSD, EPL, MPL-2.0, ISC, and CDDL or MIT
  • Published to the npm Registry using the CDK Keywords.
    • cdk, awscdk, aws-cdk, cdk8s, or cdktf

If a library meets these requirements, it will be automatically detected and published to Construct Hub in about 30 minutes.

Libraries created and published using projen will meet these requirements as long as they are under the applicable open source license. Therefore, they will be listed on Construct Hub without any special handling.

Try It!

With projen, you can focus on implementing the Construct Library (and, of course, on the regular CDK App).

Do you want to understand how to use projen in videos?
The following video published by @pahud, an AWS Developer Advocate, is very helpful.

I recently published a construct library called cdk-ecr-image-scan-notify using projen.

GitHub logo hayao-k / cdk-ecr-image-scan-notify

cdk-ecr-image-scan-notify is an AWS CDK construct library that notify the slack channel of Amazon ECR image scan results.

NPM version PyPI version Release

cdk-ecr-image-scan-notify

cdk-ecr-image-scan-notify is an AWS CDK construct library that notify the slack channel of Amazon ECR image scan results.

Overview

Amazon EventBridge (CloudWatch Events) detects the image scan execution and starts the Lambda function.
The Lambda function summarizes the scan results, formatting them and notifying Slack.

Basic scanning

Enhanced scanning (Support for initial scan only)

Click on an image name to go to the scan results page.

Getting Started

TypeScript

Installation

$ yarn add cdk-ecr-image-scan-notify

Usage

import * as cdk from 'aws-cdk-lib';
import { EcrImageScanNotify } from 'cdk-ecr-image-scan-notify';

const mockApp = new cdk.App();
const stack = new cdk.Stack(mockApp, '<your-stack-name>');

new EcrImageScanNotify(stack, 'ecr-image-scan-notify', {
  webhookUrl: '<your-incoming-webhook-url>',
});
Enter fullscreen mode Exit fullscreen mode

Deploy!

$ cdk deploy

Python

Installation

$ pip install cdk-ecr-image-scan-notify

Usage

import aws_cdk as cdk
from cdk_ecr_image_scan_notify import EcrImageScanNotify
app = cdk
Enter fullscreen mode Exit fullscreen mode

I hope this article will help you.

Top comments (2)

Collapse
 
rafalkrolxyz profile image
Rafal Krol

@hayao_k, great work 🔥🔥🔥

I was able to publish my first CDK construct to Construct Hub by following your article 💪

Collapse
 
guan840912 profile image
Neil Kuan

awesome