今回は決定論という考え方を中心に、コンテキストについてさらに詳しく見ていきます。決定論を語る上でコンテキストは欠かせません。しかし逆に、コンテキストを語るには決定論という観点だけでは足りないと筆者は考えており、その点についても説明します。前回、前々回に説明したコンテキストの知識が前提となりますので、適宜参照してください。
決定論とは
決定論 (determinism) は AWS CDK に限らず、哲学などさまざまな分野で用いられる用語です。今回取り上げるのは哲学などの決定論ではもちろんなく、AWS CDK における決定論です。
AWS CDK アプリが決定論的 (deterministic) であると言うとき、そのアプリをデプロイするといつも同じ結果、同じリソースが得られることを意味します。AWS CDK 公式のベストプラクティスでは、以下のようにアプリを決定論的に作るよう勧めています。
Determinism is key to successful AWS CDK deployments. An AWS CDK app should have essentially the same result whenever it is deployed to a given environment.
決定論は AWS CDK のデプロイを成功させる鍵である。AWS CDK アプリというものは基本的に所与の環境にデプロイされたらいつも同じ結果になるべきである。
アプリを決定論的にするために、暗黙の前提として Git などのバージョン管理システムを利用することが想定されています。決定論的なアプリであれば、同じコミットのデプロイ結果はいつも同じになるはずです。
以下はおなじみ、AWS CDK を生み出したチームを率いていた Elad Ben-Israel 氏の GitHub のコメントです。
When we say “deterministic” in this context we mean that a commit in your repository will always produce the same CDK output. This is a common invariant for compilers and build systems and this is where the CDK tenet comes from.
この文脈における「決定論的」とは、リポジトリのコミットが常に同じ CDK の出力を生成するということです。これはコンパイラやビルドシステムに共通する不変条件であり、CDK の信条が由来するところです。
コンパイルやビルド、CI/CD の領域においても “deterministic” や “reproducible” という言葉を冠する同じような考え方があります。それらが AWS CDK における決定論の基になっているのでしょう。
決定論的である例・決定論的でない例
決定論は AWS CDK 全体にかかわるコンセプトであり、GitHub のコメントやソースコード等あちこちに登場します。しかし AWS CDK の開発者向け公式ガイド『Developer Guide』には専用の解説ページはなく、抽象的・概念的な説明としては、上のように短く断片的なものしかありません。
そこで決定論とは何かをさらに詳しく説明するために、以下では決定論的である・決定論的でないとよく言われる具体的な例を紹介します。
はじめに、決定論的でない例として CloudFormation のパラメータを紹介します。AWS CDK アプリでは CloudFormation のパラメータを使えますが、使うことはアンチパターンであると Elad Ben-Israel 氏は述べています。
決定論的である例としては cdk.context.json
ファイルを紹介します。決定論的であるために、出力した cdk.context.json
は消さずにそのままコミットします。
決定論的でない例:CloudFormation のパラメータ
それでは決定論的でないとよく言われる AWS CloudFormation のパラメータから見ていきましょう。
CloudFormation はパラメータを用いて、一つのテンプレートから異なるデプロイ結果を得ることができます。デプロイするときにパラメータの値を変更し、作成するリソースを柔軟にカスタマイズできるのです。
CloudFormation の公式ベストプラクティスは、パラメータを用いてテンプレートを再利用するよう勧めています (“Reuse templates to replicate stacks in multiple environments”)。AWS CDK が決定論的であることを勧めているのに対し、そのバックエンドのサービスである CloudFormation は真逆のことを勧めているのです。
CloudFormation のパラメータのコード例
パラメータは、テンプレートの Parameters
セクションに定義します。
"Parameters": { "BucketParameter": { "Type": "String", "AllowedValues": [ "yes", "no" ], "Default": "yes", } }
BucketParameter
に指定できる値は "yes"
か "no"
いずれかの文字列です。何も指定しない場合、デフォルトで "yes"
が指定されたことになります。
パラメータはよく Conditions
セクションと組み合わせて使います。
"Conditions": { "BucketCondition": { "Fn::Equals": [ { "Ref": "BucketParameter" }, "yes" ] } }
BucketCondition
は BucketParameter
が "yes"
の場合は true
、それ以外は false
になります。これを以下のようにリソースの Condition
に指定することで、リソースを作成するかしないか制御できます。
"Resources": { "Bucket": { "Type": "AWS::S3::Bucket", "Condition": "BucketCondition" } }
この例では BucketCondition
が true
の場合に Bucket
は作成され、false
の場合は作成されません。
このようなテンプレートを生成する AWS CDK アプリは以下のようになります。
import * as core from 'aws-cdk-lib/core' import * as s3 from 'aws-cdk-lib/aws-s3' const app = new core.App() const stack = new core.Stack(app) const bucket = new s3.Bucket(stack, 'Bucket') // Parameters セクション const bucketParameter = new core.CfnParameter(stack, 'BucketParameter', { allowedValues: ['yes', 'no'], default: 'yes' }) // Condition セクション const bucketCondition = new core.CfnCondition(stack, 'BucketCondition', { expression: core.Fn.conditionEquals(bucketParameter, 'yes') }) // バケットの L1 コンストラクトを取り出して Condition を設定する const cfnBucket = bucket.node.defaultChild as s3.CfnBucket cfnBucket.cfnOptions.condition = bucketCondition
この例の bucketParameter
が "yes"
か "no"
か判定しようとして if
文を書いても、期待通りには動きません。bucketParameter
はトークン (第7回) であり、シンスタイムでは "yes"
とも "no"
とも決まっていないからです。決まるのはデプロイタイムであり、"yes"
か "no"
かで異なるデプロイ結果になるため、決定論的ではないというわけです。
パラメータは CDK のアンチパターン?
CloudFormation テンプレートのパラメータの値は、cdk コマンドの --parameters
オプションで指定できます。
cdk deploy --parameters BucketParameter=no
この例では "no"
なのでバケットは作成されません。パラメータを何も指定しなければ、デフォルト値は "yes"
なのでバケットは作成されます。
この --parameters
オプションが追加されたのは、AWS CDK v1 が GA 化されてから半年以上後の v1.28.0 でした。初期の頃から GitHub のイシューが作られ、コミュニティの要望として認識されていたにもかかわらずです。
理由は CloudFormation のパラメータは決定論的ではなく、CDK のアンチパターンとみなされていたからでした。Elad Ben-Israel 氏は以下のようなコメントを残しています。
As mentioned above, using CloudFormation parameters is generally an anti-pattern for CDK apps given “synth-time” resolution is more deterministic and allows you to reason about values in your code, but we understand that people who come from existing CloudFormation workflows may still want to leverage parameters.
上で述べたように、「シンスタイム」での解決はより決定論的でかつコード内の値を推測できると考えると、CloudFormation のパラメータを使うことは一般的に CDK アプリにとってアンチパターンである。しかしそれでもなお既存の CloudFormation のワークフローからやって来た人たちがパラメータを活用したいかもしれないことは理解している。
AWS CDK の創始者に言わせれば、現在 AWS CDK でパラメータが使えるのはあくまで実用的な要求からであって、決定論の観点からは望ましくないわけです。
決定論的である例:cdk.context.json ファイル
次に、決定論的である例として cdk.context.json
ファイルを見てみましょう。
前回紹介したように、AWS CDK が提供するコンストラクトやクラスの中には AZ や VPC など「環境」のコンテキスト (Environmental Context) を cdk.context.json
に保存してくれるものがあります。次の例、aws-ec2
モジュールの LookupMachineImage
クラスもその一つで、「環境」から条件に合致する最新のイメージ情報を取得、保存しています。
import * as ec2 from 'aws-cdk-lib/aws-ec2' const lookupMachineImage = new ec2.LookupMachineImage({ // 条件1: AMI名 name: 'al2023-ami-2023.*-kernel-*-x86_64', // 条件2: AMIの所有者 owners: ['amazon'], }); // 最新のイメージ情報 const imageConfig = lookupMachineImage.getImage(stack) // AMI ID const imageId = imageConfig.imageId
以下は cdk.context.json
に保存された最新のイメージ情報です。
{ "ami:account=123456789012:filters.image-type.0=machine:filters.name.0=al2023-ami-2023.*-kernel-*-x86_64:filters.state.0=available:owners.0=amazon:region=ap-northeast-1": "ami-1111" }
キーはアカウント/リージョンつまり「環境」とイメージの検索条件であり、値は AMI ID です。次にシンセサイズ、デプロイするときは「環境」から再び情報を取得することはなく、代わりにこのキャッシュ (cache) された AMI ID を取得します。そのため AWS CDK アプリを何度デプロイしても、この AMI ID を用いて作成する EC2 インスタンスは同じ "ami-1111"
のインスタンスになります。
それだけでも決定論的と言えそうですが、AWS CDK の決定論はバージョン管理システムの利用を前提としていましたね。AWS CDK 公式のベストプラクティスでは、cdk.context.json
ファイルをコミットすることを勧めています (“Commit cdk.context.json to avoid non-deterministic behavior”) 。そうすることで、同じコミットからいつも同じ "ami-1111"
のインスタンスを作成してくれる、決定論的なアプリを実現できるわけです。
コミットされたコンテキストは、すぐに変化・消失しかねない印象のキャッシュと呼ぶよりも、もっと長く保存して利用するスナップショット (snapshot) と呼ぶ方が適切かもしれません。以下は、同じベストプラクティスからの引用です。
Fortunately, the AWS CDK includes a mechanism called context providers to record a snapshot of non-deterministic values.
幸いなことに、AWS CDK にはコンテキストプロバイダーという仕組みがあり、非決定論的な値のスナップショットを記録できる。
このコンテキストプロバイダーを含めた cdk.context.json
関連の仕組みについては、前回の説明を参照してください。
もしも cdk.context.json が無かったら
上の例でもしも cdk.context.json
が無かったら、アプリは決定論的ではなくなります。最新のイメージ情報はある日 AWS によって変更され、それにともないデプロイ結果も異なるものになるからです。
この点について、もう少し詳しく説明してみます。
上の例で初めて AWS CDK アプリをデプロイした時、最新の AMI ID は 'ami-1111'
だったとします。そしてある日、AWS によって最新の AMI ID は 'ami-9999'
に変更されたとします。
cdk.context.json
が無ければ、このアプリはシンセサイズのたびに最新の AMI ID を取得します。そのため、次にアプリをシンセサイズして出力されるクラウドアセンブリには、既存の 'ami-1111'
のインスタンスの代わりに 'ami-9999'
のインスタンスの定義が出力されます。それをデプロイすると、既存の 'ami-1111'
のインスタンスは破棄され、代わりに 'ami-9999'
のインスタンスが作成されます。
この場合、インスタンスに関連するコードを変更したかどうかは関係ありません。アプリのコードは同じでも、再度シンセサイズ、デプロイしたある日突然、意図せずしてインスタンスが置き換えられてしまうのです。
決定論でコンテキストを説明できるか?
決定論的であるとは cdk.context.json
をコミットすることであると言ってもよいぐらいに、コンテキストは AWS CDK の決定論を語る上で欠かせないものです。しかし逆に、コンテキストの特徴や使いどころを語るにあたり、決定論という観点だけで十分かというと、そうではないと筆者は考えています。
なぜなら、決定論的であることだけでよいならば、コンテキストは絶対必要であるとも最良の選択肢であるとも言えないからです。また、AWS CDK アプリは絶対に決定論的でなければならないかというと、そうでもありません。以下の本記事後半では、それらの点について説明した上で、コンテキストの特徴や使いどころをより的確に表すと筆者が思う言葉「制御された更新」を紹介します。
コンテキストの代わりのハードコード
決定論的でさえあればよいならば、コンテキストを使わずその値をハードコードする方法があります。前回説明したとおり、cdk.context.json
やコンテキストプロバイダーは AWS CDK アプリから他のアプリで管理されているリソースを簡単に参照するための仕組みです。それらの代わりに、面倒ではありますが fromAttribute
メソッド等を用いて、リソースの属性値を一つ一つ調べてハードコードしてもよいのです (第5回)。
ハードコードは単なる定数やマジックナンバーであるのに対し、コンテキストはコンストラクトツリーと関連付けられている点が特徴と言えるかもしれません。しかし、今のところその特徴を生かしたコンテキストならではの使い方は特にありません。ふつうコンテキストはアプリのルートである App
に渡されたものがツリー全体で参照できるため、実践的にはパブリックな定数とそこまで大きな違いは感じられません。
AWS が公開している BLEA という AWS CDK テンプレート群では、かつて v2 までは各ユースケース固有のパラメータを指定するためにコンテキストを利用していました。しかし v3 からは parameter.ts
というファイルにハードコードする方式に変更しています。決定論的であるかどうかという観点だけでは、コンテキストでもハードコードでも大きな違いはないのです。むしろハードコードには型チェックや補完が効くなど JSON 形式のコンテキストにはない利点があり、コンテキストよりも優れた方法にすら見えてきます。
決定論的にしないという選択肢
AWS CDK は決定論的でないことを絶対に禁止しているわけではありません。そのことは、先ほど見たように CloudFormation のパラメータを利用できることからも明らかでしょう。
また、AWS CDK は決定論的であるかどうか選択できるクラスも提供しています。以下は aws-ec2
モジュールの MachineImage
クラスを用いて、最新の Amazon Linux 2023 イメージの情報を取得するコードです。
const machineImage = ec2.MachineImage.latestAmazonLinux2023({ cachedInContext: true })
例のように cachedInContext
プロパティが true
の場合、取得した情報は先ほどの LookupMachineImage
と同様、cdk.context.json
ファイルに保存されて再利用されます。
しかし cachedInContext
のデフォルト値は false
です。latestAmazonLinux2023
メソッドの主なユースケースは決定論的ではなく、最新のイメージ情報をコンテキストとして保存せずに都度取得することなのです。
cachedInContext
が false
の場合、出力される CloudFormation テンプレートは以下のようになります。
// CloudFormation のパラメータを用いて、デフォルトで SSM パラメータストアから最新の AMI ID を取得する "Parameters": { "SsmParameterValueawsserviceamiamazonlinuxlatestal2023amikernel61x8664Parameter": { "Type": "AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>", "Default": "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64" }, ... // CloudFormation のパラメータを参照して AMI ID を設定する "ImageId": { "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestal2023amikernel61x8664Parameter" }, ...
アンチパターンとまで言われた CloudFormation のパラメータが活用されていますね。EC2 インスタンスの破棄・再作成に慎重になる必要が無く、なるべく早めに最新の AMI を適用したい場合、コンテキストで決定論的に作るよりもこちらの方がずっと便利でしょう。あるいは AMI に特にこだわりが無い場合、デフォルトでは最新を使い、ときどき必要に応じて --parameters
オプションで AMI ID を指定するような使い方もできるでしょう。
そもそもデータによっては cdk.context.json
に保存したくないものもあります。例えば SSM パラメータストアや AWS Secrets Manager に保存する秘密情報は随時変更されうるものですが、ファイルに平文で書き込んでコミットするわけにはいきません。その点はベストプラクティスも認めており、秘密情報をコミットする代わりに当該リソースの名前や ARN で参照することを勧めています
(“Use services like Secrets Manager and Systems Manager Parameter Store for sensitive values that you don’t want to check in to source control, using the names or ARNs of those resources.”)。
決定論的であるべきかどうかは、結局のところ目的やユースケース次第なわけです。実践的なアドバイスとして、決定論的であるほどデプロイは安定し成功しやすくなるとは言えても、いついかなるときも決定論的である必要はないはずです。
制御された更新
以上のようにハードコードのような代替手段や、決定論的でない方が便利なケースなどを見ていると、コンテキストの特徴や使いどころについて考えるのに決定論だけでは足りないように思えてきます。この点に関して、AWS CDK の初期からの主要な開発者である Rico 氏のコメントを紹介します。
The whole point of context was to do controlled updates. … The point of context was that you would explicitly opt in to updates to “critical” parameters like that.
コンテキストの核心は制御された更新を行うことでした。 … コンテキストの要点はそのような(新しい AMI 等)「重大な」パラメータの更新を明示的にオプトインすることでした。
決定論という用語を使わず、代わりに制御された更新 (controlled updates) という言葉でコンテキストの核心を表現しています。筆者もまた、制御された更新の方が決定論よりもコンテキストの特徴や使いどころを的確にとらえられる用語ではないかと思います。
制御された更新という用語のポイントは二つあります。一つは、コンテキストを単に決定するだけでなく、時とともに「更新 (updates)」するものとしてとらえているという点です。決定論という観点からコンテキストを見る場合、コミットというある一時点におけるコンテキストに注目しがちです。しかし例えば AMI ID は、ある一時点で決定しても、その後 AMI の機能改善や脆弱性の対応などさまざまな理由で更新していくものでしょう。決定やコミットという「点」をつなげて、更新し続ける「線」としてコンテキストを見なければ、コンテキストの全体をとらえるのに十分ではないのです。
もう一つのポイントは、コンテキストが主にアプリの外部の変化を表現するものであることが、「制御された (controlled)」という言葉に含まれている点です。初期のコンテキストは Environmental Context 、つまりアプリの管理下にない「環境」とそのアンオウンドなリソースを表すものとして開発されてきました。コンテキストの主な使いどころは、それらアプリの外部の変化を、アプリのコードから明確に分離し、コントロールしやすくすることにあります。
このことは Environmental Context だけでなく、機能フラグからも説明できます。機能フラグは、AWS CDK で生じる破壊的変更、すなわちアプリの管理下にない外部の変化を、コンテキストとして表現し管理しやすくしたものです。
コンテキストの管理機能
それでは、制御された更新をはじめ、コンテキストの管理作業全般を行うための機能にはどのようなものがあるでしょうか。
今のところ cdk コマンドは以下のような機能を提供しています。
cdk context --clear
:cdk.context.json
のコンテキストを全て削除するオプションcdk context --reset
:cdk.context.json
のコンテキストを指定して削除するオプションcdk --context
:cdk.context.json
などのファイルより優先してコンテキストを適用できるオプション (第8回)
こうした管理のための機能が提供されていることは、ハードコード等とは異なるコンテキストの特徴と言えるでしょう。
しかし以上の機能だけでは、コンテキストを管理する機能としてはまったく十分ではないでしょう。不足していると思われる機能の例を GitHub のイシューからいくつか挙げてみます。
- Issue #19614: ネストされたコンテキストを cdk コマンドのオプションで追加・更新できるようにする
- Issue #19797: コンテキストの有効期間を設定できるようにする
- Issue #23911: コンテキストを定義したファイルを cdk コマンドのオプションで指定できるようにする
- Issue #29821: コンテキストを定義できるファイルの種類を増やす(主にローカル開発用)
この他、機能フラグの管理機能 (第8回) やサードパーティーが拡張可能な Open Context Providers (第9回) も、不足しているコンテキスト管理の機能と言えるでしょう。
「環境」にひもづけてコンテキストを管理する機能もあると便利でしょう。よく dev, stg, prod のように自力でコンテキストを分類、管理している例を見かけますが、そのような機能を AWS CDK として提供するのです。Elad Ben-Israel 氏のコメントによると、AWS CDK の初期から構想はあったようですが、いまだに実現はしていません。
おわりに
今回は決定論という考え方を中心に、コンテキストについてさらに詳しく見てきました。第8回から三回にわたったコンテキストの説明は、今回でひとまず終了です。
第8回の冒頭で「コンテキストは使い始めるのは簡単ですが、使いこなそうとするとあれこれ判断に迷う機能です。」と書きました。その一因は、最後に紹介した、制御された更新というコンテキストの核心を実現する管理機能の不足にあると思います。そうした機能不足が解消されれば、それらを活用した実践的なテクニックやベストプラクティスも新たに開発され、コンテキストは今よりもずっと使いやすいものになると思います。
参考資料
本文では取り上げられませんでしたが、Web アプリの方法論として知られる The Twelve-Factor App の III. Config とコンテキストを比較して頭をひねっていました。コンテキストや CloudFormation のパラメータに関連して、「XXXX は Twelve-Factor に則っているかどうか?」というような議論がかつて GitHub で繰り広げられていたためです。