[AWS CDK] クロススタック参照で、参照先から参照を削除する場合は、参照元で exportValue を使おう

2023.06.03

こんにちは、CX事業本部 Delivery部の若槻です。

今回は、AWS CDK のクロススタック参照で、「参照先での参照(エクスポートの使用)」を削除する方法を確認してみました。

先にまとめ

  • 参照先での参照を削除する場合は、参照元で exportValue を使って、エクスポートの存在を保証する必要がある
  • 参照先での参照と、参照元でのエクスポートを、1度のデプロイで削除することは出来ない

試してみた

次のような参照元スタックと参照先スタックを作成し、試してみます。

  • CDK App

bin/cdk_sample_app.ts

import * as cdk from 'aws-cdk-lib';
import { CdkSampleStack } from '../lib/cdk-sample-app';
import { CdkSampleStack2 } from '../lib/cdk-sample-app-2';

const app = new cdk.App();

// 参照元スタック作成
const cdkSampleStack = new CdkSampleStack(app, 'CdkSampleStack', {});

// 参照先スタック作成
new CdkSampleStack2(app, 'CdkSampleStack2', {
  sampleParameter: cdkSampleStack._sampleParameter,
});
  • 参照元スタック CdkSampleStack

lib/cdk-sample-app.ts

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

// 参照元スタック
export class CdkSampleStack extends Stack {
  public readonly _sampleParameter: aws_ssm.StringParameter;

  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    const sampleParameter = new aws_ssm.StringParameter(
      this,
      'SampleParameter',
      {
        parameterName: 'SampleParameter',
        stringValue: 'SampleValue',
      }
    );

    this._sampleParameter = sampleParameter;
  }
}
  • 参照先スタック CdkSampleStack2

lib/cdk-sample-app-2.ts

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

interface CdkSampleStack2Props extends StackProps {
  sampleParameter: aws_ssm.StringParameter;
}

// 参照先スタック
export class CdkSampleStack2 extends Stack {
  constructor(scope: Construct, id: string, props: CdkSampleStack2Props) {
    super(scope, id, props);

    const sampleFunction = new aws_lambda_nodejs.NodejsFunction(
      this,
      'SampleFunction',
      {
        entry: 'src/sample-function.ts',
        environment: {
          SAMPLE_PARAMETER_NAME: props.sampleParameter.parameterName,
        },
      }
    );
    props.sampleParameter.grantRead(sampleFunction);
  }
}

上記の CDK App をデプロイします。

デプロイ後には Export が作成されていることが確認できます。

$ aws cloudformation list-exports \
  --query "Exports[?contains(ExportingStackId,'CdkSampleStack')]"

[
    {
        "ExportingStackId": "arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/CdkSampleStack/e68b6900-f01c-11ed-8538-06bbc29e5367",
        "Name": "CdkSampleStack:ExportsOutputRefSampleParameterEC52094C22ED334E",
        "Value": "SampleParameter"
    }
]

1. exportValue を使わない場合、参照先でのエクスポートの使用を削除できない

参照先スタックで、インポートして使用している参照を削除します。

lib/cdk-sample-app-2.ts

// 参照先スタック
export class CdkSampleStack2 extends Stack {
  constructor(scope: Construct, id: string, props: CdkSampleStack2Props) {
    super(scope, id, props);

    // const sampleFunction = new aws_lambda_nodejs.NodejsFunction(
    //   this,
    //   'SampleFunction',
    //   {
    //     entry: 'src/sample-function.ts',
    //     environment: {
    //       SAMPLE_PARAMETER_NAME: props.sampleParameter.parameterName,
    //     },
    //   }
    // );
    // props.sampleParameter.grantRead(sampleFunction);
  }
}

cdk deploy '*' コマンドを実行してデプロイをしようとするとエラーが発生し失敗しました。ここでデプロイが先に走っているのは参照元スタック CdkSampleStack です。

$ cdk deploy '*'

  Synthesis time: 3.06s

CdkSampleStack:  start: Building e729aa46a22d433584a5885847489098cb0505e19d9ddee61aa310b08456b63c:current_account-current_region
CdkSampleStack:  success: Built e729aa46a22d433584a5885847489098cb0505e19d9ddee61aa310b08456b63c:current_account-current_region
CdkSampleStack2:  start: Building 2823a2fbf0949d41981e8bdbe73ebddaa74da24257a897be46ba45c3646a911b:current_account-current_region
CdkSampleStack2:  success: Built 2823a2fbf0949d41981e8bdbe73ebddaa74da24257a897be46ba45c3646a911b:current_account-current_region
CdkSampleStack:  start: Publishing e729aa46a22d433584a5885847489098cb0505e19d9ddee61aa310b08456b63c:current_account-current_region
CdkSampleStack:  success: Published e729aa46a22d433584a5885847489098cb0505e19d9ddee61aa310b08456b63c:current_account-current_region
CdkSampleStack
CdkSampleStack2:  start: Publishing 2823a2fbf0949d41981e8bdbe73ebddaa74da24257a897be46ba45c3646a911b:current_account-current_region
CdkSampleStack: deploying... [1/2]
CdkSampleStack2:  success: Published 2823a2fbf0949d41981e8bdbe73ebddaa74da24257a897be46ba45c3646a911b:current_account-current_region
CdkSampleStack: creating CloudFormation changeset...

   CdkSampleStack failed: Error: The stack named CdkSampleStack failed to deploy: UPDATE_ROLLBACK_COMPLETE
    at FullCloudFormationDeployment.monitorDeployment (/Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:397:10236)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async Object.deployStack2 [as deployStack] (/Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:400:149977)
    at async /Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:400:135508

  Deployment failed: Error: The stack named CdkSampleStack failed to deploy: UPDATE_ROLLBACK_COMPLETE
    at FullCloudFormationDeployment.monitorDeployment (/Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:397:10236)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async Object.deployStack2 [as deployStack] (/Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:400:149977)
    at async /Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:400:135508

The stack named CdkSampleStack failed to deploy: UPDATE_ROLLBACK_COMPLETE

CloudFormation のスタックのイベントを確認してみると、参照元スタックで Export <参照先スタック名>:<エクスポート名> cannot be deleted as it is in use by <参照元スタック名> というエラーが発生しています。

Export CdkSampleStack:ExportsOutputRefSampleParameterEC52094C22ED334E cannot be deleted as it is in use by CdkSampleStack2

ここで CDK Diff コマンドで、デプロイしようとしているスタックと、デプロイ済みのスタックの差分を確認してみると、参照先のリソースだけでなく、参照元スタックのエクスポートまで削除されていることがわかります。

$ cdk diff                                                                                       
Stack CdkSampleStack
Outputs
[-] Output ExportsOutputRefSampleParameterEC52094C22ED334E: {"Value":{"Ref":"SampleParameterEC52094C"},"Export":{"Name":"CdkSampleStack:ExportsOutputRefSampleParameterEC52094C22ED334E"}}

Stack CdkSampleStack2
IAM Statement Changes
┌───┬─────────────────────────────────────────────────────────────────────────────────────────────────────────┬────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────┬───────────────────────────────────┬───────────┐
│   │ Resource                                                                                                │ Effect │ Action                                                                                                   │ Principal                         │ Condition │
├───┼─────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────┼───────────┤
│ - │ arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/{"Fn::ImportValue":"CdkSampleStack │ Allow  │ ssm:DescribeParameters                                                                                   │ AWS:${SampleFunction/ServiceRole} │           │
│   │ :ExportsOutputRefSampleParameterEC52094C22ED334E"}                                                      │        │ ssm:GetParameter                                                                                         │                                   │           │
│   │                                                                                                         │        │ ssm:GetParameterHistory                                                                                  │                                   │           │
│   │                                                                                                         │        │ ssm:GetParameters                                                                                        │                                   │           │
└───┴─────────────────────────────────────────────────────────────────────────────────────────────────────────┴────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────┴───────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Resources
[-] AWS::IAM::Role SampleFunction/ServiceRole SampleFunctionServiceRoleE420739A destroy
[-] AWS::IAM::Policy SampleFunction/ServiceRole/DefaultPolicy SampleFunctionServiceRoleDefaultPolicy49A1212C destroy
[-] AWS::Lambda::Function SampleFunction SampleFunction7DB1D36A destroy

2. exportValue を使って、参照先でのエクスポートの使用を削除できる

参照先での参照を削除したい場合は、参照元スタックで exportValue を使う必要があります。

参照先で使用する情報(ここではパラメーター名)をエクスポートする記述を追加します。

lib/cdk-sample-app.ts

// 参照元スタック
export class CdkSampleStack extends Stack {
  public readonly _sampleParameter: aws_ssm.StringParameter;

  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    const sampleParameter = new aws_ssm.StringParameter(
      this,
      'SampleParameter',
      {
        parameterName: 'SampleParameter',
        stringValue: 'SampleValue',
      }
    );

    this._sampleParameter = sampleParameter;
    this.exportValue(sampleParameter.parameterName); // exportValue を追加
  }
}

差分を確認してみると、参照元スタックのエクスポートは削除されていないことがわかります。

$ cdk diff
Stack CdkSampleStack
There were no differences
Stack CdkSampleStack2
IAM Statement Changes
┌───┬─────────────────────────────────────────────────────────────────────────────────────────────────────────┬────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────┬───────────────────────────────────┬───────────┐
│   │ Resource                                                                                                │ Effect │ Action                                                                                                   │ Principal                         │ Condition │
├───┼─────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────┼───────────┤
│ - │ arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/{"Fn::ImportValue":"CdkSampleStack │ Allow  │ ssm:DescribeParameters                                                                                   │ AWS:${SampleFunction/ServiceRole} │           │
│   │ :ExportsOutputRefSampleParameterEC52094C22ED334E"}                                                      │        │ ssm:GetParameter                                                                                         │                                   │           │
│   │                                                                                                         │        │ ssm:GetParameterHistory                                                                                  │                                   │           │
│   │                                                                                                         │        │ ssm:GetParameters                                                                                        │                                   │           │
└───┴─────────────────────────────────────────────────────────────────────────────────────────────────────────┴────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────┴───────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Resources
[-] AWS::IAM::Role SampleFunction/ServiceRole SampleFunctionServiceRoleE420739A destroy
[-] AWS::IAM::Policy SampleFunction/ServiceRole/DefaultPolicy SampleFunctionServiceRoleDefaultPolicy49A1212C destroy
[-] AWS::Lambda::Function SampleFunction SampleFunction7DB1D36A destroy

CDK デプロイをすると、参照先でのエクスポートの使用を削除できました。

$ cdk deploy '*'

  Synthesis time: 2.77s

CdkSampleStack2
CdkSampleStack:  start: Building d832fc04e7acebd774869f97322f00940219322e638150f74ae23396ee3e3e5e:current_account-current_region
CdkSampleStack:  success: Built d832fc04e7acebd774869f97322f00940219322e638150f74ae23396ee3e3e5e:current_account-current_region
CdkSampleStack:  start: Publishing d832fc04e7acebd774869f97322f00940219322e638150f74ae23396ee3e3e5e:current_account-current_region
CdkSampleStack2: deploying... [2/2]
CdkSampleStack2: creating CloudFormation changeset...
CdkSampleStack:  success: Published d832fc04e7acebd774869f97322f00940219322e638150f74ae23396ee3e3e5e:current_account-current_region

   CdkSampleStack2

  Deployment time: 27.65s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/CdkSampleStack2/87605200-0206-11ee-b170-0aa1384f745b

  Total time: 30.42s

CdkSampleStack
CdkSampleStack: deploying... [1/2]
CdkSampleStack: creating CloudFormation changeset...

   CdkSampleStack

  Deployment time: 17.32s

Outputs:
CdkSampleStack.ExportsOutputRefSampleParameterEC52094C22ED334E = SampleParameter
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/CdkSampleStack/e68b6900-f01c-11ed-8538-06bbc29e5367

  Total time: 20.09s

これであとは参照元のエクスポートを削除するなり、残したまま別のスタックでの参照に使用するなり出来ます。

3. 参照先での参照と、参照元のエクスポートを、1回のデプロイで削除できるのか?(できなかった)

ここで、参照先での参照と、参照元のエクスポートを、1回のデプロイで削除できるのか、気になったので試してみました。

一度、参照先での参照と、参照元のエクスポートをどちらも作成し直した上で、いずれも削除(コメントアウトされたコード部分)します。

  • CDK App

bin/cdk_sample_app.ts

import * as cdk from 'aws-cdk-lib';
import { CdkSampleStack } from '../lib/cdk-sample-app';
import { CdkSampleStack2 } from '../lib/cdk-sample-app-2';

const app = new cdk.App();

// 参照元スタック作成
const cdkSampleStack = new CdkSampleStack(app, 'CdkSampleStack', {});

// 参照先スタック作成
new CdkSampleStack2(app, 'CdkSampleStack2', {
  // sampleParameter: cdkSampleStack._sampleParameter, // 参照先でのエクスポートの使用を削除
});
  • 参照元スタック CdkSampleStack

lib/cdk-sample-app.ts

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

// 参照元スタック
export class CdkSampleStack extends Stack {
  public readonly _sampleParameter: aws_ssm.StringParameter;

  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    const sampleParameter = new aws_ssm.StringParameter(
      this,
      'SampleParameter',
      {
        parameterName: 'SampleParameter',
        stringValue: 'SampleValue',
      }
    );

    // 参照元のエクスポートを削除
    // this._sampleParameter = sampleParameter;
    // this.exportValue(sampleParameter.parameterName);
  }
}
  • 参照先スタック CdkSampleStack2

lib/cdk-sample-app-2.ts

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

interface CdkSampleStack2Props extends StackProps {
  // sampleParameter: aws_ssm.StringParameter; // 参照先でのエクスポートの使用を削除
}

// 参照先スタック
export class CdkSampleStack2 extends Stack {
  constructor(scope: Construct, id: string, props: CdkSampleStack2Props) {
    super(scope, id, props);

    // 参照先でのエクスポートの使用を削除
    // const sampleFunction = new aws_lambda_nodejs.NodejsFunction(
    //   this,
    //   'SampleFunction',
    //   {
    //     entry: 'src/sample-function.ts',
    //     environment: {
    //       SAMPLE_PARAMETER_NAME: props.sampleParameter.parameterName,
    //     },
    //   }
    // );
    // props.sampleParameter.grantRead(sampleFunction);
  }
}

デプロイをしようとすると、参照元スタックで Export <参照先スタック名>:<エクスポート名> cannot be deleted as it is in use by <参照元スタック名> というエラーが発生し、デプロイが失敗しました。

$ cdk deploy '*'

  Synthesis time: 2.72s

CdkSampleStack
CdkSampleStack: deploying... [1/2]
CdkSampleStack: creating CloudFormation changeset...

   CdkSampleStack failed: Error: The stack named CdkSampleStack failed to deploy: UPDATE_ROLLBACK_COMPLETE
    at FullCloudFormationDeployment.monitorDeployment (/Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:397:10236)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async Object.deployStack2 [as deployStack] (/Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:400:149977)
    at async /Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:400:135508

  Deployment failed: Error: The stack named CdkSampleStack failed to deploy: UPDATE_ROLLBACK_COMPLETE
    at FullCloudFormationDeployment.monitorDeployment (/Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:397:10236)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async Object.deployStack2 [as deployStack] (/Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:400:149977)
    at async /Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:400:135508

The stack named CdkSampleStack failed to deploy: UPDATE_ROLLBACK_COMPLETE

結果として、exportValue を使用した場合でも、参照先での参照と、参照元のエクスポートを、1回のデプロイで削除することはできませんでした。

exportValue の仕様を確認する

ここで CDK のドキュメントで exportValue の仕様を改めて確認してみます。

exportValue は スタックコンストラクトで使えるメソッドです。

ExportValueOptions ではオプションでエクスポートの名前を指定できます。

public exportValue(exportedValue: any, options?: ExportValueOptions): string

exportValue を使うことにより参照元にエクスポートを明示的に作成できます。よって次の記述の通り、参照先から参照を削除する際に、参照元でのエクスポートの存在を保証することが出来るようになります。

One of the uses for this method is to remove the relationship between two Stacks established by automatic cross-stack references. It will temporarily ensure that the CloudFormation Export still exists while you remove the reference from the consuming stack. After that, you can remove the resource and the manual export.

(日本語訳) このメソッドの用途の 1 つは、自動スタック間参照によって確立された 2 つのスタック間の関係を削除することです。 これにより、使用スタックから参照を削除する間、CloudFormation エクスポートがまだ存在することが一時的に確保されます。 その後、リソースと手動エクスポートを削除できます。

まとめ

ここまでの検証と仕様確認の結果、参照元での exportValue の使用の有無によって、参照先で参照を削除した際の挙動はそれぞれ次のようになることが分かりました。

exportValue を使わない場合

exportValue を使わなければ、参照元スタックにあるエクスポートの存在は参照先での参照に依存するようになるため、次のようにデプロイがエラーとなり失敗します。

  1. デプロイ前の合成で、参照元スタックのエクスポートが「削除」された CloudFormation テンプレートが作成される
  2. 依存関係の順番から、参照元スタックのデプロイが最初に行われる
  3. テンプレートに則って参照元スタックにあるエクスポートの削除が試みられるが、実際にはまだ参照先スタックで使用されているため、削除できずにエラーとなる

この挙動は本記事で試したうちの「1. exportValue を使わない場合、参照先でのエクスポートの使用を削除できない」と「3. 参照先での参照と、参照元のエクスポートを、1回のデプロイで削除できるのか?(できなかった)」に該当します。

抜け道としては、削除時に cdkSampleStack.addDependency(cdkSampleStack2); という作成時とは逆の依存関係を CDK App で定義すれば、参照先スタックが先にデプロイされて参照が削除されるので、デプロイが成功するようになります。しかし同じスタック間で参照の作成と削除を同時にできなくなります。逆の依存関係の定義を削除する掃除用デプロイもどちらにせよ必要となります。

exportValue を使った場合

exportValue を使えば、参照元スタックにあるエクスポートの存在は参照先での参照に依存しなくなり、次のようにデプロイが成功するようになります。

  1. デプロイ前の合成で、参照元スタックのエクスポートが「保持」された CloudFormation テンプレートが作成される
  2. 依存関係の順番から、参照元スタックのデプロイが最初に行われる
  3. 参照元スタックにあるエクスポートは保持される
  4. 続いて参照先スタックがデプロイされ、参照先の参照が削除される

この挙動は本記事で試したうちの「2. exportValue を使って、参照先でのエクスポートの使用を削除できる」に該当します。

おすすめの記事

今回の内容について詳しく知りたい方へは、以下の記事がおすすめです。

おわりに

AWS CDK のクロススタック参照で、「参照先でのエクスポートの使用」を削除する方法を確認してみました。

最近案件での実装でハマってしまったので、改めて確認してみた次第でした。

参考

以上