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.
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.
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.
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.
publishToPypi: {
distName: 'cdk-sample-lib',
module: 'cdk_sample_lib',
},
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();
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.
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
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-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
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
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 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)
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.
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.
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
, andpackages
. - 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.
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.
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.
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>',
});
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
…I hope this article will help you.
Top comments (2)
@hayao_k, great work 🔥🔥🔥
I was able to publish my first CDK construct to Construct Hub by following your article 💪
awesome