[AWS CDK] StackProps の Environment を指定すると、AWS アカウント ID がハードコードされることに留意しよう

2024.06.14

こんにちは、製造ビジネステクノロジー部の若槻です。

AWS CDK ではコンテキストメソッドを使用して AWS 環境からコンテキスト情報を取得することができます。

コンテキストメソッドには次のようなものがあり、リソース名や ID で AWS 環境からリソースを検索(Lookup)するメソッドとなります。

  • stack.availabilityZones
  • HostedZone.fromLookup
  • stack.availabilityZones
  • StringParameter.valueFromLookup
  • Vpc.fromLookup
  • LookupMachineImage

さてこのコンテキストメソッドを使用する場合には、StackProps のEnvironmentで「AWS アカウント ID」と「リージョン」を指定する必要があります。

下記は Vpc.fromLookup により VPC ID を指定して VPC を取得する実装例です。

lib/cdk-sample-stack.ts

import { aws_ec2, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSampleStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    const vpc = aws_ec2.Vpc.fromLookup(this, 'VPC', { vpcId: 'vpc-1e0d0779' });

    new aws_ec2.SecurityGroup(this, 'SecurityGroup', {
      vpc,
    });
  }
}

StackProps への Environment の指定は下記のように行います。

bin/cdk_sample_app.ts

import { App } from 'aws-cdk-lib';
import { CdkSampleStack } from '../lib/cdk-sample-stack';

const app = new App();

const awsAccountId = app.node.tryGetContext('awsAccountId');

export const cdkSampleStackprops = {
  env: {
    account: awsAccountId,
    region: 'ap-northeast-1',
  },
};

new CdkSampleStack(app, 'CdkSampleStack', cdkSampleStackprops);

もしコンテキストメソッド使用時に AWS アカウント ID とリージョンのいずれかが Environment で未指定の場合は、スタックの合成(CDK Synth)時にエラーが発生します。

エラー例

$ npx cdk synth 
/Users/wakatsuki.ryuta/projects/cm-rwakatsuki/cdk_sample_app/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}}constructor(){}}exports.ContextProvider=ContextProvider,_a=JSII_RTTI_SYMBOL_1,ContextProvider[_a]={fqn:"aws-cdk-lib.ContextProvider",version:"2.145.0"};function extractProviderError(value){if(typeof value=="object"&&value!==null)return value[cxapi().PROVIDER_ERROR_KEY]}function colonQuote(xs){return xs.replace(/\$/g,"$$").replace(/:/g,"$:")}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.

Environmnt を指定している場合は AWS アカウント ID がハードコードされる

さて、上述のコンテキストメソッドを使用しているなどの理由で Environmnt を指定している場合は、いくつかのファイルにAWS アカウント ID がハードコードされるため、それらファイルを Git などで管理する際には留意が必要です。

下記にハードコードがされ得るファイルをいくつか示します。

cdk.context.json

コンテキストメソッドを使用すると、スタック合成の結果として cdk.context.json というファイルが生成されます。下記は Vpc.fromLookup で取得した VPC の情報が記載された cdk.context.json の一部です。

cdk.context.json

{
  "vpc-provider:account=012345678901:filter.isDefault=true:region=ap-northeast-1:returnAsymmetricSubnets=true": {
    "vpcId": "vpc-1e0d0779",
    "vpcCidrBlock": "172.31.0.0/16",
    "ownerAccountId": "012345678901",
    "availabilityZones": [],
    "subnetGroups": [
      {
        "name": "Public",
        "type": "Public",
        "subnets": [
          {
            "subnetId": "subnet-2866b260",
            "cidr": "172.31.32.0/20",
            "availabilityZone": "ap-northeast-1a",
            "routeTableId": "rtb-b95b38df"
          },
          {
            "subnetId": "subnet-f69690ad",
            "cidr": "172.31.0.0/20",
            "availabilityZone": "ap-northeast-1c",
            "routeTableId": "rtb-b95b38df"
          },
          {
            "subnetId": "subnet-8374aba8",
            "cidr": "172.31.16.0/20",
            "availabilityZone": "ap-northeast-1d",
            "routeTableId": "rtb-b95b38df"
          }
        ]
      }
    ]
  },
  "vpc-provider:account=012345678901:filter.isDefault=true:region=us-east-1:returnAsymmetricSubnets=true": {
    // 略
  },
  "vpc-provider:account=012345678901:filter.vpc-id=vpc-1e0d0779:region=ap-northeast-1:returnAsymmetricSubnets=true": {
    // 略
  }
}

cdk.context.json は、VPC が配置されているアベイラビリティーゾーンなどの物理情報をキャッシュして固定化する役割があり、ドキュメントでは Git 管理の対象とすることが推奨されています。そしてこのファイルには情報として AWS アカウント ID が含まれるため、結果として AWS アカウント ID がハードコードされることになります。

CDK スナップショットファイル

AWS CDK のスナップショットテストを利用すると、CDK の合成結果に意図しない差分が発生しないかテストを行うことができます。

そして次の条件が合わさった時にこのスナップショットテストのファイルにも AWS アカウント ID がハードコードされる場合があります。

  • StackProps での Environment 指定時
  • アカウント ID をスタックから取得している場合

例として次のようなスタックの実装で試してみます。CfnOutput でスタックのアカウント ID を出力しています。

lib/cdk-sample-stack.ts

import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSampleStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    const awsAccountId1 = this.account;
    const awsAccountId2 = Stack.of(this).account;

    new CfnOutput(this, 'awsAccountId1', {
      value: awsAccountId1,
    });

    new CfnOutput(this, 'awsAccountId2', {
      value: awsAccountId2,
    });
  }
}

スナップショットテストの実装です。

test/cdk_sample_app.test.ts

import { test, expect } from 'vitest';
import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';

import { CdkSampleStack } from '../lib/cdk-sample-stack';

test('CDK Sample Stack', () => {
  const app = new cdk.App();

  const cdkSampleStackprops = {
    env: {
      account: process.env.AWS_ACCOUNT_ID,
      region: 'ap-northeast-1',
    },
  };

  const stack = new CdkSampleStack(app, 'CdkSampleStack', cdkSampleStackprops);

  const template = Template.fromStack(stack);

  expect(template).toMatchSnapshot();
});

Vitest でスナップショットを生成します。

npx vitest ./test --run -u

生成されたスナップショットファイルを確認すると、AWS アカウント ID がハードコードされていることが確認できます。

test/__snapshots__/cdk_sample_app.test.ts.snap

// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`CDK Sample Stack 1`] = `
{
  "Outputs": {
    "awsAccountId1": {
      "Value": "012345678901",
    },
    "awsAccountId2": {
      "Value": "012345678901",
    },
  },
  "Parameters": {
    "BootstrapVersion": {
      "Default": "/cdk-bootstrap/hnb659fds/version",
      "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
      "Type": "AWS::SSM::Parameter::Value<String>",
    },
  },
  "Rules": {
    "CheckBootstrapVersion": {
      "Assertions": [
        {
          "Assert": {
            "Fn::Not": [
              {
                "Fn::Contains": [
                  [
                    "1",
                    "2",
                    "3",
                    "4",
                    "5",
                  ],
                  {
                    "Ref": "BootstrapVersion",
                  },
                ],
              },
            ],
          },
          "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.",
        },
      ],
    },
  },
}
`;

ちなみに Environment を指定していない場合は、スナップショットファイルには AWS アカウント ID は含まれません。{ "Ref": "AWS::AccountId" } というプレースホルダーが出力されます。

Environment を指定していない場合の例

Environment を指定していない場合のスナップショットファイルの例です。

test/cdk_sample_app.test.ts

import { test, expect } from 'vitest';
import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';

import { CdkSampleStack } from '../lib/cdk-sample-stack';

test('CDK Sample Stack', () => {
  const app = new cdk.App();

  const cdkSampleStackprops = {
    // env: {
    //   account: process.env.AWS_ACCOUNT_ID,
    //   region: 'ap-northeast-1',
    // },
  };

  const stack = new CdkSampleStack(app, 'CdkSampleStack', cdkSampleStackprops);

  const template = Template.fromStack(stack);

  expect(template).toMatchSnapshot();
});

生成されたスナップショットファイルに AWS アカウント ID は含まれません。

test/__snapshots__/cdk_sample_app.test.ts.snap

// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`CDK Sample Stack 1`] = `
{
  "Outputs": {
    "awsAccountId1": {
      "Value": {
        "Ref": "AWS::AccountId",
      },
    },
    "awsAccountId2": {
      "Value": {
        "Ref": "AWS::AccountId",
      },
    },
  },
  "Parameters": {
    "BootstrapVersion": {
      "Default": "/cdk-bootstrap/hnb659fds/version",
      "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
      "Type": "AWS::SSM::Parameter::Value<String>",
    },
  },
  "Rules": {
    "CheckBootstrapVersion": {
      "Assertions": [
        {
          "Assert": {
            "Fn::Not": [
              {
                "Fn::Contains": [
                  [
                    "1",
                    "2",
                    "3",
                    "4",
                    "5",
                  ],
                  {
                    "Ref": "BootstrapVersion",
                  },
                ],
              },
            ],
          },
          "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.",
        },
      ],
    },
  },
}
`;

おわりに

AWS CDK の StackProps の Environment を指定すると、AWS アカウント ID がハードコードされることに留意しようという内容のご紹介でした。

通常の開発では AWS アカウント ID 程度であればハードコードしても問題ない場合が多いかもしれませんが、ソースコードをテンプレートやサンプルとして公開したい場合などには留意した方が良さそうです。

参考

以上