AWSのCDKをデプロイしたら、envが見当たらないと怒られてちょっと迷子した話。
実際に出たエラーはこちら。
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.
雑にまとめるとStackProps
にEnvironment型のenv
が入っててそれを参照するから、ちゃんと渡してよねという話。
実装
スタック定義はこんな感じ。
export interface Props extends StackProps {
readonly vpcId: string;
}
export class HogeStack extends Stack {
constructor(scope: Construct, id: string, props: Props) {
super(scope, id, props);
// 問題はここ 既存のVPCを名前から参照して、これ以降のリソース生成で利用したい。
const vpc = Vpc.fromLookup(this, 'Vpc', {vpcId: props.vpcId});
...
}
}
それをこんな感じでnewする。
const stack = new HogeStack(app, `${env}-hoge`, {
vpcId: 'hoge-vpc'
}
エラーと原因
この段階でビルドはできるので cdk diff
(もしくはdeploy)とすると、冒頭のようなエラーが返ってくる。
エラーの内容をもう少し載せるとこんな感じ。
$ cdk diff HogeStack --profile nice-role
/XXXXXX/cdk/cdk-project/node_modules/aws-cdk-lib/core/lib/context-provider.js:2
This usually happens when one or more of the provider props have unresolved tokens`);const propStrings=propsToArray(props);return{key:`${options.provider}:${propStrings.join(":")}`,props}}static getValue(scope,options){try{jsiiDeprecationWarnings.aws_cdk_lib_GetContextValueOptions(options)}catch(error){throw process.env.JSII_DEBUG!=="1"&&error.name==="DeprecationError"&&Error.captureStackTrace(error,this.getValue),error}const stack=stack_1.Stack.of(scope);if(token_1.Token.isUnresolved(stack.account)||token_1.Token.isUnresolved(stack.region))throw new Error(`Cannot retrieve value from context provider ${options.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.`);const{key,props}=this.getKey(scope,options),value=constructs_1.Node.of(scope).tryGetContext(key),providerError=extractProviderError(value);return value===void 0||providerError!==void 0?(stack.reportMissingContextKey({key,provider:options.provider,props}),providerError!==void 0&&annotations_1.Annotations.of(scope).addError(providerError),{value:options.dummyValue}):{value}}}exports.ContextProvider=ContextProvider,_a=JSII_RTTI_SYMBOL_1,ContextProvider[_a]={fqn:"aws-cdk-lib.ContextProvider",version:"2.44.0"};function extractProviderError(value){if(typeof value=="object"&&value!==null)return value[cxapi.PROVIDER_ERROR_KEY]}function colonQuote(xs){return xs.replace("$","$$").replace(":","$:")}function propsToArray(props,keyPrefix=""){const ret=[];for(const key of Object.keys(props))if(props[key]!==void 0)switch(typeof props[key]){case"object":{ret.push(...propsToArray(props[key],`${keyPrefix}${key}.`));break}case"string":{ret.push(`${keyPrefix}${key}=${colonQuote(props[key])}`);break}default:{ret.push(`${keyPrefix}${key}=${JSON.stringify(props[key])}`);break}}return ret.sort(),ret}
^
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.
at Function.getValue (/XXXXXX/cdk/cdk-project/node_modules/aws-cdk-lib/core/lib/context-provider.js:2:554)
at Function.fromLookup (/XXXXXX/cdk/cdk-project/node_modules/aws-cdk-lib/aws-ec2/lib/vpc.js:1:12496)
at new HogeStack (/XXXXXX/cdk/cdk-project/lib/stack.ts:82:25)
at Object.<anonymous> (/XXXXXX/cdk/cdk-project/bin/cdk-project.ts:49:21)
...
なにを怒られているのかというと…
Stackには基本的に Enviroment 型のenv
をコンストラクタ引数に持っていて、実装によってはこれを利用する場合があって、「必要なのに指定がない」という感じに怒られている。
上記の実装例で言うと、「問題はここ」と書いたVpc.fromLookup()
がそれに該当する。
そこでenv
を利用したいのに、cdk-project.ts
でStackPropsを継承しているPropsを引数に取るHogeStackのコンストラクタに対して、env
に何も渡していないため、「envがundefなんですが」と怒られている。
StackPropsの実装では下記の通りOptionalになっているので、自分で書いたコード上で明示的に使っていないと上記のエラーに出くわしやすい。
readonly env?: Environment;
正しくは
const sampleEnv = {
account: 'XXXXXXXXXXXX'
region: 'ap-northeast-1',
}
const stack = new HogeStack(app, `${env}-hoge`, {
env: sampleEnv, // ここが必要
vpcId: 'hoge-vpc'
}
スタックの定義は特に修正しなくともOK。
渡し忘れが不安な場合はPropsでenv
をrequiredにしてやっても良いかも。
ついつい「いるものだけ渡したい」という気持ちで省略したくなるが、コーディングルールとして常に必須という運用でもいいかも。
余談
今回のCDKプロジェクトでは、1つのコードから開発環境(dev)と本番環境(prod)の両方のAWSリソースを作成したかった。
そのためCDKのデプロイ時に -c env=dev
といった感じで、パラメータを指定すればデプロイ環境も変わるように実装している。
そのせいもあり、「あれ、パラメータで渡した値があってればアカウントとリージョンが指定されるはずなのに…」と余計に混乱したのでした。
デプロイ時のパラメータenvの実装
export const Env = {
DEV: 'dev',
PROD: 'prod'
} as const;
export type Env = typeof Env[keyof typeof Env];
{
〜中略〜
"context": {
〜中略〜
"env": "dev",
"dev": {
"account": "ZZZZZZZZZZZZ",
"region": "ap-northeast-1"
},
"prod": {
"account": "XXXXXXXXXXXX",
"region": "ap-northeast-1"
}
}
}
// コマンドラインから指定したパラメータから、env.tsで自炊したEnvに変換
const env: Env = app.node.tryGetContext('env');
// 自炊してるEnvから、cdkのEnvironmentに変換する
const awsEnv: cdk.Environment = app.node.tryGetContext(env);
const stack = new HogeStack(app, `${env}-hoge`, {
env: awsEnv, // Environment型のもの
stackEnv: env, // 独自のEnv型のもの
...
}
利用側の例
export interface Props extends StackProps {
readonly env: Environment;
readonly stackEnv: Env;
...
}
export class HogeStack extends Stack {
constructor(scope: Construct, id: string, props: Props) {
super(scope, id, props);
...
// Env型のほうはこんな感じで指定したパラメータに応じて環境の設定差分を作ったりしたい。特に問題はない。
const REMOVAL_POLICY = props.stackEnv == Env.PROD ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY;
// 問題はここ
const vpc = Vpc.fromLookup(this, 'Vpc', {vpcId: props.vpcId});
}
}
ここまでの実装で、確かに -c env=dev
パラメータによってデプロイするアカウント、リージョンを指定できるようになる。
しかしこのときenv
には何も渡していないので、cdkコマンド実行時に前述のエラーに陥る。
(そして環境指定しているのになぜ?!となる。)
おわりのつぶやき
今回遭遇したエラーについては、ちゃんとenv
は渡しましょうという話でした。
CDKだとIDEのサポートなどに従いつつ書いていけば概ねおかしな実装にはならないのでありがたいですが、
時たまこういうdiffとったりdeployするまで気づきにくいポイントがあったりして困惑します。
とはいえまずdiffを取れるし、deployで失敗してもいい感じにロールバックしてくれるし、やっぱりありがたい気持ちは変わらない。