🐡

cdk.context.json が CDK 的にどう扱われているか調べてみた - part3

2024/07/30に公開

以下の記事の続編です。

https://zenn.dev/hassaku63/articles/ecf1ca0cf74342

https://zenn.dev/hassaku63/articles/3bf6d3a02481a8

前提事項

ここまでのシリーズと同様に、この記事で参照する CDK のバージョンは v2.150.0 を前提とします。

https://github.com/aws/aws-cdk/tree/v2.150.0

https://github.com/aws/aws-cdk/tree/v2.150.0

$ git checkout -b v2.150.0 tags/v2.150.0

そんなに調査した結果に確信があるわけじゃありません。読み飛ばしや誤読などにより私が不正確な理解をしている可能性もあります。内容に 100% の正確性は保証できないことをご了承ください。

やったことや得られた洞察など、結果を先に見たいという人は最後の「まとめ」までジャンプしてください。

前回のおさらいと今回の主テーマ

cdk synth コマンドを実行すると、コマンドの内部動作としては --app で指定した CDK App が実行されます。

# e.g.) execute my-app
$ cdk synth --app 'npx ts-node bin/my-app.ts'

この --app の中で指定している内容がなんなのかを追いかけました。CDK プロジェクトの開発者は、 App.synth() の実行を明示的あるいは暗黙的に指定します。結果として Cloud Assembly が生成sれます。この過程で fromLookup など一部の機能を使うと "missing context" が発生するケースがあります。このとき、値の解決方法として cdk.context.json のような "Context" が利用されます。App クラスのロジックとしての "synth" はこのような振る舞いをします。

一方で、CLI としての "synth" コマンドは、 "missing context" が発生した場合に再び上述のような CDK App を実行し、"missing context" の解消を図ります。最終的にすべての "missing context" が解決されるか、あるいは解決不能と判断されるまでこの処理は繰り返されます。

cdk.context.json の読み書きを行う用事として最も典型的なのが fromLookup 系統のメソッドです。この機能は AWS API のリクエストを伴い、実在する環境からリソース情報を取得します。一方で、 Construct クラスの中では必要とされる「値」の取得処理は Context を情報ソースとしており、AWS API へのリクエストを行っているようには見えません。

それでは、AWS API へのリクエストはいつ誰が行っているのでしょうか?これが今回のテーマです。

結論

上記の前置きの時点でほとんど答えは出ているような気もしますが、最初に結論を述べておくと、AWS API を実行しているのは CLI コマンドとしての "synth" です

synth コマンドがサブプロセスとして実行している「CDK App の実行」という流れので、AWS API へのリクエストが実行されています。

やったことを雑多に書く (part3)

part2 に引き続き、fromLookup の(ある意味)お仲間である Stack.availabilityZones の実装を題材にします(とはいえ、すぐ CLI 側の実装にジャンプするのですが)。

Stack.availabilityZones が使っている ContextProvider を確認

ポイントは ContextProvider です。Stack.availabilityZones では "AVAILABILITY_ZONE_PROVIDER" という ContextProvider を使用しています。

https://github.com/aws/aws-cdk/blob/v2.150.0/packages/aws-cdk-lib/core/lib/stack.ts#L835-L838

synth コマンドの実装内で ContextProvider に触れている箇所を探す

CLI の実装を方を見ていくと、ContextProvider に関する実装が CloudExecutable の doSynthesize の実装に含まれていることがわかります。ちょうど、part1 でも「複数回 synth が実行される場合がある」として触れていた部分です。

https://github.com/aws/aws-cdk/blob/v2.150.0/packages/aws-cdk/lib/api/cxapp/cloud-executable.ts#L102-L105

provideContextValues という関数の実装を紐解いていきます。

https://github.com/aws/aws-cdk/blob/v2.150.0/packages/aws-cdk/lib/context-providers/index.ts#L27-L80

"missing context" がある場合に ContextProvider が使われていることがわかります。このあたりはこれまで読んできたソースの解釈とも整合します。

ざっくり中身を見ると、パラメータで利用したい ContextProvider を選択し、その Provider の getValue を呼び出しているようです。 "AVAILABILITY_ZONE_PROVIDER" の場合は AZContextProviderPlugin クラスが対応関係にあります。

AZContextProviderPlugin クラス

実装はこちら。

https://github.com/aws/aws-cdk/blob/v2.150.0/packages/aws-cdk/lib/context-providers/availability-zones.ts#L8-L26

SdkProvider とはなんなのか?などの細部の疑問はありますが、本質的にやっていることは次の2つだと言ってよいでしょう。

  • "lookupRole" を使って AssumeRole したクレデンシャルを取得している
  • 取得したクレデンシャルで Environment (account/region) の AZ 情報を得るために EC2 DescribeAvailabilityZones API を呼び出している

つまり、AWS API を呼び出しを行い、その結果で "missing context" を穴埋めして cdk.context.json に書き出す役割は、Construct クラス側では持っていない。CLI 側で実行されている ContextProvider クラスの各派生クラスの責務として API 呼び出しを行っている、ということがわかります。

まとめ (part3)

part1 から続いた内容をさらっとまとめます。

"cdk.context.json" というファイル自体は、Context としての扱い という観点からは他の Context ソース (cdk.json やコマンドライン引数など) と変わりありません。ただし、CDK の内部実装が使うためのほぼ専用のソースとして読み書きしているファイルであることや、実際に格納されるオブジェクトのキー/値はシステム (CDK) 側が取り決めた仕様に従う表現を用いている点が、他の Context と異なります。

このような性質のあるファイルなので、基本的に cdk.context.json は CDK ユーザーが Context のソースとして使うべきファイルではなく、手で編集すべきでもない、ということが言えます。

そして、このファイルが生成される条件は "missing context" というモノが発生した場合であることもわかりました。CDK ユーザーの視点では、これが発生するケースは主に formLookup 系列の機能を使っているケースです。

そして、"missing context" が発生するケースでは、内部で synth(厳密には CDK App の実行)が複数回走ってしまう仕様があります。コードを読み解く中で、その理由もわかりました。

まさしく "missing context" が理由であり、実態は fromLookup 等を使って AWS API 経由で取得した情報...例えば AZ 情報などを、手元 (cdk.context.json) に書き出しています。その書き出し結果をもとに、もう1度 synth が実行され、"missing context" がなくなるまでこれが繰り返されます。この仕様によって「synth が2回以上走る」というケースが発生しています。

fromLookup を使った CDK App の synth は AWS API へのリクエストを伴う場合があります。しかし、CDK App の実行時には API 呼び出しを行っていません。「CDK App の実行」をサブプロセスとして spawn している "CLI コマンドとしての synth" の方で、ContextProvider クラスを用いて API リクエストを行っている(そしてその内容を cdk.context.json に書き出す)らしい、ということがわかりました。

余談

この調査の過程で新たな疑問が生じたので、今後の探求テーマとして。

自前で Construct を実装する際に、AWS API を実行したくなる場合があると思います。例えば fromLookup をサポートしない何らかのリソースの存在確認をしたい、とかですね。

CDK synth コマンドや App.synth の実装を見ている限りでは、API 呼び出し自体は CDK App の実行サイクルの外で行うようになっています。つまり、 CDK ユーザーとしても、AWS API の呼び出しは「 Construct の中で行うべきではない」 ということなのでしょうか?

Construct の中で API 呼び出しをすべきでない場合、どこで代替すれば良いのでしょうか?そもそも呼ぶべきではないのでしょうか、それとも ContextProvider を自作すべきなのでしょうか?あるいは、そんなことは気にする必要がなく、Construct の中で実装してしまっても良いのでしょうか。

このへんはまた、別途調べて記事にしてみようと思います。

part1〜3 を通して感想

一応、掲題のシリーズはこれにて完結です。くぅ疲。

この記事が一番ボリュームが小さいのですが、それでも約 4,200 文字くらいあるようです。感覚が麻痺してしまっているけどこれでも十分多いですね。part1 からの全体では約 58,000 字ほどあるようです。削るのが下手くそすぎる。

こんな誰得かわからんような記事を読んでいただける方自体が相当レアだと思うんですが、もしここまで全部見てくれた方がいらっしゃるのでしたら、めちゃくちゃ嬉しいです。通読ありがとうございました。

Discussion