maybe daily dev notes

私の開発日誌

AWS CDK コンストラクトライブラリ開発に関する5つのTips

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.jsonGitHub Actionsの構成などまで!) はすべて入っているので、開発者は文字通りコンストラクトを定義するコードだけ書けば、自作コンストラクトを良い感じにリリースできる。 この開発体験はかなり良いと思った。

また、projenで管理するプロジェクトは基本的にすべて .projenrc.js というファイルで諸々の変更を管理する。例えば package.jsoneslintrc.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 というフッターをつけたり、 chorerefactor といったコミットをすることも許可されている。

このルールは 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関数を定義すると、好ましくない場合がある。

例えば、 NodejsFunctionPythonFunction を使って定義したとしよう。この場合、そのコンストラクトを利用する全ユーザーはCDKデプロイ時にそのLambda関数をビルドする必要が生じる。これは明らかに無駄である。できるなら最初からビルド済みのコードをパッケージとして提供する方が、余計なビルド時間をユーザーに課さなくても済む。

また、Custom resourceのハンドラーとしてLambda関数を使う場合、 SingletonFunciton という特殊なコンストラクトを利用することが推奨されている。これは同一スタック内で何度定義されても実体は必ず1つだけになるFunctionなので、ライブラリとして提供するCustom resourceのハンドラーには重複定義を避けられる点で都合が良い。

実際、CDK公式が提供しているCustom resourceハンドラの実装 (例: BucketDeployment) は大抵これが利用されている。ここで重要なのは SingletonFunctionlambda.Function を直に継承するリソースのため、 NodejsFunctionPythonFunction の機能を利用できないということ。この点でも、通常のCDKコードとは少し異なる方法でLambda関数を定義すべきであることがわかる。

Construct library way

ではどう定義すればよいかだが、まだベストプラクティスは定まっていなさそうに思える。基本的には、Lambda関数は外部ライブラリ(Lambda実行環境に標準装備のaws-sdk/boto3は除く)に依存するとデプロイが面倒になるので、できるだけ標準ライブラリだけで書くのが楽。とはいえ、少しの工夫で依存関係を含めることもできる。現実的な方法としては、以下の4つが考えられる:

  1. Lambdaを素のPythonで書く (Node.jsよりは標準ライブラリの機能が充実している)
  2. Lambdaを素のTypeScriptで書く (projen build中にtscのステップを挟む必要あり)
  3. Lambdaを素のJavaScriptで書く (tscも不要のため楽)
  4. 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', // generated for this construct
  lambdaPurpose: 'NodejsBuildCustomResourceHandler',
});

これで、依存関係のあるLambda関数を定義でき、さらにライブラリのユーザー側ではLambda関数のビルド処理は不要となる。ちなみに ↑ の uuid はSingletonFunctionごとに生成する必要があるので、決して巷のサンプルコードからコピペしないように気をつけよう。UUIDは任意の方法で生成できるが、手軽なのはこれ

また、src以外にTypeScriptのファイルがあるとeslintが騒ぐので、projenから黙らせている。

  eslintOptions: {
    ignorePatterns: ['example/**/*', "lambda/**/*"],
  },

example フォルダを作ると親切

Constructの使い方は基本的には README.md に書くべき。しかしながら、動作する完全なサンプルがあるとさらに役立つものである。このため、CDK Constructでは example ディレクトリ以下にサンプルのスタック定義を含めることが多い模様。

一例はここ。最低限 cdk.jsoncdk.App の定義があれば動作するようなので、簡潔な記述・ファイル構成でサンプルを提供できる。

// index.ts
import { Stack, StackProps, App } from 'aws-cdk-lib';

class TestStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps = {}) {
    super(scope, id, props);
    // 自作Constructの利用例を書く
  }
}

class TestApp extends App {
  constructor() {
    super();

    new TestStack(this, 'TestStack');
  }
}

new TestApp().synth();

// cdk.json
{
    "app": "npx ts-node --prefer-ts-exts index.ts"
}

リリース前に確認すべき projen の設定項目

まず、authorauthorAddressrepositoryUrl は、正しい値が入っていることを確認しよう!

また、Construct Hub にライブラリを掲載するには、 keywordsaws-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を紹介した。まだまだ駆け出しなので、今後開発が継続してさらに知見が溜まったら、また放出できればと思う。