CDKコマンド実行時にアカウントとリージョンがないと言われた話

AWSのCDKをデプロイしたら、envが見当たらないと怒られてちょっと迷子した話。  

実際に出たエラーはこちら。

:::note alert
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`が入っててそれを参照するから、ちゃんと渡してよねという話。

## 実装
スタック定義はこんな感じ。
```typescript:lib/stack.ts
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する。
```typescript:bin/cdk-project.ts
const stack = new HogeStack(app, `${env}-hoge`, {
    vpcId: 'hoge-vpc'
}
```

## エラーと原因
この段階でビルドはできるので `cdk diff` （もしくはdeploy）とすると、冒頭のようなエラーが返ってくる。
エラーの内容をもう少し載せるとこんな感じ。

```shell
$ 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](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Environment.html) 型の`env`をコンストラクタ引数に持っていて、実装によってはこれを利用する場合があって、「必要なのに指定がない」という感じに怒られている。

上記の実装例で言うと、「問題はここ」と書いた`Vpc.fromLookup()`がそれに該当する。
そこで`env`を利用したいのに、`cdk-project.ts`でStackPropsを継承しているPropsを引数に取るHogeStackのコンストラクタに対して、`env`に何も渡していないため、「envがundefなんですが」と怒られている。

StackPropsの実装では下記の通りOptionalになっているので、自分で書いたコード上で明示的に使っていないと上記のエラーに出くわしやすい。
```typescript
 readonly env?: Environment;
 ```

## 正しくは
```typescript:bin/cdk-project.ts
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の実装
```typescript:lib/env/env.ts
export const Env = {
    DEV: 'dev',
    PROD: 'prod'
} as const;

export type Env = typeof Env[keyof typeof Env];
```

```json:cdk.json
{
  〜中略〜
  "context": {
    〜中略〜
    "env": "dev",
    "dev": {
      "account": "ZZZZZZZZZZZZ",
      "region": "ap-northeast-1"
    },
    "prod": {
      "account": "XXXXXXXXXXXX",
      "region": "ap-northeast-1"
    }
  }
}
```
```typescript:bin/cdk-project.ts
// コマンドラインから指定したパラメータから、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型のもの
    ...
}
```

### 利用側の例
```typescript:lib/stack.ts
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で失敗してもいい感じにロールバックしてくれるし、やっぱりありがたい気持ちは変わらない。