A Beginner's Guide to Create AWS CDK Construct Library with projen
Introduction
AWS CDK allows you to create your own 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.3.162
- AWS CDK: v1.73.0
What is projen?
projen is a tool for defining and managing increasingly complex project configurations in code.
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 but also continuously updates and maintains these settings.
You can easily start a new project using the pre-defined project types.
As of November 2020, the following project types are supported.
Commands:
projen new awscdk-app-ts AWS CDK app in TypeScript.
projen new awscdk-construct AWS CDK construct library project.
projen new cdk8s-construct CDK8s construct library 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 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.
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
npx: installed 63 in 8.37s
🤖 Created .projenrc.js for AwsCdkConstructLibrary
🤖 Synthesizing project...
$GIT_USER_NAME is not defined
Under the project directory, .projenrc.js
has been created.
const { AwsCdkConstructLibrary } = require('projen');
const project = new AwsCdkConstructLibrary({
authorAddress: "user@domain.com",
authorName: $GIT_USER_NAME,
cdkVersion: "1.60.0",
name: "cdk-sample-lib",
repository: "https://github.com/user/cdk-sample-lib.git",
});
project.synth();
You can add dependencies on AWS CDKs and other modules to be used.
cdkDependencies: [
'@aws-cdk/core',
'@aws-cdk/aws-apigatewayv2',
'@aws-cdk/aws-apigatewayv2-integrations',
'@aws-cdk/aws-lambda'
],
deps: [
'super-useful-lib'
]
If you want to cross-compile to languages other than TypeScript with jsii, add the target language.
python: {
distName: 'cdk-sample-lib',
module: 'cdk_sample_lib',
},
See API.md for other available options.
As an example, the modified file looks like this
const { AwsCdkConstructLibrary } = require('projen');
const PROJECT_NAME = "cdk-sample-lib"
const project = new AwsCdkConstructLibrary({
authorAddress: "hayaok333@gmail.com",
authorName: "hayao-k",
cdkVersion: "1.73.0",
name: PROJECT_NAME,
repository: "https://github.com/hayao-k/cdk-sample-lib.git",
defaultReleaseBranch: 'main',
cdkDependencies: [
'@aws-cdk/core',
'@aws-cdk/aws-apigatewayv2',
'@aws-cdk/aws-apigatewayv2-integrations',
'@aws-cdk/aws-lambda'
],
python: {
distName: PROJECT_NAME,
module: 'cdk_sample_lib',
},
});
project.synth();
Once you have edited .projenrc.js
, run the projen command to reflect the changes (yarn is required).
$ npx projen
npx: installed 63 in 5.491s
🤖 Synthesizing project...
🤖 yarn install --check-files
yarn install v1.22.5
info No lockfile found.
[1/5] Validating package.json...
[2/5] Resolving packages...
[3/5] Fetching packages...
info fsevents@2.2.1: The platform "linux" is incompatible with this module.
info "fsevents@2.2.1" is an optional dependency and failed compatibility check. Excluding it from installation.
[4/5] Linking dependencies...
[5/5] Building fresh packages...
success Saved lockfile.
Done in 20.34s.
🤖 jest: * => ^26.6.3
🤖 @types/jest: * => ^26.0.15
🤖 ts-jest: * => ^26.4.4
🤖 eslint: * => ^7.13.0
🤖 eslint-import-resolver-node: * => ^0.3.4
🤖 eslint-import-resolver-typescript: * => ^2.3.0
🤖 eslint-plugin-import: * => ^2.22.1
🤖 json-schema: * => ^0.2.5
🤖 Synthesis complete
----------------------------------------------------------------------------------------------------
Commands:
BUILD
compile Only compile
watch Watch & compile in the background
build Full release build (test+compile)
TEST
test Run tests
test:watch Run jest in watch mode
eslint Runs eslint against the codebase
RELEASE
compat Perform API compatibility check against latest version
release Bumps version & push to master
docgen Generate API.md from .jsii manifest
bump Commits a bump to the package version based on conventional commits
package Create an npm tarball
MAINTAIN
projen Synthesize project configuration from .projenrc.js
projen:upgrade upgrades projen to the latest version
MISC
start Shows this menu
Tips:
💡 The VSCode jest extension watches in the background and shows inline test results
💡 Install Mergify in your GitHub repository to enable automatic merges of approved PRs
💡 Set `autoUpgradeSecret` to enable automatic projen upgrade pull requests
💡 `API.md` includes the API reference for your library
💡 Set "compat" to "true" to enable automatic API breaking-change validation
You will see that projen automatically generates the package.json, the .gitignore, .npmignore, eslint, jsii configuration, license files, etc., as well as 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.
Whenever you edit these files, you need to modify the .projenrc.js file and re-run the projen command.
If you edit them manually, the build will fail.
$ tree -L 1 -a
.
├── .eslintrc.json
├── .github
├── .gitignore
├── LICENSE
├── .mergify.yml
├── node_modules
├── .npmignore
├── package.json
├── .projenrc.js
├── README.md
├── src
├── test
├── tsconfig.jest.json
├── version.json
├── .versionrc.json
└── yarn.lock
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 November 2020.
The following directory has already been created by projen.
.
├── lib/
├── src/
├── test/
The lib directory will contain the compiled files.
The code for the Lambda functions can also be inserted inline into the CDK code, but in this example create 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;
};
Create the following two files in the src directory
src/index.ts
import { HttpApi } from '@aws-cdk/aws-apigatewayv2';
import { LambdaProxyIntegration } from '@aws-cdk/aws-apigatewayv2-integrations';
import { Code, Function, Runtime } from '@aws-cdk/aws-lambda';
import * as cdk from '@aws-cdk/core';
export class CdkSampleLib extends cdk.Construct {
constructor(scope: cdk.Construct, id: string) {
super(scope, id);
const handler = new Function(this, 'HelloWorld', {
handler: 'index.handler',
code: Code.fromAsset('functions'),
runtime: Runtime.NODEJS_12_X,
});
const api = new HttpApi(this, 'API', {
defaultIntegration: new LambdaProxyIntegration({ handler }),
});
new cdk.CfnOutput(this, 'ApiURL', { value: api.url! });
}
}
src/integ.default.ts
import * as cdk from '@aws-cdk/core';
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 files in the test directory
test/hello.test.ts
import * as cdk from '@aws-cdk/core';
import { CdkSampleLib } from '../src/index';
import '@aws-cdk/assert/jest';
test('create app', () => {
const app = new cdk.App();
const stack = new cdk.Stack(app);
new CdkSampleLib(stack, 'TestStack');
expect(stack).toHaveResource('AWS::Lambda::Function');
expect(stack).toHaveResource('AWS::ApiGatewayV2::Api');
expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration');
});
Unit Test
Various scripts are predefined in the package.json
generated from projen.
Run the test with yarn test
.
yarn 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
yarn run v1.22.5
$ yarn run test && yarn run compile && yarn run package
$ rm -fr lib/ && jest --passWithNoTests --updateSnapshot && yarn run eslint
PASS test/hello.test.ts
✓ create app (197 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: 1 passed, 1 total
Snapshots: 0 total
Time: 4.713 s, estimated 7 s
Ran all test suites.
$ eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern src test
$ jsii --silence-warnings=reserved-word --no-fix-peer-dependencies && jsii-docgen
$ jsii-pacmak
Done in 51.38s.
Once the build is successful, let's try deploying locally.
$ cdk deploy --app='./lib/integ.default.js'
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...
[0%] start: Publishing 20472c64d312cc547a9359d36b04cfc75633027c0b13c3c07b96dfdf1b1c428f:current
[100%] success: Published 20472c64d312cc547a9359d36b04cfc75633027c0b13c3c07b96dfdf1b1c428f:current
MyStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (9/9)
✅ MyStack
Outputs:
MyStack.CdkSampleLibApiURL32C6192A = https://4p9zte6ny8.execute-api.ap-northeast-1.amazonaws.com/
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:123456789012:stack/MyStack/8fc38650-2a78-11eb-8d53-0a6e476ede30
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...
5:27:29 PM | DELETE_IN_PROGRESS | AWS::CloudFormation::Stack | MyStack
✅ MyStack: destroyed
Release
First, commit the changes.
$ git add .
$ git commit -m "feat: release 0.0.1"
yarn release
bumps the version and automatically updates CANGELOG.md
.
Then push to the release branch of GitHub.
$ yarn release
yarn run v1.22.5
$ yarn run --silent no-changes || (yarn run bump && git push --follow-tags origin main)
$ yarn run --silent no-changes || standard-version
✔ bumping version in version.json from 0.0.0 to 0.0.1
✔ Running lifecycle script "postbump"
ℹ - execute command: "yarn run projen && git add ."
🤖 yarn install --check-files
🤖 Synthesis complete
----------------------------------------------------------------------------------------------------
Commands:
BUILD
compile Only compile
watch Watch & compile in the background
build Full release build (test+compile)
TEST
test Run tests
test:watch Run jest in watch mode
eslint Runs eslint against the codebase
RELEASE
compat Perform API compatibility check against latest version
release Bumps version & push to main
docgen Generate API.md from .jsii manifest
bump Commits a bump to the package version based on conventional commits
package Create an npm tarball
MAINTAIN
projen Synthesize project configuration from .projenrc.js
projen:upgrade upgrades projen to the latest version
MISC
start Shows this menu
Tips:
💡 The VSCode jest extension watches in the background and shows inline test results
💡 Install Mergify in your GitHub repository to enable automatic merges of approved PRs
💡 Set `autoUpgradeSecret` to enable automatic projen upgrade pull requests
💡 `API.md` includes the API reference for your library
💡 Set "compat" to "true" to enable automatic API breaking-change validation
✔ created CHANGELOG.md
✔ outputting changes to CHANGELOG.md
✔ committing version.json and CHANGELOG.md and all staged files
✔ tagging release v0.0.1
ℹ Run `git push --follow-tags origin master` to publish
Or you can bump to any version by running yarn bump
.
$ yarn bump --release-as 1.0.0 && git push --follow-tags origin main
The Github Actions workflow definition is also generated when the projen command is executed, which makes it easy to automate the release to the package repository.
Build workflow (.github/workflows/build.yaml) Runs when a pull request is created.
Builds the library and checks for tampering (i.e., manual modification).Release workflow (.github/workflows/release.yaml): It is triggered by push to the release branch.
After building in the release branch, it automatically publishes to the repositories, such as npm and PyPI by jsii-release.
In order for the Release job to work properly, you need to register the Secrets for the language you want to publish in the GitHub repository.
- 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
Try It!
With projen, you can focus on the implementation of 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.
I hope this article will help you.