オブジェクトの広場はオージス総研グループのエンジニアによる技術発表サイトです

クラウド/Webサービス

もっとじっくり AWS CDK のコンセプト

第9回 コンテキストと「環境」
オージス総研
樋口 匡俊
2024年9月26日

前回はコンテキストの利用例として機能フラグをとりあげました。とりあげた理由は、機能フラグはシンプルでありコンテキストの基本を説明するのに適しているためです。
けれども機能フラグは、v1 が GA 化された後に追加された機能です。コンテキストはその一年以上前、AWS CDK が公開された当初から存在していました。
そこで今回は、コンテキストの初期からある使い方を中心に、コンテキストについてさらに詳しく見ていきます。そのために、まずは関係の深い「環境」の説明から始めたいと思います。

「環境」とは

「環境」 (environment) とは、一言でいうと AWS のアカウントとリージョンの組のことです。つまり 123456789012 のようなアカウント ID と ap-northeast-1 のようなリージョンコードの組のことです。

「環境」は AWS CDK の開発者向け公式ガイド『Developer Guide』が単独のページを割いて説明するほどの重要な概念のはずです。しかし実用的には、アカウント/リージョンの単なる言い換えとみなして事足りる場合がほとんどです。

単に環境と書いてしまうと、一般的な単語としての環境と区別がつきにくいため、この記事では AWS CDK の概念として明確に区別したい場合は「環境」とかぎかっこを付けることにします。

「環境」の設定方法

「環境」すなわちアカウント/リージョンは、Stack コンストラクトの env プロパティを用いてスタックごとに設定できます。設定方法は大きく以下の二つに分けられます。

1. ハードコード

一つ目は、アカウント/リージョンを固定値として書き込む方法です。

import * as core from 'aws-cdk-lib/core'

const app = new core.App()

const hardCodedStack = new core.Stack(app, 'HardCodedStack', {
  env: {
    account: '123456789012',
    region: 'ap-northeast-1'
  }
})

このようにハードコード (hard-code) したアカウント/リージョンは、クラウドアセンブリの各種ファイルに出力されます。以下は manifest.json ファイル (第6回) から抜粋したものです。

"HardCodedStack": {
  "environment": "aws://123456789012/ap-northeast-1",
  ...
}

もちろん Stack から参照したり、AWS CloudFormation テンプレートに出力したりもできます。確認のため、次のコードを追加してみましょう。

new core.CfnOutput(hardCodedStack, 'HardCodedStackEnv', {
  value: `${hardCodedStack.account}/${hardCodedStack.region}`
})

これをシンセサイズすると、テンプレートに次のような Outputs セクションが出力されます。

"Outputs": {
  "HardCodedStackEnv": {
    "Value": "123456789012/ap-northeast-1"
  }
}

このようにアカウント/リージョンをハードコードすると、その「環境」に関連付けられたクラウドアセンブリを生成できます。

2. プロファイルに基づく環境変数

もう一つは、AWS CDK 固有の環境変数を使う方法です。

const profileStack = new core.Stack(app, 'ProfileStack', {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION
  }
})

これら CDK_DEFAULT_XXXX という二つの環境変数の値は、AWS CDK が自動で設定してくれます。設定される値は、開発者があらかじめ用意しておくプロファイルに基づいて決定されます。

プロファイルとは、ここでは認証情報やリージョンの設定のことです。具体的には、AWS CLI や各言語の AWS SDK でも利用される config ファイルや credentials ファイル、AWS_DEFAULT_REGION などの環境変数のことです。

先ほどのハードコードと同様、シンセサイズすると「環境」に関連付けられたクラウドアセンブリを生成できます。ハードコードする代わりに環境変数を使っているだけで、env にアカウント/リージョンの具体値を設定している点では同じだからですね。

ハードコードとの違いは、「環境」を切り替えたい場合にプロファイルを切り替えるだけで済むことです。cdk コマンドには、プロファイルを切り替える --profile オプションがあります。コードとして固定されていない分、意図せず「環境」が切り替わるリスクは高くなりますが、コードを書き換える手間がないので利便性も高いと言えます。

これら二つの方法は、目的によって使い分けるとよいでしょう。『Developer Guide』は、「環境」が固定されるハードコードは本番環境で使い、利便性の高い環境変数は開発環境で使うことを勧めています。

environment-agnostic:「環境」を問わない

env は必須のプロパティではありません。これまでの連載のサンプルコードがそうだったように、「環境」を設定しない Stack も作れます。

const environmentAgnosticStack = new core.Stack(app, 'EnvironmentAgnosticStack')

このようなスタックは environment-agnostic なスタックと呼ばれます。プロパティを何も設定しないデフォルトの Stack は environment-agnostic なのです。

「agnostic」はコンピューターの世界ではよく「非依存」と訳されますが、この場合はどの「環境」かを問わず使えるという意味になります。環境非依存と訳すと、ちょっと目立たなくなる気がするので、あえて訳さず説明を続けます。

environment-agnostic なスタックをシンセサイズすると、どのアカウント/リージョンにもデプロイできるような形でクラウドアセンブリが生成されます。たとえばハードコードの例で見た manifest.json は以下のように変わります。

"EnvironmentAgnosticStack": {
  "environment": "aws://unknown-account/unknown-region",
  ...
}

アカウント/リージョンの具体値の代わりに埋め込まれている unknown-xxxx は、このスタックを environment-agnostic として扱ってもらうための目印になります。

Stackaccountregion はどうなっているのか、ログに出力してみましょう。

console.log(environmentAgnosticStack.account)
console.log(environmentAgnosticStack.region)

すると string トークン (第7回) が出力されます。

${Token[AWS.AccountId.0]}
${Token[AWS.Region.4]}

これらトークンが解決されるとどうなるのか、ハードコードの例と同様に Outputs セクションに出力してみましょう。

"Outputs": {
  "EnvironmentAgnosticStackEnv": {
    "Value": {
        "Fn::Join": [
          "",
          [{ "Ref": "AWS::AccountId" }, "/", { "Ref": "AWS::Region" }]
        ]
    }
  }
}

AWS::AccountIdAWS::Region は、それぞれデプロイ先のアカウント ID とリージョンコードを表す擬似パラメータ (pseudo parameter) です。組み込み関数 Ref の引数として、これら擬似パラメータの具体的な値はデプロイタイムに決定されます。このように environment-agnostic なクラウドアセンブリには、シンスタイムで具体的なアカウント/リージョンを埋め込まずに済む工夫が散りばめられています。

ここまでの説明だけですと、いつでもデフォルトの environment-agnostic なスタックを使えば良さそうな気がしてきます。わざわざ「環境」を設定して、ポータビリティを下げなくてもよいのではないでしょうか。

けれども environment-agnostic なスタックには、「環境」を設定したスタックには無い制約があります。次に説明する「環境」のコンテキストを処理できないのです。

Environmental Context

『Developer Guide』の履歴をたどっていくと、初期のコンテキストは Environmental Context という用語の下に開発が進められていたことが分かります。

時がたつにつれて Environmental Context という用語は存在感を失い、今ではソースコードのコメント等に断片的に残っている程度になりました。けれどもコンテキストに関するドキュメントのタイトルが『Environmental Context』であったこと等から、初期の AWS CDK では重要な概念だったことがうかがえます。

Environmental Context とは、文字通り「環境」の情報を表すコンテキストなのですが、以下では AZ を例にもう少し詳しく説明します。

Environmental Context と AZ

AWS の各リージョンは AZ (Availability Zone, アベイラビリティーゾーン) という複数の場所に分かれています。例えば現在の東京リージョンには、四つの AZ があると公表されています。

しかし公表されているからといって、自分のアカウントですべての AZ が使えるとは限りません。同じリージョンであっても、アカウントが異なれば使える AZ も異なる場合があるのです。

AWS の公式ドキュメントの説明を見てみましょう。

As Availability Zones grow over time, our ability to expand them can become constrained. If this happens, we might restrict you from launching an instance in a constrained Availability Zone unless you already have an instance in that Availability Zone. Eventually, we might also remove the constrained Availability Zone from the list of Availability Zones for new accounts. Therefore, your account might have a different number of available Availability Zones in a Region than another account.

時とともに AZ が成長すると、AZ を拡大する能力が制約される場合があります。その場合、あなたの (EC2) インスタンスがそこにない限り、その制約のある AZ でインスタンスを起動することを制限するかもしれません。ゆくゆくは、新しいアカウントで使える AZ のリストからその制約のある AZ を削除するかもしれません。その結果、あなたのアカウントと他のアカウントとで、あるリージョンにおける AZ の数は異なるかもしれません。

https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html

この説明は使える AZ が減るという話ですが、逆に増えることもありえます。東京リージョンに四つ目の AZ が追加されたのは 2018 年のことでした。

Environmental Context とは、この AZ のようにアカウント/リージョンごとに異なり変化しうる情報を、AWS CDK アプリで利用できるようにするコンテキストなのです。

Environmental Context を処理する仕組み:AZ の場合

次に Environmental Context を処理する仕組みについて、やはり AZ を例に見ていきましょう。初期の AWS CDK と最新の v2 とでは実装は異なりますが、おおよその仕組みは同じですので、以下では v2 をもとに説明します。

StackavailabilityZones プロパティ

まず先ほどのハードコードで「環境」を設定した Stack から、その「環境」で利用できる AZ のリストを取得してみましょう。

const azs = hardCodedStack.availabilityZones
console.log(azs)

この後すぐ説明しますが、availabilityZones プロパティ (getter) はこのようにアクセスしただけで、やや込み入った処理を引き起こします。そして最終的には、コンテキストとして渡された値をもとに AZ のリストを返します。

初回の cdk synth

ひとまずこのアプリを cdk synth コマンドでシンセサイズしてみましょう。初回のシンセサイズでは、以下のようなログが出力されます。

[ 'dummy1a', 'dummy1b', 'dummy1c' ]
[ 'ap-northeast-1a', 'ap-northeast-1c', 'ap-northeast-1d' ]

どうやらログが二回も出力されているようですね。実際に、上記のコードは二回実行されています。Environmental Context が絡んでくると、このように cdk synth コマンド一回につき複数回アプリのコードが実行されることがあるのです。

cdk.context.json ファイル

このとき cdk.context.json という新たなファイルも出力されます。ファイルの中身は、キーがアカウント/リージョンで値が AZ のリストであるコンテキストです。

{
  "availability-zones:account=123456789012:region=ap-northeast-1": [
    "ap-northeast-1a",
    "ap-northeast-1c",
    "ap-northeast-1d"
  ]
}

前回 コンテキストを設定するもっとも基本的なファイルとして cdk.json を紹介しましたが、cdk.json はコンテキストに限らない cdk コマンド全般のファイルでした。いっぽう cdk.context.json は、このようにコンテキスト専用のファイルです。

cdk コマンドとアプリの協調

上の cdk synth で何が起こっているのかというと、cdk コマンドとアプリがそれぞれ役割を分担し、協調して AZ をコンテキストとして処理しています。

cdk コマンドとアプリの協調

前回説明したように、cdk コマンドはアプリを実行するときにコンテキストを渡します(図の①)。

アプリは渡されたコンテキストから AZ を取得しようと試みますが、もし取得できなければその結果を記録します(図の②)。先ほどの一つ目のログ ('dummy1a' 等) のときは、AZ のコンテキストが欠けていることがアプリ(のスタック)に記録されています。

その記録をもとに cdk コマンドは足りないコンテキストを補おうとします。AZ であれば、AWS SDK を用いてその「環境」から AZ の情報を取得し(図の③)、コンテキストとして cdk.context.json ファイルに保存します(図の④)。

そうして取得した新たなコンテキストを加えて、再びアプリを実行します(図の⑤)。先ほどの二つ目のログ ('ap-northeast-1a' 等) は、cdk.context.json に保存された AZ のコンテキストが無事にアプリに渡され、availabilityZones プロパティから取得できたことを示しているわけです。

コンテキストプロバイダー

こうした cdk コマンドとアプリが協調して行う処理の中核は、コンテキストプロバイダー (context provider) によって実現されています。

コンテキストプロバイダーは、コンテキストを処理する仕組みであり、クラス群です。コンテキストプロバイダーは、コンテキストの種類の増減など変化に柔軟に対応できるような設計・実装になっています。通常 AWS CDK アプリの開発者がコンテキストプロバイダーを意識する必要はありませんが、興味のある方はソースコードを検索して ContextProvider クラスや AZContextProviderPlugin クラスなどを調べてみてください。

二回目の cdk synth とキャッシュ

このままもう一度 cdk synth を実行してみましょう。すると今度はダミーの AZ はログに出力されません。

[ 'ap-northeast-1a', 'ap-northeast-1c', 'ap-northeast-1d' ]

つまり二回目の cdk synth では、コードは一回だけ実行されたということです。原因は、すでに AZ のコンテキストが cdk.context.json ファイルに出力されているからですね。一度取りに行った情報は cdk.context.json に保存して、再び取りに行かなくて済むようにしているわけです。このような cdk.context.json の使い方は、よくキャッシュ (cache) と表現されます。

Environmental Context と VPC

AZ のように「環境」そのものの情報ではなく、「環境」に作成されたリソースの情報も Environmental Context として表される場合があります。その代表が VPC です。

第5回で少しふれた L2 コンストラクト VpcfromLookup メソッドを用いると、「環境」から VPC の情報を取得し、コンテキストとして cdk.context.json ファイルに保存できます。その仕組みは上で説明した AZ と同様であり、処理の中核はコンテキストプロバイダーによって実現されています。

同じ仕組みを用いて「環境」の情報をコンテキストとして処理するという点では、たしかに AZ と VPC を表すコンテキストは Environmental Context として一括りにしても良さそうです。けれどもコンテキストを使う目的が AZ と VPC では異なります。

先ほど見た availabilityZones プロパティが返すのは、単なる string 型の文字列のリストでした。AZ を表すコンテキストは、特定のアカウント/リージョンに関するデータをデータのまま扱うものと言えます。

いっぽう VpcfromLookup メソッドは、コンテキストを用いて IVpc インターフェースを持つオブジェクトを生成します。

import * as ec2 from 'aws-cdk-lib/aws-ec2'

const vpc: ec2.IVpc = ec2.Vpc.fromLookup(stack, 'Vpc', {
  vpcId: 'vpc-12345678'
})

この例の const vpc: ec2.IVpc は、第5回で説明したアンオウンドなコンストラクトです。つまり VPC を表すコンテキストは、「環境」のデータそのものというよりも、そのデータから参照用のコンストラクトを生成するためにあるのです。

このことは VPC 用のコンテキストプロバイダーが実装されたときの GitHub のイシューにも表れています。イシューの主題は、AWS CDK アプリから他のアプリで管理されているリソースをどのように取り込むかということであり、そのリソースとして主に検討されていたのが VPC だったのです。

Open Context Providers

VPC を参照して利用する場合、たいていは VPC だけでなく関連するサブネットなど多種多様なリソースも一緒に参照したいものです。しかし第5回で紹介した fromAttribute メソッドを用いて、リソースの属性値を一つ一つ調べて引数に設定するのは大変です。それよりは、コンテキストプロバイダーの仕組みを活用して、必要な情報は自動的に取得するほうが利便性は高いわけです。

AZ のように「環境」ごとの違いを管理するためというより、リソースを簡単に参照するためにコンテキストを使ってよいのであれば、VPC 以外のリソースにも利用範囲を広げたくなります。実際、VPC 以外にも fromLookup メソッドがほしいというような要望はちょくちょく見かけます ()。けれども VPC と同様にコンテキストをもとに参照できるリソースというと HostedZone など数えるほどしかありません。

そうした要望を解決できそうな案として、かつてサードパーティーのコンテキストプロバイダーを開発するための AWS CDK RFC『Open Context Providers』が提案されていました。この RFC にはときどき実現を望むコメントがついていましたが、残念ながら昨年末に突然クローズされてしまいました。

environment-agnostic の制約

以上の説明をふまえて、environment-agnostic の制約とはどのようなものか具体的に見ていきましょう。

environment-agnostic なスタックでは、具体的なアカウント/リージョンがデプロイタイムに決まるので、Environmental Context として AZ の情報を表すことはできません。

先ほどの例のスタックで、AZ をログに出力してみましょう。

const azs = environmentAgnosticStack.availabilityZones
console.log(azs)

結果は以下のようになります。

[ '${Token[TOKEN.11]}', '${Token[TOKEN.13]}' ]

トークンが二つ出力されました。利用できる AZ は具体的には決まっていないけれども、数は二つということですね。

二つである理由は、通常どのアカウント/リージョンでも AZ は最低二つはあるためです。「環境」を問わない代わりに、本来なら三つ、四つと AZ が使える場合でも、二つまでに制限しているわけです。

次に AZ を CloudFormation テンプレートの Outputs セクションに出力してみましょう。

new core.CfnOutput(environmentAgnosticStack, 'AZ1', {
  value: `${environmentAgnosticStack.availabilityZones[0]}`
})
new core.CfnOutput(environmentAgnosticStack, 'AZ2', {
  value: `${environmentAgnosticStack.availabilityZones[1]}`
})

シンセサイズした結果は以下のようになります。

"Outputs": {
  "AZ1": {
    "Value": { "Fn::Select": [0, { "Fn::GetAZs": "" }] }
  },
  "AZ2": {
    "Value": { "Fn::Select": [1, { "Fn::GetAZs": "" }] }
  }
},

トークンが解決されて、AZ を取得する組み込み関数 Fn::SelectFn::GetAZs になりました。このように environment-agnostic なクラウドアセンブリは、「環境」を設定したクラウドアセンブリとはかなり異なります。

AZ は数が限られるとはいえ対応できていますが、VpcfromLookup メソッドは呼び出すと以下のようなエラーを出力します。

Error: Cannot retrieve value from context provider vpc-provider
since account/region are not specified at the stack level.
Configure "env" with an account and region when you define your stack.
See https://docs.aws.amazon.com/cdk/latest/guide/environments.html for more details.

VPC の ID や名前だけ指定しても、どこの「環境」にその VPC を探しに行けばよいか分からないからですね。

environment-agnostic はセンシブルデフォルトか?

Stack はデフォルトで environment-agnostic になるわけですが、このような制約のあるものがよく練られた「いい感じ」のセンシブルデフォルト (第3回) と言えるのでしょうか。

GitHub の記録をたどると、environment-agnostic は v0.36.0 でおおよそ現在のような形にまとめられ、Stack のデフォルトになったことが分かります。それ以前のデフォルトは、プロファイルか、今はもう廃止されたデフォルトのアカウント/リージョンを表すコンテキストに基づいて自動的に決まっていました。CDK_DEFAULT_XXXX のようなプロファイルに基づく環境変数をわざわざ書かなくてもよかったのですね。

AWS CDK v1 の一般提供 (GA) が開始されたのは、environment-agnostic が導入されてから約一か月後のことでした。environment-agnostic は v1 の直前に追加され、デフォルトを大きく変えてしまった機能なのです。1

しかしそのわずか二カ月後、デフォルトを変えようという提案が Elad Ben-Israel 氏から提出されました。それによると environment-agnostic はデフォルトでなく、開発者が意識的に選択して使うものに変えようとしていたようです。

目的はいわゆる開発者体験の改善でした。コンストラクトの中には「環境」を設定しないと利用できないものがあります。そのようなコンストラクトを使い始めるときに、environment-agnostic のためにエラーが発生し、いきなりつまずいてしまうというのは、たしかにあまり良い体験ではありません。

しかしながら、結局この提案は後日検討するとしてクローズされました。environment-agnostic は今でもデフォルトのままです。

environment-agnostic なスタックはたしかに便利な場合もあり、筆者も使うことがあります。けれども AWS CDK の開発者も認めるほどに、デフォルトとしてはあまりふさわしくない、制約のある特殊なスタックです。筆者としては AWS CDK アプリは原則として「環境」を設定することにして、environment-agnostic にしたいと思っても、まずはプロファイルに基づく環境変数で代用できないか検討するのがよいと考えています。

おわりに

今回は、関係の深い「環境」とともに、コンテキストについて詳しく見てきました。cdk.context.json やコンテキストプロバイダーなど、コンテキストのやや込み入った仕組みについても説明しました。その背景には、AWS CDK 全体に関わるような概念、考え方があるのですが、それについては次回以降に説明できればと思います。


参考資料

本文でも少しふれた『Developer Guide』の古い履歴がとても参考になりました。今でこそ『Developer Guide』は独立した GitHub リポジトリで管理されていますが、もともとは AWS CDK 本体と同じリポジトリに置かれていました。独立直前のコミットは https://github.com/aws/aws-cdk/tree/4c4b01536197c1997475be9818d3632694418edd/docs/src で、そこからさらに古い履歴をたどれます。


  1. environment-agnostic とともに、今回紹介した StackavailabilityZones プロパティも追加されました。これも当時のレビューで、Stack が持つべきものなのか疑問を投げかけられていましたが、「今のところこのままにしておく (but we decided that for now we will leave it under stack)」として現在まで続いているものです。