cdk.context.json が CDK 的にどう扱われているか調べてみた - part2
以下の記事の続編です。
タイトルは「その2」と銘打っておりますが、実際には「CDK App クラスの synth メソッドが何をしているのか」あるいは「synth が2回以上走る原因である "missing context" を生じうる construct 周辺の機能の読解」とったサブテーマがこの記事で扱う中心的な話題となります。
前編と同じように、ソースコードを読み進めるうえで私がやったことを雑に書きなぐっていきます。
得られた成果だけ見たい場合は「まとめ」のセクションまで読み飛ばしてください。なんなら先にまとめを見てから「やったこと」に戻ってくる方が、流れが俯瞰できて良いかもしれません。
(追記) part3 書きました
前提事項
前編と同様、この記事で参照する CDK のバージョンは v2.150.0
を前提とします。
https://github.com/aws/aws-cdk/tree/v2.150.0
$ git checkout -b v2.150.0 tags/v2.150.0
そんなに調査した結果に確信があるわけじゃありません。読み飛ばしや誤読などにより私が不正確な理解をしている可能性もあります。内容に 100% の正確性は保証できないことをご了承ください。
それなりに回り道もしていると思います。有用な情報に最短距離でたどり着くような情報提供を主旨とする記事ではありません。このあたりのテンションは前編と同じです。
やったことや得られた洞察など、結果を先に見たいという人は最後の「まとめ」までジャンプしてください。
やったことを雑多に書く (part2)
App クラスの synth メソッドを追いかけます。
この機能の実行結果が、前記事で登場した "missing context" なるモノを含む場合があるらしい、ということを念頭に置きつつ読み進めます。
ドキュメントを読んでおさらい
まずは公式ドキュメントを見てみることにします。App クラスをインスタンス化して synth メソッドを呼び出すような記述があります。
const app = new App();
new MyFirstStack(app, 'hello-cdk');
app.synth();
App クラスの synth メソッドを追いかけていけば、 synth の正体にもたどり着くでしょう。
App lifecycle の図もおさらいしておきます。
ライフサイクルを構成するフェーズは次の5つです。
- Construction
- Preparation
- Validation
- Synthesis
- Deployment
今回のテーマではデプロイには興味がないので、それ以外の部分に着目します。いくつか興味深い記述がありました。
Preparation フェーズでは、prepare
メソッドを実装した constrcut が関係しています。CDK ユーザーとしては通常ここで行っていることはほぼ意識する必要がありません。表立って見えない処理であり prepare メソッドを自前で実装する必要がある場面もほぼありません。ただし、ソースコードを読み進めるうえでは、このメソッドを実装しているクラスはこのフェーズに関係する何らかの処理を行っているらしいと判断できます。
Synthesis フェーズは、app.synth()
の呼び出しによってトリガーされるようです。app.synth()
はライフサイクルのうちの Synthesis フェーズ に該当する処理を行っているものと理解すれば良いようです。この処理の実態としては、 synthesize メソッドを実装するすべての construct をトラバースしているようです。
it traverses the construct tree and invokes the synthesize method on all constructs.
app.synth()
の成果物として "cloud assembly" が生成されます。Cloud Assmebly はこのフェーズの出力結果であり、以下のようなものが含まれます。
- AWS CloudFormation templates
- AWS Lambda application bundles
- file and Docker image assets
- other deployment artifacts
ここまでの内容で、app.synth()
は CDK App における Synthesis フェーズに該当する処理を担っているらしい、その出力結果は "Cloud Assmebly" という形式らしい、ということがわかりました。
Cloud Assembly 形式の定義を眺める
cdk synth
の成果物としての、あるいはファイルシステム上に実在するコンテンツとしての Cloud Assembly という形式についてざっと調べてみます。前述のドキュメントからリンクをたどって、ソースを確認できます。JSON Schema の定義です。
$.AssemblyManifest
というスキーマ定義の下に missing
というキーの定義が確認できます。
前編の synth コマンドの実装で見た、CloudExecutable クラスの doSynthesize メソッドの中に登場する assembly.manifest.missing
という字面が、この定義とリンクしていそうですね。
さらに MissingContext の詳細を見てみます。
ContextProvider
, ContextQueryProperties
という型が出てきました。前編に登場した "missing context" なる概念と、いかにも密接に関係していそうです。ちなみに、CDK の実装にもContextProvider というクラスが存在します。
深堀りしすぎると沼にはまるので、これらのプロパティの中身は軽めに流し読むことにします。まずは ContextProvider
から。
"ami" とか "availability-zone" とか、なんだか見覚えがありますね。前編の「なんとなく自分の観測事例を思い出してみる」のセクションで触れた話です。
AMI や AZ を始めとしたここに列挙されている要素は、どうやら ContextProvider なる仕組みによって具象値が解決されるらしく、かつ1回の CDK App の実行では具象値がわからない場合がある、ということのようです。
次に ContextQueryProperties
を見てみます。
色々な型の Union として定義されているようです。また、型のリストは先の ContextProvider の enum の定義とも対応関係にあることがわかります。
ざっと眺めて見た感じ、それぞれの XxxQuery
型は概ね共通して lookupRoleArn
という属性を持っていることが目につきました。このことから、いわゆる "fromLookup" 系の機能との関連がありそうだと予想しました。実際、VPC.fromLookup メソッドのリファレンスにも以下のように書かれており、cdk.context.json
との関係が明言されています。
The VPC information will be cached in
cdk.context.json
and the same VPC will be used on future runs. To refresh the lookup, you will have to evict the value from the cache using the cdk context command. See https://docs.aws.amazon.com/cdk/latest/guide/context.html for more information.
他の ContextProvider もすべて同じ要領で理解できるものなのでしょうか?順番に確認していけば答えは出そうですが、この場はいったん棚上げして次に進んでみることにします。
実装を追いかける(1) - CDK App クラスの synth を追う
App クラスの実装は以下です。これを読み進めることにします。
https://github.com/aws/aws-cdk/blob/v2.150.0/packages/aws-cdk-lib/core/lib/app.ts
App クラス自身は synth メソッドの実装を持たず、派生元の Stage をオーバーライドしています。実装はこちらです。
戻り値の cxapi.CloudAssembly
は、先ほど見た JSON Schema の Cloud Assembly の定義を TypeScript に起こしたような内容でした。このことから、synth メソッドの中で CloudAssembly オブジェクトの manifest.missing
プロパティを変更するような用事があれば、それは前編の結論である "missing context" の発生理由、すなわち cdk synth
コマンドによって複数回の App.synth()
が呼び出されるケースの説明になると言えそうです。
実装を追いかける(2) - CDK App クラスの synth メソッド
App クラス(厳密にはその親である Stage クラス)の synth メソッドの実質的な処理は以下の synthesize 関数です。
prepareApp や validateTree, synthesizeTree, synthNestedAssemblies などの関数がありますが、いずれも見覚えのある語彙です。これらはCDK App のライフサイクルで説明されている各フェーズに対応しているように見えます。
synthNestedAssemblies に関しては再帰的な synth の実行であろうと予想がつくので、今読解したいテーマからするとおそらくさして重要ではないでしょう。また、validateTree もあくまでバリデーションのロジックであることが予想できるので、こちらも今回のテーマにはあまり関係ないと推察します。synthesizeTree はおそらく今の関心の中核なので読むのは必須。prepareApp はまだ判断がつきません。
prepareApp の読解の必要性を探るため、ここでドキュメントの記載をあたってみることにします。ライフサイクルの Preparation の説明を抜粋してみます。
Preparation – All constructs that have implemented the prepare method participate in a final round of modifications, to set up their final state. The preparation phase happens automatically. As a user, you don't see any feedback from this phase. It's rare to need to use the "prepare" hook, and generally not recommended. Be very careful when mutating the construct tree during this phase, because the order of operations could impact behavior.
これを見ても、ちょっとまだわからないです。しょうがないので実装を見ていきます。
実装を追いかける(3) - prepareApp
興味の中心は synth なので、ここの読解はテーマとの強い関連性が見えない限りほどほどにしようと心に留めながら読み進めます。該当箇所は以下です。
JSDoc や呼び出している関数の名前などを拾って文字起こししてみます
- Construct tree の root に対して適用する関数である
- リソース間の依存関係を検出して追加
- root Construct 内の「参照」を「解決」しようとする (resolveReferences 関数)
- Stack のネスト構造がある場合、以下
- ネストされた Stack に関して、Stack テンプレートのアセットを準備(?)
- レガシーな Synthesizer と3階層以上ネストする Stack のための処理
ここで、resolveReferences という関数が登場しました。内部的には IResolvable とか Token を扱っており、いわゆる「遅延解決」の実装が入っているようです。内部的には Token の一種である CfnReference クラス[3]を利用していて、この関数が指す "References" とはこのクラスが意図している概念と対応しているか、あるいは近そうに見えました。
本記事の元々のテーマである「CDK 内部での cdk.context.json
ファイルの扱い」との関連性ですが、現時点では「なさそう」寄りで推測しました。なんらかのタイミングで「解決される値」が context ファイルに書き出されるという前提から、"Reference" を扱う機能はなんらかの形で関係している可能性があります。しかし、 prepareApp 関数の中では ContextProvider クラスのような "context" の語彙を見かけることがなく、現状スルーして良いと考えました。
実装を追いかける(4) - synthesizeTree
ソースの該当箇所はこちら。
ここで登場する主要そうな概念は "synthesize" と "CloudAssemblyBuilder" の2つです。
"missing context" は CloudAssembly に帰属する語彙ですので、先に "missing context" と関係しそうな機能などがないか見ておくことにします。
CloudAssemblyBuilder クラスのメソッドを見てみると、 addMissing
といういかにもなメソッドがあります。
実装内容もシンプルで、まぁだいたい想像通りの挙動だろうと予想できます。これを "build" あるいは "synth" の中で呼び出している場所が、今回のテーマの核心ということになります。
情報を絞れそうなので、先に CloudAssemblyBuilder.buildAssembly()
から見ていきます。
AssemblyManifest 型の変数を宣言している箇所を見ると、案の定、このメソッドを呼び出した時点で CloudAssembly の中身はほぼ確定しているようです。よって見るべきは synthesizeTree 関数のみであろうと推察できます。
visit 関数を使って再帰的に construct tree を走査しています。3番目の引数がコールバック関数で、第2引数が 'post'
であることから、自分自身に対するコールバックの実行順序は 再帰的な走査が一通り終わったあと 、ということになります。要するに、ツリーの親の方がコールバックの実行順序が後回しにされる、ということです。
肝心のコールバックの中身は以下。
// construct => {
const session = {
outdir: builder.outdir,
assembly: builder,
validateOnSynth,
};
if (Stack.isStack(construct)) {
construct.synthesizer.synthesize(session);
} else if (construct instanceof TreeMetadata) {
construct._synthesizeTree(session);
} else {
const custom = getCustomSynthesis(construct);
custom?.onSynthesize(session);
}
// }
今取り組んでいる読解テーマなら、Stack に関する synthesize だけを気にすれば良さそうです。
この synthesize は IStackSynthesizer のインタフェースです。
(ここに来て、ようやく Stack に対する synth の概念が出てきましたね...!)
IStackSynthesizer の定義を確認してみます。
我々にとって馴染みのある(?) DefaultStackSynthesizer もこのインタフェースを実装しています。ひとまずは標準的な StackSynthesizer である DefaultStackSynthesizer の synthesize 実装を見ていくことにします。
実装を追いかける(5) - DefaultStackSynthesizer.synthesize
実装はこちら。
冒頭にある assertBound という関数の書き方が個人的には勉強になりました。記事の本旨からは外れますが、興味深い内容だったので以下の注釈でご紹介します。
この関数の中でも特にテーマに関係していそうなのは以下の箇所でしょう。
const templateAssetSource = this.synthesizeTemplate(session, this.lookupRoleArn);
中を追っていくと、Stack クラスの _synthesizeTemplate メソッドが「合成」の本体っぽいです。
最後の方で、例の addMissing メソッドを呼んでいる箇所がありました。
for (const ctx of this._missingContext) {
if (lookupRoleArn != null) {
builder.addMissing({ ...ctx, props: { ...ctx.props, lookupRoleArn } });
} else {
builder.addMissing(ctx);
}
}
addMissing の呼び出しは Stack インスタンスの _missingContext
が存在する場合ですので、ここを変更している機能を探せば良さそうです。 _missingContext
の定義は次の通りです。JSDoc にかかれていることをほんのり覚えておくと良さそう。
/**
* Lists all missing contextual information.
* This is returned when the stack is synthesized under the 'missing' attribute
* and allows tooling to obtain the context and re-synthesize.
*/
private readonly _missingContext: cxschema.MissingContext[];
頭から処理を追いかけるのは難解そうなので、このプロパティが private であり参照箇所が少ないであろうことを期待して this._missingContext
でソースコードを検索してみました。すると、このプロパティを変更するための reportMissingContextKey メソッドの存在を発見できました。このメソッドを呼び出す類似名の reportMissingContext というメソッドもありますが、全文検索してみた感じこちらはもう内部で使われていないようですので無視します。
/**
* DEPRECATED
* @deprecated use `reportMissingContextKey()`
*/
public reportMissingContext(report: cxapi.MissingContext) {
if (!Object.values(cxschema.ContextProvider).includes(report.provider as cxschema.ContextProvider)) {
throw new Error(`Unknown context provider requested in: ${JSON.stringify(report)}`);
}
this.reportMissingContextKey(report as cxschema.MissingContext);
}
/**
* Indicate that a context key was expected
*
* Contains instructions which will be emitted into the cloud assembly on how
* the key should be supplied.
*
* @param report The set of parameters needed to obtain the context
*/
public reportMissingContextKey(report: cxschema.MissingContext) {
this._missingContext.push(report);
}
reportMissingContextKey を呼び出している場所を検索してみると、この機能を呼び出しているのは ContextProvider クラスの getValue メソッドのみのようです。
次は、ContextProvider クラスの getValue メソッドについて見ていきます。このメソッドが実際に使われている場所を調べれば、どのような機能をどのような状況で使った場合に "missing context" が発生する = synth が複数回走るケースが生じる、と言えそうです。
実装を追いかける(6) - ContextProvider.getValue
このメソッドは以下のようなシグネチャを持っています。
public static getValue(
scope: Construct,
options: GetContextValueOptions
): GetContextValueResult
注目すべきはこのメソッドのオプションの型と、このメソッド自身がどう使われるかの2点です。
GetContextValueOptions 型の定義を以下に抜粋します。
export interface GetContextValueOptions extends GetContextKeyOptions {
/**
* The value to return if the context value was not found and a missing
* context is reported. This should be a dummy value that should preferably
* fail during deployment since it represents an invalid state.
*/
readonly dummyValue: any;
}
export interface GetContextKeyOptions {
/**
* The context provider to query.
*/
readonly provider: string;
/**
* Provider-specific properties.
*/
readonly props?: { [key: string]: any };
/**
* Whether to include the stack's account and region automatically.
*
* @default true
*/
readonly includeEnvironment?: boolean;
}
どうやら Context provider の名前を指定して、その名前に対応する Provider から値を共有してもらう、という流れで使うメソッドのようです。
static メソッドなので、これを呼び出している場所は比較的特定がしやすいはずです。全文検索してみるといくつか見つかりました。ContextProvider の使用例のひとつである Stack クラスの availabilityZones メソッドを見ていくことにします。
ContextProvider を用いて AZ を取得する実装は、Stack クラスの availabilityZones
という getter メソッドとして実装されています[4]。
さて、ここで availabilityZones メソッドの内部に出現する "agnostic" という語彙ですが、個人的には非常に印象的でした。過去の「点」が繋がって大変気分が良かったので、記事の本筋から脱線してでもこの話を掘り下げたいと思い、次のセクションを作ることにします。本筋だけ通すなら次のセクションはスキップでOKです。
閑話: Stack.availabilityZones メソッド内部の "agnostic" という表現に感心した話
該当するソースコードはこちら。
agnostic という語彙は、オープンソースのソースコード内で稀に目にした記憶があります。単語自体には見覚えがありました。
私はこの単語に対して学術的なニュアンスのイメージを持っていましたが、ふと気になって ChatGPT にこの単語のニュアンスを聞いてみました。すると、どうやら技術的文脈においてこの単語は「特定の環境や条件に依存しない」という意味を持つらしいとわかりました。
私なら安直に "unknown" という言葉を選んでしまいそうですが、"agnostic" であれば単に「不明である」という以上の周辺文脈のニュアンスが滲み出ています。非常に明瞭で、わかりやすい語彙選択だと思いました。
そして、この "agnostic" という単語ですが、実は CDK の公式ドキュメントにもちょいちょい登場しています。そして、私たち一般の CDK ユーザーにとってもこの言葉は身近です。
CDK の利用者向けの表現で一例を挙げるなら、おそらく代表的なのは「Environment を指定せず初期化した Stack リソース」です。ここまで呼んでくれた読者であれば、次のようなコードを1度は書いた経験があるのではないでしょうか?
const app = cdk.App();
const stack = new MyStack(app, 'MyStack', {
// Environment が未指定の Stack リソース
// env: {},
});
上記のように env を指定せず作られた Stack では、Vpc.fromLookup などの一部機能が使えません。なぜなら、lookup を実行するためには実在する具体的な「環境」にアクセスするクレデンシャルが必要だからです。環境が特定できないなら fromlookup の処理自体も動かせません。このことは以下のドキュメントでも明言されており、ドキュメントやソースコード上ではこのような env 未指定の Stack は "environment-agnostic stack" と呼ばれています。
https://docs.aws.amazon.com/cdk/v2/guide/resources.html#resources_external
Furthermore, Vpc.fromLookup() works only in stacks that are defined with an explicit account and region (see Environments). If the AWS CDK tries to look up an Amazon VPC from an environment-agnostic stack, the CDK Toolkit doesn't know which environment to query to find the VPC.
また、上記文章中に記載のあるリンク先で、さらに詳しい話を知ることができます。もう少しだけ掘り下げます。
https://docs.aws.amazon.com/cdk/v2/guide/stacks.html#stack_api
例えば、Stack プロパティの region
と account
は、"environment-agnostic stack" の場合は具体的なアカウント、リージョンの文字列ではなく Token の文字列表現となることが示されています。
A string-encoded token that resolves to the AWS CloudFormation pseudo parameters for account and Region to indicate that this stack is environment agnostic
同様に、availabilityZones
プロパティについても "environment-specific stack" に関する言及があります。
stack.availabilityZones (Python: availability_zones) – Returns the set of Availability Zones available in the environment in which this stack is deployed. For environment-agnostic stacks, this always returns an array with two Availability Zones. For environment-specific stacks, the AWS CDK queries the environment and returns the exact set of Availability Zones available in the Region that you specified.
"environment-agnostic stacks" である場合の仕様が面白いですね。本サブセクションの冒頭で提示した Stack.availabilityZones メソッド(一部)がこの仕様の実装であることは明白です。コードコメントにこのような実装にした理由が補足されています。なるほど、と思いました。
if account/region are tokens, we can't obtain AZs through the context
provider, so we fallback to use Fn::GetAZs. the current lowest common
denominator is 2 AZs across all AWS regions.
実装を追いかける(7) - Stack.availabilityZones メソッドの読解
本筋に戻ります。今は ContextProvider.getValue の使用例を追いかけているところで、その事例のひとつである Stack.availabilityZones メソッドを見ていました。実装箇所を再掲します。
ここで ContextProvider.getValue が登場しました。account/region が明示的に指定されている場合に、 "AVAILABILITY_ZONE_PROVIDER" という ContextProvider から値を引こうとしています。
const value = ContextProvider.getValue(this, {
provider: cxschema.ContextProvider.AVAILABILITY_ZONE_PROVIDER,
dummyValue: ['dummy1a', 'dummy1b', 'dummy1c'],
}).value;
ContextProvider.getValue メソッドは、すべての Context のソースから「値」を取得しようとします。Context の情報源は CDK Concepts のドキュメント "Runtime Context" に記載されている仕様の通りです。cdk.context.json だけではなく --context
引数や cdk.json
など複数のソースから値の取得を試みます。このあたりの話は我々一般の CDK ユーザーにとっても馴染みのある話であろうと思います。内部的には Node クラスの tryGetContextを呼び出しています。
AZ の場合も同様にすべての Context ソースから Context value を探そうとする点は同じです。ただ、ContextProvider と通常の(我々が普段自前で利用する)Context の場合では、その運用方法について違いがあります。ContextProvider 特有の話として注目すべきは以下の2点です。
- Context Key として利用する文字列の規則を ContextProvider コントロールしている
- ContextProvider によって取得しようとした Context value が見つからなかった場合のハンドリングは CDK の内部実装のハンドリングに依存し、ユーザーが介在する余地はない
というか、ContextProvider によって管理される Context と、普通の Context との違いはこのくらいしかありません。
単に CDK が内部仕様としてキーの名前空間を管理していて、ユーザーが fromLookup 系の機能を用いた場合は対応する ContextProvider が使われている、というだけの話です。Context という概念の扱いについては、なにも特殊なことはありません。
さて、ここでいったん ContextProvider.getValue の実装に戻ります。
AZ の場合は、AVAILABILITY_ZONE_PROVIDER で規定されている Context key に基づき、Context value の取得を試みます。この処理は、基本的に値が cdk.context.json から読み出されることを期待した設計思想で実装されていること以外は通常の Context となんら変わらず、synth 対象の construct から tryGetContext を呼び出すだけです。
cdk synth/deploy の初回実行時は cdk.context.json が存在しません。ユーザーが意図的に --context
引数や cdk.json
などで値を指定しない限り、ContextProvider による値の取得は失敗します。
ContextProvider による値取得が失敗した場合のハンドリングとして、AZ の場合は Stack.reportMissingContextKey メソッドを介して Cloud Assembly の manifest.missing
に「AZ の情報が手元になかったよ」と書き込まれる(あるいは報告される?)ようになっています。つまり、ここで "missing context" が発生します。
やっと本記事のテーマの答えと言えそうな部分にたどり着きました。
実装を追いかける(9) - ContextProvider を使っている他の場所を探してみる
ここまで Stack.availabilityZones()
の話を掘り下げてきましたが、Lookup と比べたら幾分マイナーな機能であろうと思います(偏見)。
もうちょっと CDK ユーザーにとって身近な例が提示できると良いなと思いました。そこで、ContextProvider.getValue()
が実際に使われている箇所を調べてみました。
VS Code の全文検索で簡単に拾えますが、該当するファイル一覧を書き出しておく方が記事としては明瞭だと思いますので find/grep で探したファイルリストを以下に貼り付けます。
$ find packages -type f -name "*.ts" | xargs grep -n 'ContextProvider.getValue' 2>/dev/null | grep -v test | cut -d: -f 1 | sort | uniq
packages/aws-cdk-lib/aws-ec2/lib/machine-image/machine-image.ts
packages/aws-cdk-lib/aws-ec2/lib/security-group.ts
packages/aws-cdk-lib/aws-ec2/lib/vpc-endpoint.ts
packages/aws-cdk-lib/aws-ec2/lib/vpc.ts
packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/shared/base-listener.ts
packages/aws-cdk-lib/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts
packages/aws-cdk-lib/aws-kms/lib/key.ts
packages/aws-cdk-lib/aws-route53/lib/hosted-zone.ts
packages/aws-cdk-lib/aws-ssm/lib/parameter.ts
packages/aws-cdk-lib/core/lib/stack.ts
packages/aws-cdk/lib/api/plugin/plugin.ts
CDK ユーザーにとって身近そうなものをピックアップして整理してみました。
module | class | method | description |
---|---|---|---|
aws-cdk-lib/aws_ec2 | SecurityGroup | fromLookupById | - |
aws-cdk-lib/aws_ec2 | SecurityGroup | fromLookupByName | - |
aws-cdk-lib/aws_ec2 | InterfaceVpcEndpoint | - | コンストラクタで使用 |
aws-cdk-lib/aws_ec2 | Vpc | fromLookup | - |
aws-cdk-lib/aws_ec2 | LookupMachineImage | getImage | - |
aws-cdk-lib/aws_elasticloadbalancingv2 | BaseListener | _queryContextProvider | protected メソッド。後述する |
aws-cdk-lib/aws_elasticloadbalancingv2 | BaseLoadBalancer | _queryContextProvider | protected メソッド。後述する |
aws-cdk-lib/aws_kms | Key | fromLookup | - |
aws-cdk-lib/aws_route53 | HostedZone | fromLookup | - |
aws-cdk-lib/aws_ssm | StringParameter | valueFromLookup | - |
aws-cdk-lib | Stack | availabilityZones | getter としての実装なので、プロパティとして参照 |
BaseListener, BaseLoadBalancer の2つについてはそれぞれ ALB, NLB の派生クラスが存在しており、それぞれの派生クラスの fromLookup メソッドで利用されています。
- ApplicationListener: fromLookup
- NetworkListener: fromLookup
- ApplicationLoadBalancer: fromLookup
- NetworkLoadBalancer: fromLookup
直接的に ContextProvider.getValue の呼び出し関係がある場所だけをピックアップしてみましたが、概ね fromLookup 系統の機能であることが見て取れます。
実際には、上記で列挙したメソッドを内部的に呼び出している機能も多々あると思いますのでそれらも含めた機能の集合が cdk.context.json を読み書きする可能性がある = synth を複数回実行する可能性がある機能、ということになりそうです。
まとめ (part2)
おおよその流れとして何を調べていたのかを振り返っておきます。
(1) ドキュメントを見る
概要レベルの知識をおさらいしておきます。
(2) App.synth の実装を追う
App.synth が最終的に CloudAssembly 形式を吐き出していることを確認しました。
さらに、関連しそうなドキュメントや JSDoc を漁って、CloudAssembly 形式の JSON Schema による定義を見つけました。ここで、ConetextProvider なる概念があることを突き止めました。これが、前記事で出ていた synth を複数回実行してしまう原因となる "missing context" を発生させている直接的な要因らしいと推測できました。
(3) ContextProvider の実装を追う
実際に ContextProvider が "missing context" に関係する処理を行っている場所を特定しました。また、それを呼び出している機能 (Stack.availabilityZones) をピックアップし、振る舞いを確認しました。ここで、 "missing context" なるモノが synth 実行時(厳密には CDK App の実行時)にどうやって発生しているのかがわかりました。理屈は CDK ユーザーが普段目にする存在としての "Context" とほぼ変わらないらしい、ということが理解できました。
(4) 総括と、最後に残った疑問
ここまでの成果をまとめました。主に "fromLookup" という名前が付く系統の機能の中で ContextProvider が使われていることもわかりました。
part1 では、cdk.context.json への読み書きは cdk synth コマンドの実行時に "missing context" なるモノが発生した場合に行われることがわかりました。
"missing context" は、synth の実行時(厳密には CDK App の実行時)という観点では、fromLookup 系統の機能を使う場合に起こり得るようです。より直接的な原因は、ContextProviver というクラスの getValues メソッドを使用している箇所が "missing context" を生じうる箇所のようです。
ContextProviver というのは少しだけ特殊な Context の仕組みです。ContextProvider は CDK 内部で予約語的に使っている Context key と、その key に対応する value の読み出しを責務とする仕組みと言えます。
ContextProviver が扱っている Context は、基本的に普通の Context となんら変わりません。ただし、ContextProvider では Context key の命名規則にその Provider 独自の仕様があります。また、原則的に ContextProvider が情報源として使うのは実質的に cdk.context.json のみを想定したような設計思想で書かれているようです[5]。これら2点が通常の Context と異なります。それ以外は普通の Context となんら変わりません。
ここまでの成果で、「cdk.context.json の CDK 的にどう扱われているか」という問いにはある程度の回答ができます。つまり、
cdk.context.json は主に fromLookup を使った構築をしている場合に生成される可能性があるファイルで、CDK が synth 時に内部的にキャッシュとして読み書きする、CDK 専用の Context ソースが cdk.context.json である
ということです。普通の CDK ユーザーが cdk.context.json を手で変更することが推奨されない理由は、当該ファイルはこのような立ち位置であるからだと推察します。
cdk.context.json から具体的な値を取得できた場合は "missing context" が発生しないらしい、ということがわかりました。"missing context" が発生した場合は synth が2回以上走るケースがあるというのは前回調査した通りですが、今回の記事ではその主な出所が fromLookup 系統の機能であることもわかりました。
fromLookup 系統の機能は、synth 時に AWS API へのリクエストを伴います。しかし、fromLookup を実装したクラスの中では直接的に API を呼び出すような記述がなく、外部とのやりとりは context からの読み出しを試みているくらいです。Construct クラスの実装で AWS API を呼び出していないとすれば、誰がいつ AWS API を呼び出すのでしょうか...??
この問いに回答するためには、再び CLI 側の synth サブコマンドの実装に立ち戻る必要があります。part3 に続きます。
-
電通総研さんから、一部の日本語訳+解説記事が出ています。DENTU SOKEN Tech blog. - CDK Security And Safety Dev Guide を読んでみた ↩︎
-
拙作ながら私も同 wiki の内容を扱ったアウトプットがありますので、この場で2つご紹介します。(1) Zenn scrap (hassaku63) - [AWS CDK] Security And Safety Dev Guide を通読する, (2) CDK Day 2023 - Configure cross-account deployment using CDK ↩︎
-
CfnReference クラスの "Reference" は CloudFormation の Ref を指すとは限らないことに注意してください。CloudFormation で用いられる広義の「参照」を表現した概念で、JSDoc を眺める限りでは GetAtt や ImportValue に解決される場合もあるようです。ざっくりめのイメージとして、prepareApp は「依存関係」や「参照」をある程度解決する役割を持っていそうだと捉えました。 ↩︎
-
コード検索をする直前までは、AZ の具象値への解決は VPC 関係のクラスに紐づくものと予想していましたので、少々意外でした。ただ、考えてみれば AZ 自体は VPC の従属概念ではないですし、どちらかと言えば AWS サービス的にはグローバルな概念と言えます。具象値としての AZ はリージョンスコープ内で閉じた概念ですが、具象値ではない サービスロケーションとしての AZ はグローバルな概念と解釈しています。そういう意味で、AZ の値を解決する機能が VPC に紐づかないことはごく自然な発想ですし、Stack に紐づくというのも割と妥当な落とし所であるように思いました。 ↩︎
-
このように表現しましたが、逆かもしれません。すなわち、cdk.context.json というファイルの運用方法を CDK が内部的に利用するための領域として位置付けていて、それを扱うための仕組みとして ContextProvider が存在する、という解釈です。個人的にはこちらの解釈の方が CDK の設計思想の説明としては綺麗なように思います ↩︎
Discussion