コンテキストは使い始めるのは簡単ですが、使いこなそうとするとあれこれ判断に迷う機能です。筆者も試行錯誤をくり返しているのですが、使いこなすにはまず核となる基礎知識の整理が必要だろうと常々思っていました。そこで今回はコンテキストの基本的な特徴や仕組みにしぼって、cdk コマンドや cx-api など関連するツールやモジュールとともに説明したいと思います。また、コンテキストの利用例として機能フラグをとりあげ、その概要と課題を説明します。
コンストラクトの機能としてのコンテキスト
第2回で説明したように、コンストラクトを表す Construct
クラスは AWS に限定されない汎用的なクラスです。もともと AWS CDK に含まれていましたが、CDK8s など AWS CDK 以外でも利用できるよう、v1 の途中から単独の constructs
パッケージとして配布されるようになりました。
コンテキスト (context) は、その汎用的なコンストラクトの機能であり、AWS CDK とは関係なく使えます。もちろん AWS CDK でも使えるのですが、コンテキストを使いこなすにはまず AWS CDK と切り離して、コンストラクトの機能としてとらえる視点が必要だと思います。
コンストラクトツリーにおけるコンテキスト
コンテキストは、コンストラクトツリーの外部から内部へと、なんらかのデータを渡すために用いられます。
データ構造としてのコンテキストは、キーと値の組合せにすぎません。以下のコンストラクト (の Node
クラス) のメソッドを用いると、そのコンストラクトに対して任意の値を設定したり、キーに対応する値を取得したりできます。
public getContext(key: string): any public setContext(key: string, value: any): void
ふつう、コンテキストはコンストラクトツリーのルートノードにのみ設定します。コンテキストは、親ノードから子ノードへと引き継がれるため、ルートに設定したコンテキストはツリー内のどのノードからも取得できます。その上で、必要に応じて子ノードで新たなコンテキストを追加したり、親から引き継いだコンテキストの値を変更したりもできます。
例を見てみましょう。
import { Construct } from 'constructs' // ルートノードに Root-context を設定する const root = new Construct(undefined as any, 'Root') root.node.setContext('Root-context', 'set by Root') // ノード A で追加した A-context は A の子ノード B に引き継がれる const a = new Construct(root, 'A') a.node.setContext('A-context', 'set by A') const b = new Construct(a, 'B') // ノード X で変更した Root-context は X の子ノード Y に引き継がれる const x = new Construct(root, 'X') x.node.setContext('Root-context', 'set by X') const y = new Construct(x, 'Y')
いずれの setContext
メソッドも、子ノードを追加する前に呼び出しています。これは子ノードを追加すると、コンテキストを追加したり変更したりできなくなるためです。
このコードを実行すると、次の図のようなコンストラクトツリーが生成されます。
ルートに設定したコンテキストを基本として、サブツリーごとにコンテキストをカスタマイズできることが見てとれると思います。「A」→「B」のサブツリーでは「Root-context」はそのままですが、新たに「A-context」を追加しています。「X」→「Y」のサブツリーでは「Root-context」を書き換えています。
確認のために、各ノードのコンテキストをログに出力してみましょう。
console.log('Root-context of Root', '=', root.node.getContext('Root-context')) console.log('Root-context of A', '=', a.node.getContext('Root-context')) console.log('Root-context of B', '=', b.node.getContext('Root-context')) console.log('Root-context of X', '=', x.node.getContext('Root-context')) console.log('Root-context of Y', '=', y.node.getContext('Root-context')) console.log('A-context of A', '=', a.node.getContext('A-context')) console.log('A-context of B', '=', b.node.getContext('A-context'))
出力結果は以下のようになります。
Root-context of Root = set by Root Root-context of A = set by Root Root-context of B = set by Root Root-context of X = set by X Root-context of Y = set by X A-context of A = set by A A-context of B = set by A
AWS CDK におけるコンテキスト
コンテキストは AWS CDK アプリにおいても、アプリの外部から内部へなんらかのデータを渡すために用いられます。渡されたデータは、アプリの内部でさまざまな処理に利用できます。
上で見たコンストラクトの機能としてのコンテキストは、もちろん AWS CDK アプリでも使えます。次の例では、おなじみの Stack
コンストラクトに setContext
メソッドでコンテキストを設定しています。
const stack = new Stack(app) stack.node.setContext('Stack-context', 'set by Stack')
ただ、このように直接 setContext
メソッドを呼び出してコンテキストを設定することはあまりないでしょう。AWS CDK アプリでは、ふつうは cdk コマンドのオプションやファイルを用いてコンテキストを設定します。以下、それらの設定方法を詳しく見ていきましょう。
--context
オプションによるコンテキストの設定
一つ目は cdk コマンドの --context
オプションを用いる方法です。--context
は synth
や deploy
など各 cdk コマンドで使える global option の一種です。
--context
オプションの引数には キー=値
の形式でコンテキストを指定します。コンテキストが複数ある場合は --context
も複数指定します。以下はアプリをシンセサイズするときにコンテキストを指定する例です。
cdk synth --context key1=value1 cdk synth --context key1=value1 --context key2=value2
cdk コマンドを実行すると --context
で指定したコンテキストはあらかじめ決められた場所に格納されます。そこはいわば AWS CDK アプリにコンテキストを渡すための中継所です。
中継所は、具体的には環境変数や一時ファイルなのですが、普段は特に気にする必要はありません。cdk コマンドと AWS CDK アプリが、見えないところで適切に処理してくれるからです。
cdk コマンドにより起動された AWS CDK アプリの App
コンストラクトは、その中継所からコンテキストを取得し自身に設定します。App
はツリーのルートですので、設定したコンテキストはツリー全体で使えます。App
は子ノードとなる Stack
や L1/L2/L3 コンストラクトのためにコンテキストを設定する特別な役割を担っているのです。
cx-api モジュール
第1回で、ふつう AWS CDK アプリは直接実行せず、cdk コマンドを用いて実行すると説明しました。cdk コマンドとアプリが協調して動作できるのは、コンテキストの中継所など、あらかじめ決められた前提条件や約束事に cdk コマンドもアプリも従っているからです。
それら前提条件や約束事が明確に定義されている公式ドキュメントがほしいところですが、筆者が知る限りそのようなものはありません。かわりにソースコードや GitHub のコメントなどから推測することになるのですが、その中でも重要なのが cx-api
というモジュールです。
cx-api
は cdk コマンドとアプリが協調して動作するための共通のクラスやインターフェース、定数等をまとめたモジュールです。cdk コマンドのパッケージ aws-cdk
と AWS CDK コンストラクトライブラリのパッケージ aws-cdk-lib
は、どちらも cx-api
を用いて開発されています。
コンテキストの中継所である環境変数の名前も cx-api
の定数 CONTEXT_ENV
として定義されています。確かめたければ、適当な AWS CDK アプリに次のようなログを出力するコードを追加してみましょう。
import * as cxapi from 'aws-cdk-lib/cx-api' console.log(cxapi.CONTEXT_ENV, '=', process.env[cxapi.CONTEXT_ENV])
そうしてコンテキストを --context key1=value1
のように指定して cdk コマンドを実行してみましょう。次のように、指定したコンテキストが CDK_CONTEXT_JSON
という環境変数に含まれていることがわかるはずです。
CDK_CONTEXT_JSON = {...,"key1":"value1",...}
ちなみに cdk コマンドは「CDK Toolkit」とも呼ばれますが、初期の CHANGELOG によると AWS CDK が公開される前に「cx Toolkit」と呼ばれていた時期があり、コマンド名も「cdk」ではなく「cx」だったようです。cx-api
モジュールと cdk コマンドは、それぞれ API とそのクライアントツールとして密接な関係のもとに構想されたのでしょう。
cdk.json ファイルによるコンテキストの設定
cdk コマンドでコンテキストを設定するもう一つの方法は、ファイルを使うことです。使えるファイルはいくつかありますが、今回はもっとも基本的な cdk.json ファイルについて説明します。
AWS CDK アプリには定番と言える各種ファイルやディレクトリがあります。それらは一つ一つ開発者が作るのではなく、cdk init
コマンドを実行して生成するのが一般的であり、その一つが cdk.json です。
cdk.json を読みとり利用するのは cdk コマンドです。cdk.json の各キーと値は、cdk コマンドのオプションとデフォルト値に相当します。あらかじめ cdk.json を書いておくと、cdk コマンドを実行するたびにオプションを付けなくてよいので便利というわけです。1
以下は cdk init
コマンドを実行して出力される TypeScript 用の cdk.json の例です。
{ "app": "npx ts-node --prefer-ts-exts bin/app.ts", ... }
cdk コマンドの --app
オプションには、AWS CDK アプリを実行するためのコマンドを指定します。この例では npx ts-node
によりアプリのファイル bin/app.ts
を実行しています。--app
はアプリを実行するには必須のオプションですが、ふだんは cdk コマンドに直接付けることはありません。このように cdk.json にデフォルトの値が設定されているからです。
同じように --context
オプションもあらかじめ cdk.json に書いておけます。
{ "app": "npx ts-node --prefer-ts-exts bin/app.ts", "context": { "key1": "value1", "key2": "value2" }, ... }
cdk synth
コマンドを実行するときに、この cdk.json と同じオプションを付けると以下のようになります。
cdk synth \ --app 'npx ts-node --prefer-ts-exts bin/app.ts' \ --context key1=value1 \ --context key2=value2
cdk コマンドに付けたオプションと cdk.json に書いたオプションが重複した場合、cdk コマンドが優先されます。cdk.json はあくまで cdk コマンドで何も指定しないときのデフォルト値です。
cdk.json は cdk コマンドのファイル
このように cdk.json は cdk コマンドが読み取って利用するファイルです。先ほども紹介した初期の CHANGELOG によると、もともとは cx.json という名前であり、cx コマンドを cdk コマンドに改名するとき一緒に cdk.json に改名したようです。そのことからも cdk.json が cdk コマンドのファイルであることは明らかでしょう。
しかし、名前からなんとなく AWS CDK アプリのファイルのようにみなしてしまう人も多いと思います。筆者も Jest でユニットテストをしていて、cdk.json に書いたはずのコンテキストがアプリで取得できず悩んだことがあります。GitHub にも同様のイシューが上がっており、わりとよくあることなのではと思います。
けれども先ほどの説明を踏まえれば、cdk コマンドを使わないユニットテストで cdk.json が読み込まれないのは当然ですね。ユニットテストでコンテキストを設定したければ、setContext
メソッドや、App
コンストラクトの context
/postCliContext
プロパティを使うとよいでしょう。
コンテキストの利用例:機能フラグ
さてここからは AWS CDK の機能フラグ (feature flag) を題材に、コンテキストが具体的にどのように利用されるのか見ていきましょう。
一般に機能フラグとは、コードを書き換えることなく特定の機能を有効にしたり無効にしたりできる仕組みと言えます。AWS CDK の機能フラグは、破壊的変更を新しい AWS CDK のバージョンに取り込むために用いられます。破壊的変更とは、既存の AWS CDK アプリが期待通りに動作しなくなるような新機能やコード修正のことです。
機能フラグはコンテキストとして設定します。AWS CDK の内部では、機能フラグのコンテキスト値をもとに、破壊的変更かそれ以外の処理に分岐します。
機能フラグのコンテキストは、ふつうは以下のように cdk.json に書いておきます。
"context": { "@aws-cdk/core:checkSecretUsage": true }
checkSecretUsage
は対象となる機能の名前で、aws-cdk/core
はそのモジュールです。true
ですのでこの機能は有効です。
破壊的変更の機能フラグは、デフォルトではすべて false
つまり無効になっています。そのおかげで、既存のアプリは破壊的変更による影響を受けずに AWS CDK のバージョンを上げられます。
いっぽう新規にアプリを開発する際、cdk init
を実行すると cdk.json には上のような true
の機能フラグが多数出力されます。AWS CDK は、新規のアプリでは破壊的変更であってもなるべく使ってもらおうとするのです。
機能フラグの課題
機能フラグは、コンテキストとして値を設定するだけの単純なものです。一見それで問題なさそうですが、まだ検討段階だった 2019 年末当時から、機能フラグにはいくつかの課題が指摘されていました。
それらは機能フラグに限らず、コンテキストを利用するときの一般的な注意としても参考になると思います。以下、機能フラグに関する AWS CDK RFC 0055 や GitHub のコメントに書き残されているそれらの課題を、筆者の私見をまじえて紹介します。
課題1. 見分けにくい
たとえば、以下のような cdk.json ファイルがあるとします。
"context": { "@aws-cdk/core:target-partitions": [ "aws", "aws-cn" ], "@aws-cdk/core:permissionsBoundary": { "name": "cdk-PermissionsBoundary", }, "@aws-cdk/core:checkSecretUsagee": true }
このうち、機能フラグは一つ目のコンテキスト @aws-cdk/core:target-partitions
だけです。この機能フラグを有効にするには、珍しく true
ではなくこのように文字列の配列を指定します。
二つ目の @aws-cdk/core:permissionsBoundary
は、デフォルトの permissions boundary を適用するための機能です。2022 年末に追加された機能ですが、破壊的変更とはみなされておらず、機能フラグではありません。
三つ目の @aws-cdk/core:checkSecretUsagee
は、先ほどの例の機能フラグをわざと書き間違えて、末尾を ee
にしたものです。機能フラグではありませんが、コンテキストとしては正しいので特にエラーは発生しません。静かにアプリに渡されるだけで、どこでも使われることはないでしょう。
このように機能フラグは単なるコンテキストであり、ながめているだけでは他のコンテキストと区別がつきません。どれが機能フラグであり、どんな効果があり、どう使ったらよいかを、FEATURE_FLAGS.md というドキュメントで調べて、注意深く設定しなければならないのです。
課題2. 乱用されやすい
FEATURE_FLAGS.md は、すべての機能フラグについて詳しく説明しているドキュメントです。それをもとに AWS CDK v2 の機能フラグを数えてみると、現在 40 個以上あることが分かります。
いっぽう機能フラグの RFC 0055 には、一つのメジャーバージョンで 20 個を超えたら多すぎだろうと書かれています。それと合わせて、機能フラグは後方互換性を保てない場合の最終手段であり乱用 (abuse) しないよう注意を促しているのですが、結局当初の目安の倍以上に増えてしまったのです。
増えすぎた一因は、コンテキストの手軽さにあるように思います。破壊的変更を避けて、後方互換性を保てる機能を設計・実装するよりも、機能フラグを追加することは簡単です。キーと値の組合せにすぎないコンテキストを追加し、その値によって処理を分岐させればよいからです。
課題3. 機能を追加しにくい
この課題はひょっとしたらスーパーエンジニアによってあっさり解決されてしまうかもしれませんが、現在の機能フラグを改善・強化するにはかなりの労力が必要なように思います。機能フラグには、これといった抽象化も、保守性や拡張性を高める設計もなく、キーと値の組合せというむき出しのコンテキストだからです。
GitHub には 機能フラグをもっと使いやすい形に再設計しようというイシューが上がっていますが未解決のままです。RFC 0055 や GitHub のコメントを見ると、2019 年末の検討段階では、複数のフラグをグループとして管理する機能や、AWS CDK のバージョンに合わせて管理する機能などが議論されていたようです。それらは機能フラグが増えるなど必要になったときに実装すればよいとされましたが、数年経って機能フラグが増えすぎた今でも実装されていません。
おわりに
今回はコンテキストの特徴や仕組みについて、基本的なものにしぼって説明しました。
コンテキストは、まずコンストラクトの機能としてとらえる視点が必要です。データ構造としてのコンテキストはキーと値の組合せにすぎませんが、設定・取得する対象はコンストラクトツリーです。
コンテキストを AWS CDK アプリに設定する際、コンストラクトの機能である setContext
メソッドを直接呼び出すことはあまりないでしょう。かわりに、ふつうは cdk コマンドのオプションやファイルを用いて設定します。
コンテキストの利用例としては、シンプルな機能フラグをとり上げました。この他にもやや込み入った利用例や関連するコンセプトがコンテキストにはありますが、それらは次回以降に説明できればと思います。
参考資料
本文ではふれませんでしたが、以下の公式ガイドがあります。
- コンテキスト:『Context values and the AWS CDK』
- 機能フラグ:『AWS CDK feature flags』