はじめに
AWS CDK では 独自の Construct Library を作成し、npm や PyPI に公開することができます。
私も いくつか Construct Library を公開しているのですが、projen を使用することで Construct Library の作成~リリースがとても捗ったのでご紹介します。
2022/5/29 以下のバージョン時点の内容に更新しました。
- projen: v0.56.33
- AWS CDK: v2.25.0
What is projen?
projen は近年複雑化しているプロジェクト構成をコードで定義し、管理するためのツールです。
元 AWS のエンジニアで AWS CDK の作成者である Elad Ben-Israel 氏を中心に開発されています。
projen では package.json
、 .gitignore
等、通常は自分で管理する必要のある多くのプロジェクトファイルを自動で管理します。ジェネレーターとしてプロジェクト作成時に各種ファイルを生成するだけでなく、projen が継続してこれらの構成を更新し、維持します。
あらかじめ定義されているプロジェクトタイプを使用して新規プロジェクトを簡単に開始できます。
2022/5 時点で以下のプロジェクトタイプをサポートしています。
React アプリなど CDK 以外のプロジェクト作成にも使用できます。
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 は jsii を使用した Contruct をビルドするための環境を作成します。
jsii により TypeScript のコードから Python, Java, .NET で動作するライブラリを生成できます。
Create project
新規プロジェクトの作成
projen new awscdk-construct
で Construct Library プロジェクトを作成します。
$ 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
この時点でプロジェクトディレクトリ配下に、.projenrc.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();
.projenrc.js の修正
使用する AWS CDK やその他のモジュールの依存関係を追加できます。
deps: [
'@aws-cdk/aws-apigatewayv2-alpha',
'@aws-cdk/aws-apigatewayv2-integrations-alpha',
'other-useful-lib'
]
jsii で TypeScript 以外の言語にクロスコンパイルを行う場合はターゲット言語を追加します。
publishToPypi: {
distName: 'cdk-sample-lib',
module: 'cdk_sample_lib',
},
その他に指定可能なオプションについては API リファレンス をご確認ください。
参考例として、修正後のファイルは以下のようになります。
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();
projen コマンドの実行
.projenrc.js
を編集したら、projen コマンドを実行し、変更を反映します。
$ 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.
package.json
の作成とインストールをはじめ、.gitignore
や .npmignore
、eslint、jsii の構成、ライセンスファイル等が projen により自動で生成されていることがわかります。新規プロジェクトを作成するたびに、既存のプロジェクトからコピーしてくるといった作業が不要になります。
これらのファイルを編集する場合、必ず .projenrc.js
を修正し、projen コマンドを再実行する必要があります。手動で編集した場合はビルドが失敗します。
Development
ここでは Hello World の Lambda 関数を API Gateway (HTTP API) から呼び出すシンプルな例を考えてみます。Lambda 関数のコードは CDK のコードにインラインでも挿入できますが、今回は 別途 functions
ディレクトリを作成し、配置、参照させます。
exports.handler = async (event) => {
const response = {
statusCode: 200,
body: JSON.stringify('Hello from Lambda!'),
};
return response;
};
src
ディレクトリに以下の2ファイルを作成します。
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! });
}
}
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');
test
ディレクトリには以下のファイルを作成しました。
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, Build, Release
projen から生成された package.json により、各種 scripts が定義済みです。
Unit Test
yarn test
(npx projen test
) でテストを実行します。
yarn build
(npx projen build
) 実行時もテストが走るため、ここでは実行例は割愛します。
Build
yarn build
でテストを実行し、TypeScript を jsii モジュールにコンパイルします。
また jsii-docgen によりコード内のコメントから API ドキュメント (API.md) を生成します。
さらに jsii-pacmak により dist
ディレクトリに各言語固有の公開パッケージを作成します。
$ 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)
ビルドが成功したら、以下のようにローカルでデプロイを試すことができます。
$ 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
出力された API にリクエストを投げると、Lambda からの応答を確認できます。
$ curl https://4p9zte6ny8.execute-api.ap-northeast-1.amazonaws.com/
"Hello from Lambda!"
削除も同様です。
$ 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
変更をコミットし、GitHub にコードを Push します。
$ git add .
$ git commit -m "feat: initial release"
projen は Conventional Commits に基づいて自動的にセマンティックバージョニングを行います。
例えば以下のような感じです。
- fix: で PATCH バージョン (
v0.0.1
) をバンプ - feat: で MINOR バージョン (
v0.1.0
) をバンプ
MAJAR バージョンについては重大な変更からユーザーを保護するために .projenrc.js
に majorVersion: 1,
のように追記して明示的にバンプする必要があります。
projen は Github Actions の Workflow 定義も生成します。これにより各言語パッケージリポジトリへのリリースを簡単に自動化することができます。
-
Build workflow (.github/workflows/build.yaml):
pull request 作成時に起動
ライブラリのビルドおよび、改ざん (手動修正されていないか) のチェック -
Release workflow (.github/workflows/release.yaml):
リリースブランチへの push 時に起動
ライブラリのビルドおよび、改ざん (手動修正されていないか) のチェック
Conventional Commits による Release Version のバンプ
changelog の作成
GitHub Releases、npm や PyPI などの各パッケージリポジトリへの自動リリース
リポジトリへのリリースには publib が使用されています。
Workflow を正常に動作させるには、projen が使用する Personal Access Token および公開先のリポジトリに応じた API_KEY などを Actions secrets に登録しておく必要があります。
- projen 用の PAT:
PROJEN_GITHUB_TOKEN
(scope はrepo
,workflows
,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
Construct Hub への公開
Construct Hub はコミュニティや AWS、AWS パートナーから提供されるカスタム Construct Library を検出・共有するためのレジストリサイトです。昨年 12 月 に AWS CDK v2 と同日に GA になっています。現在 1000 以上の Construct Library が掲載されています。
自身が作成したライブラリを Construct Hub 上で公開するには以下の条件を満たしている必要があります。
- JSII-compatible であること
- オープンソースライセンスであること
- Apache, BSD, EPL, MPL-2.0, ISC, and CDDL or MIT
- CDK Keyword を使用して npm Registry に公開されていること
- cdk, awscdk, aws-cdk, cdk8s, or cdktf
これらを満たすと 約 30 分で自動的に検出、掲載されるような仕組みになっています。対象のオープンソースライセンスであれば、projen を使用して作成・公開したライブラリはこれらを満たしていることになります。そのため特別な対応をしなくとも Construct Hub に掲載されます。
さいごに
projen を使用することで、Construct Library の実装に集中することができます。(もちろん通常の CDK App についても)
参考の GitHub リポジトリとしては冒頭でご紹介した cdk-ecr-image-scan-notify をご確認いただければと思います。Construct Library 自体もぜひ使ってみていただけると嬉しいです。
ECR のイメージスキャン結果を Slack 通知する Lambda 関数 と EventBrigde のイベントルールを AWS CDK で Construct Library として公開してみました!
— hayao_k (@hayaok3) November 3, 2020
TypeScript と Python で利用できます。https://t.co/AseGERM45u pic.twitter.com/kPSMHBal91
projen を使用した Construct Library の作成については台北の AWS Community Builder である Neil Kuan (@guan840912) からの紹介と多くの協力により学ぶことができました。彼の GitHub アカウントで複数の Construct Library が公開されているので、こちらもチェックしてみてください!
以上です。
参考になれば幸いです。