AWS APIGateway × OpenAPI (3. 出力編)
2022-08-02
OpenAPI定義をCDKのREST API定義に統合するライブラリを開発しています。 これはライブラリの開発過程を紹介するシリーズのブログ投稿第3弾です。
背景
本シリーズ最初のブログ投稿で、以下の課題を残しました。
いつ実際にOpenAPI定義ファイルを出力すべきか?
- 保存のための関数をユーザーが明示的に呼び出さなければならないのでしょうか?
- それともCDKがCloudFormationテンプレートに対してやるようにOpenAPIの定義ファイルもいつのまにやら保存されようにできるでしょうか?
このブログ投稿では、後者のケース(ユーザの明示的な呼び出しなしにライブラリにOpenAPI定義を出力させる)に挑みます。
CDKはいつCloudFormationテンプレートを出力するのか?
AWS Cloud Development Kit (CDK)がCloudFormationテンプレートを出力するタイミングをトラップできれば、ユーザの明示的な呼び出しなしにOpenAPI定義を出力できそうです。
ということでOpenAPI定義を出力するのに応用できそうなフックを探してCDKのソースコード*を眺めていくことにします。
まずはcdkコマンドを見てみましょう。
長いコードレビューをスキップしたい場合は、節「フック」まで読み飛ばしてもよいです。
* 当時最新のCDKバージョン2.34.2を分析しました。
他のCDKバージョンでは状況は違っているかもしれません。
cdkコマンドはどこに定義されているのか?
cdkコマンドはCDKレポジトリのpackages/aws-cdkフォルダに含まれています。
CDKのレポジトリは巨大なので、cdkコマンドがどこで定義されているかというのをよくわかっていませんでした。
このブログ投稿では、packages/aws-cdkフォルダをaws-cdkと省略します。
synthとdeployサブコマンドは何をするのか?
CloudFormationテンプレートを生成するという点では、以下の2つのcdkサブコマンドが関心の対象です。
cdkコマンドのコマンドラインインターフェイスはaws-cdk/lib/cli.tsに定義されています。
yargsの設定を行う長い行の中に、synthとdeploy サブコマンドの定義があります。
synth- オプション: aws-cdk/lib/cli.ts#L86-L89
- ハンドラ: aws-cdk/lib/cli.ts#L528-L534
- 結果として
CdkToolkit#synthの呼び出しとなる。
- 結果として
deploy- オプション: aws-cdk/lib/cli.ts#L107-L150
- ハンドラ: aws-cdk/lib/cli.ts#L454-L483
- 結果として
CdkToolkit#deployの呼び出しとなる。
- 結果として
cdkのサブコマンドは結果的にaws-cdk/lib/cdk-toolkit.tsに定義されるCdkToolkitの対応するメソッドを呼び出すことになります。
CdkToolkit#synth
定義: aws-cdk/lib/cdk-toolkit.ts#L517-L545
aws-cdk/lib/cdk-toolkit.ts#L518でCdkToolkit#selectStacksForDiffを呼び出します。
;
メソッド名からは明白ではありませんが、CdkToolkit#selectionStacksForDiffはCloudFormationテンプレートの生成において重要な働きをします。
aws-cdk/lib/cdk-toolkit.ts#L621にてCdkToolkit#assemblyを呼び出すことになります。
;
CdkToolkit#deploy
定義: aws-cdk/lib/cdk-toolkit.ts#L126-L282
aws-cdk/lib/cdk-toolkit.ts#L140でCdkToolkit#selectStacksForDeployを呼び出します。
;
CdkToolkit#synthと同様、CdkToolkit#selectStacksForDeployはCloudFormationテンプレートの生成において重要な働きをします。
aws-cdk/lib/cdk-toolkit.ts#L608にて同じくCdkToolkit#assemblyの呼び出しとなります。
;
CdkToolkit#assembly
定義: aws-cdk/lib/cdk-toolkit.ts#L690-L692
private cacheCloudAssembly?: boolean: Promise<CloudAssembly>
CloudExecutable#synthesizeの呼び出しと等価です。
CloudExecutableはaws-cdk/lib/api/cxapp/cloud-executableに定義されています。
CloudExecutable#synthesize
定義: aws-cdk/lib/api/cxapp/cloud-executable.ts#L63-L68
aws-cdk/lib/api/cxapp/cloud-executable.ts#L65にてCloudExecutable#doSynthesizeを呼び出します。
this._cloudAssembly = await ;
CloudExecutable#doSynthesize
定義: aws-cdk/lib/api/cxapp/cloud-executable.ts#L70-L124
実際の合成はaws-cdk/lib/api/cxapp/cloud-executable.ts#L79で行われます。
;
this.props.synthesizerはaws-cdk/lib/api/cxapp/cloud-executable.ts#L14に定義されるSynthesizerです。
;
aws-cdk/lib/cli.tsのコンテキストでは、aws-cdk/lib/cli.ts#L305-L309で初期化されているようにthis.props.synthesizerは常にexecProgramです。
;
execProgramはaws-cdk/lib/api/cxapp/exec.ts#L12-L136に定義されています。
以下の行から分かるように、cdkコマンドのappオプションに指定したコマンドを実行します(それぞれaws-cdk/lib/api/cxapp/exec.ts#L54, aws-cdk/lib/api/cxapp/exec.ts#L65, aws-cdk/lib/api/cxapp/exec.ts#L86)。
;
;
await ' ';
cdkコマンドにappオプションなんて指定したことがないと思われるかもしれません。
CDKプロジェクトをcdk initコマンドで初期化すると、cdk.jsonファイルが作成され、そこにappオプションのデフォルト値が保存されます。
cdk.jsonファイルを覗くと、以下のような行が見つかります。
"app": "npx ts-node --prefer-ts-exts bin/cdk.ts",
このコマンド(npx ts-node --prefer-ts-exts bin/cdk.ts)がexecProgramの実行しているものです。
appオプションに渡されたコマンドを実行した後、execProgramはcdkコマンドのoutputオプションに指定した出力フォルダから生成物を読み込みます(それぞれaws-cdk/lib/api/cxapp/exec.ts#L67, aws-cdk/lib/api/cxapp/exec.ts#L78, aws-cdk/lib/api/cxapp/exec.ts#L88)。
;
env = outdir;
return outdir;
outputオプションはデフォルトで"cdk.out"です(aws-cdk/lib/settings.ts#L75-L79)。
public readonly defaultConfig = new Settings;
ということで、bin/cdk.tsが何を行っているかが次の焦点です。
AppはいつCloudFormationテンプレートを出力しているのか?
bin/cdk.tsでは、通常Appのインスタンスを作成します。
ではAppはいつCloudFormationテンプレートを出力しているのでしょうか?
Appのソースコードを見てみましょう。
Appはcore/lib/app.ts#L94-L155に定義されています。
このブログ投稿では、packages/@aws-cdk/coreフォルダをcoreと省略します。
Appのコンストラクタには、core/lib/app.ts#L131に興味深い行があります。
'beforeExit',;
これはAppを実行するプロセスが終了時にApp#synthを呼ぶようにします。
どうやらApp#synthに答えがありそうです。
AppはStageを継承しているので、App#synthはStage#synthと等価です。
Stage#synth
定義: core/lib/stage.ts#L174-L183
public options: StageSynthesisOptions = : cxapi.CloudAssembly
core/lib/private/synthesis.ts#L23-L50に定義されているsynthesizeを呼び出します。
synthesizeの以下の2行が使えそうな2つのフックにつながることがわかりました。
-
core/lib/private/synthesis.ts#L36
root;validateTreeは「Validatorフック」につながります。 -
core/lib/private/synthesis.ts#L47
root, builder, options.validateOnSynthesis;synthesizeTreeは「_toCloudFormationフック」につながります。
validateTree
定義: core/lib/private/synthesis.ts#L201-L214
validateTreeはrootから始まるConstructツリーのすべてのノードを辿って(visit)各ノードにNode#validateを適用します。
Node#validateはNode#addValidationでノードに追加したIValidationのIValidation#validateを呼び出します。
ということで IValidationはOpenAPI定義を出力するためのフック になりそうです。
使い方については「Validatorフック」を参照ください。
synthesizeTree
定義: core/lib/private/synthesis.ts#L174-L191
synthesizeTreeはrootから始まるConstructツリーのすべてのノードを辿って(visit)各Constructを処理します。
重要な行はcore/lib/private/synthesis.ts#L183にあります。
session;
ここでconstructはStackのインスタンスであり、Stack#synthesizerはデフォルトでDefaultStackSynthesizerです。
なので上記の行は通常DefaultStackSynthesizer#synthesizeの呼び出しとなります。
DefaultStackSynthesizer#synthesize
定義: core/lib/stack-synthesizers/default-synthesizer.ts#L387-L424
DefaultStackSynthesizer#synthesizeはcore/lib/stack-synthesizers/default-synthesizer.ts#L400でDefaultStackSynthesizer#synthesizeStackTemplateを呼び出します。
this.stack, session;
DefaultStackSynthesizer#synthesizeStackTemplateはStack#_synthesizeTemplateの呼び出しと等価です(core/lib/stack-synthesizers/default-synthesizer.ts#L380-L382)。
protected stack: Stack, session: ISynthesisSession
Stack#_synthesizeTemplate
定義: core/lib/stack.ts#L770-L804
このメソッドはcore/lib/stack.ts#L779でStack#_toCloudFormationを呼び出します。
;
Stack#_toCloudFormation
定義: core/lib/stack.ts#L1007-L1045
以下の2行に注目してください(core/lib/stack.ts#L1031-L1032)。
;
;
このメソッドはcfnElementsですべての子CfnElementを集め、CfnElement#_toCloudFormationをそれぞれに適用します。
CfnElementはすべてのL1 Constructのベースクラスで、CfnElement#_toCloudFormationはcore/lib/cfn-element.ts#L161に定義される内部メソッドです。
ということで CfnElement#_toCloudFormationもOpenAPI定義を出力するためのもう1つのフック になりそうです。
使い方については「_toCloudFormationフック」を参照ください。
フック
上記の分析により、OpenAPI定義の出力に応用できるフックは2つありそうです。
Validatorフック
「Validatorフック」はNode#addValidationでNodeに追加することのできるIValidationを応用します。
細かい分析については節「validateTree」を参照ください。
とりあえずシンプルさを優先して私のライブラリを実装するのには このフックを選択 しました。
以下のコードはRestApiWithSpecのコンストラクタから抽出したものです。
完全な定義はGitHubレポジトリにあります。
scope: Construct, id: string, readonly props: RestApiWithSpecProps
弱点はバリデーションが無効だと機能しないことです。
_toCloudFormationフック
「_toCloudFormationフック」は内部メソッドのCfnElement#_toCloudFormationをオーバーライドして応用します。
細かい分析については節「synthesizeTree」を参照ください。
もし私のライブラリでこのフックを使うとしたら、RestApiWithSpecのコンストラクタは以下のようなものになりそうです。
scope: Construct, id: string, readonly props: RestApiWithSpecProps
このフックには欠点がいくつかあります。
CfnElement#_toCloudFormationは内部メソッドであり変更されるかもしれない。- 私の実験では、
ToCloudFormationHook#_toCloudFormationが2回呼ばれた(何故かは不明)。
結論
このブログ投稿では、CDKのソースコードを眺めました。 OpenAPI定義に使える以下の2つのフックを見つけました。
私のライブラリではシンプルさを優先して「Validatorフック」を選択しました。
補足
この節ではCDKで使われているユーティリティ関数をいくつか紹介します。
visit
定義: core/lib/private/synthesis.ts#L219-L232
この関数はrootから始まるConstructツリーのすべてのノードを辿って、cbに指定される関数をそれぞれに適用します。
cfnElements
定義: core/lib/stack.ts#L1279-L1292
この関数はnodeから始まるConstructツリーのすべてのCfnElementを再起的に集めます。