AWS CDK TIpsシリーズの記事。
最近 deploy-time-build
というCDK コンストラクトをリリースできたので、その過程で得られた知見を共有する。
github.com
Tips
プロジェクトの初期化はprojenで一発
プロジェクト生成ツールのprojenを使うことで、コマンド一発でリポジトリのベースを生成できる。詳しくはここを参照。コマンドはこんな感じ。
mkdir cool-construct
cd cool-construct
npx projen new awscdk-construct
これに共通で必要なもの (package.jsonやeslintrc.json、GitHub Actionsの構成などまで!) はすべて入っているので、開発者は文字通りコンストラクトを定義するコードだけ書けば、自作コンストラクトを良い感じにリリースできる。
この開発体験はかなり良いと思った。
また、projenで管理するプロジェクトは基本的にすべて .projenrc.js
というファイルで諸々の変更を管理する。例えば package.json
や eslintrc.json
なども。対象のファイルは↓のようなコメントが記載されているので、そういったファイルは直接編集しないように注意しよう。 (CDK管理下のリソースは手動で変更しないのと似ている。)
~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".
ではどう編集すれば良いのかというと、リファレンスがここにある。ただし、今のところCDKのReferenceほど見やすくないので、TSの型定義を追うほうが楽かも。projen、ドキュメントまわりは発展途上に見える。
ちなみに、Projenの使い方はこの記事も詳しい。
qiita.com
コミットメッセージは conventional commits に従うべし
www.conventionalcommits.org
Conventional commits は、機械的に解釈できる統一的なフォーマットをもつコミットメッセージを書くべしというルール。ざっと例を挙げると
fix: hoge
: バグ修正のコミット
feat: hoge
: 機能追加のコミット
fix!: hoge
: APIに破壊的変更を伴うバグ修正のコミット
他にも↑のサイトに例があるので、ぜひ参照されたい。BREAKING CHANGE
というフッターをつけたり、 chore
や refactor
といったコミットをすることも許可されている。
このルールは semantic versioning を採番する上で便利に機能する。例えば
fix
のコミットだけある場合はパッチバージョンを上げる
feat
のコミットだけある場合はマイナーバージョンを上げる
fix!
や feat!
のコミットがある場合はメジャーバージョンを上げる
といったように機械的に判別できるので、結果としてconventional commitsに従うリポジトリでは自動的にsemantic versioningのバージョン番号を採番できる。
Projenで作成されるテンプレートでもこれが機能していて、コミットメッセージを conventional commits に従って書けば、↓のようにリリースノートやバージョン番号も良い感じにしてくれる。このため、CDK コンストラクトを作るときはとりあえずこのルールに従うのがお勧め。
ただし、projenではメジャーバージョンのbump (バージョンを1上げること) だけは機械的には実行せず、開発者が明示的にbumpする必要があるらしい。このあたりの挙動はメジャーバージョンが0か1以上かでも異なるらしいので、まだ試せていない。(少なくとも v0.x.yのうちは、破壊的変更があってもマイナーバージョンがbumpされるだけで済む。)
Lambda関数を含めたい時は?
コンストラクトにLambda関数を含めるときの方法をまとめる。
いつもの書き方で大丈夫?
お手製のコンストラクトライブラリにはLambda関数が含まれる場合もあるだろう。そのような時、通常のCDKアプリを書くときと同じ感覚でLambda関数を定義すると、好ましくない場合がある。
例えば、 NodejsFunction
や PythonFunction
を使って定義したとしよう。この場合、そのコンストラクトを利用する全ユーザーはCDKデプロイ時にそのLambda関数をビルドする必要が生じる。これは明らかに無駄である。できるなら最初からビルド済みのコードをパッケージとして提供する方が、余計なビルド時間をユーザーに課さなくても済む。
また、Custom resourceのハンドラーとしてLambda関数を使う場合、 SingletonFunciton
という特殊なコンストラクトを利用することが推奨されている。これは同一スタック内で何度定義されても実体は必ず1つだけになるFunctionなので、ライブラリとして提供するCustom resourceのハンドラーには重複定義を避けられる点で都合が良い。
実際、CDK公式が提供しているCustom resourceハンドラの実装 (例: BucketDeployment
) は大抵これが利用されている。ここで重要なのは SingletonFunction
が lambda.Function
を直に継承するリソースのため、 NodejsFunction
や PythonFunction
の機能を利用できないということ。この点でも、通常のCDKコードとは少し異なる方法でLambda関数を定義すべきであることがわかる。
Construct library way
ではどう定義すればよいかだが、まだベストプラクティスは定まっていなさそうに思える。基本的には、Lambda関数は外部ライブラリ(Lambda実行環境に標準装備のaws-sdk/boto3は除く)に依存するとデプロイが面倒になるので、できるだけ標準ライブラリだけで書くのが楽。とはいえ、少しの工夫で依存関係を含めることもできる。現実的な方法としては、以下の4つが考えられる:
- Lambdaを素のPythonで書く (Node.jsよりは標準ライブラリの機能が充実している)
- Lambdaを素のTypeScriptで書く (projen build中にtscのステップを挟む必要あり)
- Lambdaを素のJavaScriptで書く (tscも不要のため楽)
- LambdaをTypeScriptで書き、外部ライブラリの依存はesbuildでバンドルする
「素の」とは、「外部ライブラリに依存しない」という意図で書いた。
1〜3の方法は割と読んで字のごとしで、定義する際は単純にハンドラのコードのパスと関数名を指定するだけで良く、ビルドプロセスも単純。外部ライブラリの依存を避ける消極的な解決策とは思えるが、実際巷のコンストラクトライブラリを見るとこの方法を使っているものは多い。
とはいえどうしても外部ライブラリに依存したいときはあるので、そのときは4の方法を採る。基本的な方針は、NodejsFunction
がCDK synth時にやっている処理を、projen build時にやればOK。実際の例は弊リポジトリを参照。
あるいは、 lambda.ts
という拡張子のファイルを配置することで、projenにバンドル処理やFunctionコンストラクトの定義を肩代わりさせることができる。こちらを参照。ただし、現状この方法ではSingletonFunctionは使えないようなので注意。
4の仕組み
簡単に方法4の仕組みを説明する。projenではCIのビルドステップもまたコードで管理されており、この辺りのプロパティから触ることができる。今回はcompileTaskの辺りでLambda関数をビルドすれば良いので、下記のコードを .projenrc.js
に追加している。
project.projectBuild.compileTask.prependExec('npm ci && npm run build', {cwd: 'lambda/nodejs-build'});
これにより、projenのコンパイルタスクの際に、 lambda/nodejs-build
ディレクトリで npm ci && npm run build
のコマンドが実行される。さらに、lambda/nodejs-build/package.json
を見てほしい。
"scripts": {
"build": "esbuild index.ts --bundle --outdir=./ --platform=node --external:aws-sdk"
},
"dependencies": {
"adm-zip": "^0.5.9",
"aws-sdk": "^2.1130.0",
"extract-zip": "^2.0.1",
"node-fetch": "^3.2.4"
},
つまるところ、npm run build
はesbuildでTypeScriptをトランスパイル・バンドルしている。ビルド生成物はlambda/nodejs-build/index.js
に保存され、それを SingletonFunction
から参照している。
const handler = new SingletonFunction(this, 'CustomResourceHandler', {
runtime: Runtime.NODEJS_14_X,
code: Code.fromAsset(join(__dirname, '../lambda/nodejs-build')),
handler: 'index.handler',
uuid: '643fc8aa-9cdf-41ad-9b26-dc5b258cc071',
lambdaPurpose: 'NodejsBuildCustomResourceHandler',
});
これで、依存関係のあるLambda関数を定義でき、さらにライブラリのユーザー側ではLambda関数のビルド処理は不要となる。ちなみに ↑ の uuid
はSingletonFunctionごとに生成する必要があるので、決して巷のサンプルコードからコピペしないように気をつけよう。UUIDは任意の方法で生成できるが、手軽なのはこれ。
また、src以外にTypeScriptのファイルがあるとeslintが騒ぐので、projenから黙らせている。
eslintOptions: {
ignorePatterns: ['example/**/*', "lambda/**/*"],
},
example フォルダを作ると親切
Constructの使い方は基本的には README.md
に書くべき。しかしながら、動作する完全なサンプルがあるとさらに役立つものである。このため、CDK Constructでは example
ディレクトリ以下にサンプルのスタック定義を含めることが多い模様。
一例はここ。最低限 cdk.json
と cdk.App
の定義があれば動作するようなので、簡潔な記述・ファイル構成でサンプルを提供できる。
import { Stack, StackProps, App } from 'aws-cdk-lib';
class TestStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps = {}) {
super(scope, id, props);
}
}
class TestApp extends App {
constructor() {
super();
new TestStack(this, 'TestStack');
}
}
new TestApp().synth();
{
"app": "npx ts-node --prefer-ts-exts index.ts"
}
リリース前に確認すべき projen の設定項目
まず、author
や authorAddress
、repositoryUrl
は、正しい値が入っていることを確認しよう!
また、Construct Hub にライブラリを掲載するには、 keywords
に aws-cdk
を含める必要があるので注意。詳細はFAQ を参照。これだけで、npm に publishしてから30分以内に自動で掲載される。
最後に description
も必ず書いておこう。Construct Hub のページで見出しの一言説明として利用されるため (下図) 。
まとめるとこんな感じになるだろう。
const project = new awscdk.AwsCdkConstructLibrary({
author: 'tmokmss',
authorAddress: 'hoge@example.com',
cdkVersion: '2.20.0',
defaultReleaseBranch: 'main',
name: 'deploy-time-build',
repositoryUrl: 'https://github.com/tmokmss/deploy-time-build.git',
keywords: ['aws', 'cdk', 'lambda', 'aws-cdk'],
description: 'Build your frontend apps during CDK deployment!',
});
パッケージレジストリにリリースするために
Construct Hubに掲載されるには、最低限 npm にパッケージを登録する必要がある。
これも簡単にできて、 ただGitHubリポジトリのSecretに NPM_TOKEN
を登録すれば良い。
この辺りの記事が参考になるだろう。
なお、他の言語のライブラリを公開する場合も、同様に各パッケージレジストリのキーを取得し、登録すれば良いと思われる。現実的には結構面倒くさいので、Node.jsとPythonだけ公開しているライブラリが多い模様。この辺りは仕方ないね…
まとめ
ProjenでAWS CDKのConstructライブラリを開発する時に役立つTipsを紹介した。まだまだ駆け出しなので、今後開発が継続してさらに知見が溜まったら、また放出できればと思う。