A Beginner's Guide to Create AWS CDK Construct Library with projen
Tags: aws, tutorial, typescript, awscdk
Published: 2020-11-23 06:29:22

## 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.
```

awscdk-construct creates an environment for building Contruct using [jsii](https://github.com/aws/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
```

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

```js
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();
```

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

```js
  deps: [
    '@aws-cdk/aws-apigatewayv2-alpha',
    '@aws-cdk/aws-apigatewayv2-integrations-alpha',
    'other-useful-lib' 
  ]
```

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

```js
  publishToPypi: {
    distName: 'cdk-sample-lib',
    module: 'cdk_sample_lib',
  },
```

See the [API reference](https://projen.io/api/API.html#projen-awscdk-awscdkconstructlibrary) for other options that can be specified.
As an example, the modified `.projenrc.js` file looks like this

```js
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();
```

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.
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nvtay8nl5h15hm29syfj.png)

## 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/
```

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`

```js
exports.handler = async (event) => {
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};
```

Create the following two files in the src directory.

* `src/index.ts`

```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! });
  }
}
```

* `src/integ.default.ts`

```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');
```

Create the following file in the test directory.

* `test/cdk-sample-lib.test.ts`

```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',
  });
});
```

## 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](https://github.com/aws/jsii-docgen) generates API documentation (API.md) from comments in the code.

In addition, [jsii-pacmak](https://github.com/aws/jsii/tree/main/packages/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)
```

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
```

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!"
```

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
```

## Release
Commit the changes and push the code to GitHub.

```
$ git add . 
$ git commit -m "feat: initial release"
```

projen automatically performs semantic versioning based on [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).

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](https://github.com/cdklabs/publib) is used for releases to the repository.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lhx6hkzvhmez0u7tt6tl.png)
 
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](https://www.youtube.com/channel/UCWGGdWsfeSXwubErtDQSdCg), an AWS Developer Advocate, is very helpful.

{% youtube cTsSXYOYQPw %}

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

{% github hayao-k/cdk-ecr-image-scan-notify %}

I hope this article will help you.