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
を再起的に集めます。