別スタックで作成したAWSリソースの情報を取得する

## 概要

CloudFormationまたはCDKでのAWS環境構築は、スタックと呼ばれるAWSリソースの集合を作成することによって行われます。

スタック作成の粒度は開発者によって様々ですが、別スタックで作成したリソースの情報を利用したい場合があります。

例：
・スタックAでS3バケットを作成
・スタックBで作成するLambdaの環境変数に、スタックAで作成したバケット名を設定

この場合、スタックAで作成したバケット名をスタックBで使用するためには以下のような方法が考えられます。

1. エクスポート値を使用しない-1
CloudFormationのAWSコンソールで、スタックAのリソース名を確認。取得したバケット名をスタックB作成のテンプレートにべた書きしてデプロイ

2. エクスポート値を使用しない-2
CloudFormationのAWSコンソールで、スタックAのリソース名を確認。取得したバケット名をスタックB作成時に外から注入してデプロイ

3. エクスポート値を使用する
スタックA作成時にエクスポート値としてS3バケット名を出力。スタックB作成時にエクスポート値をインポート(クロススタック参照)してデプロイ

4. パラメータストアを利用する
スタックA作成時パラメータストアにS3バケット名を登録。スタックB作成時にパラメータストアからバケット名を取得してデプロイ

今回はこれらを実際に試していきます。

## 試してみた

では、それぞれCDKで試してみます。

### エクスポート値を使用しない-1 

Stack APPのCDKは以下になります。
```bin/cdk.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { StackA } from '../lib/stack_a';
import { StackB } from '../lib/stack_b';

const app = new cdk.App();

new StackA(app, 'StackA', {
});
new StackB(app, 'StackB', {
});
```

まず、スタックAでS3バケットを作成します。
CDKは以下になります。

```lib/stack_a.ts
import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';

export class StackA extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const bucket = new s3.CfnBucket(this, 'CreateBucketA', {
      bucketName: 'bucket-export-test'
    });
  }
}
```

スタックAをデプロイします。
```bash
cdk deploy StackA
```

スタックが作成されたので、リソース名をAWSコンソールで確認します。
![1-1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/4fe64ac6-a3a4-cdbf-18f5-b32399115834.png)

次に、確認したS3バケット名をスタックBに入力してLambdaを作成します。
CDKは以下になります。

```lib/stack_b.ts
import * as cdk from '@aws-cdk/core';
import { Role, ServicePrincipal, ManagedPolicy } from '@aws-cdk/aws-iam';
import { Function, AssetCode, Runtime } from '@aws-cdk/aws-lambda';

export class StackB extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const executionLambdaRole = new Role(this, 'LambdaRole', {
      roleName: 'lambdaRole',
      assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
      managedPolicies: [
      ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
      ]
    });
    new Function(this, 'ExportTestFunction', {
      functionName: 'export-test-function',
      runtime: Runtime.NODEJS_14_X,
      code: AssetCode.fromAsset('src'),
      handler: 'index.handler',
      role: executionLambdaRole,
      environment: {
        BUCKET_NAME: "bucket-export-test"
      }
    });
  }
}
```

スタックBをデプロイします。
```bash
cdk deploy StackB
```

AWSコンソールでLambdaを確認すると、環境変数が設定されていることが確認できます。
![1-2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/9cddb617-3fee-9410-504b-180e09f30153.png)



### エクスポート値を使用しない-2

次に、バケット名をデプロイ時に外から注入する方法を紹介します。
※スタックAのCDKは先ほどと同様なため省略します。

Stack APPのCDKの変更点は以下になります。
```diff_javascript:bin/cdk.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { StackA } from '../lib/stack_a';
import { StackB } from '../lib/stack_b';

const app = new cdk.App();

new StackA(app, 'StackA', {
});
new StackB(app, 'StackB', {
+  bucket: app.node.tryGetContext(`bucket`)
});
```

Lambdaを作成するCDKの変更点は以下になります。
```diff_javascript:lib/stack_b.ts
import * as cdk from '@aws-cdk/core';
import { Role, ServicePrincipal, ManagedPolicy } from '@aws-cdk/aws-iam';
import { Function, AssetCode, Runtime } from '@aws-cdk/aws-lambda';

export class StackB extends cdk.Stack {
-  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
+  constructor(scope: cdk.Construct, id: string, props: StackBProps) {
    super(scope, id, props);

    const executionLambdaRole = new Role(this, 'LambdaRole', {
      roleName: 'lambdaRole',
      assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
      managedPolicies: [
      ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
      ]
    });
    new Function(this, 'ExportTestFunction', {
      functionName: 'export-test-function',
      runtime: Runtime.NODEJS_14_X,
      code: AssetCode.fromAsset('src'),
      handler: 'index.handler',
      role: executionLambdaRole,
      environment: {
-        BUCKET_NAME: "bucket-export-test"
+        BUCKET_NAME: props.bucket
      }
    });
  }
}

+export interface StackBProps extends cdk.StackProps {
+  readonly bucket: string;
+}
```

スタックAをデプロイします。
```bash
cdk deploy StackA
```


スタックBをデプロイします。
引数で渡したバケット名が環境変数に設定されます。
```bash
cdk deploy StackB --context bucket=bucket-export-test
```


### エクスポート値を使用する

次に、スタックAの作成時にバケット名をエクスポートし、スタックB作成時にインポートする方法を紹介します。
※スタックBのCDKは先ほどと同様なため省略します。

Stack APPのCDKの変更点は以下になります。
```diff_javascript:bin/cdk.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { StackA } from '../lib/stack_a';
import { StackB } from '../lib/stack_b';

const app = new cdk.App();

new StackA(app, 'StackA', {
});
new StackB(app, 'StackB', {
-  bucket: app.node.tryGetContext(`bucket`)
+  bucket: cdk.Fn.importValue(`bucketName`)
});
```

S3バケットを作成するCDKの変更点は以下になります。
```diff_javascript:lib/stack_a.ts
import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';

export class StackA extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const bucket = new s3.CfnBucket(this, 'CreateBucketA', {
      bucketName: 'bucket-export-test'
    });

+    new cdk.CfnOutput(this, 'BucketName', {
+      value: 'bucket-export-test',
+      exportName: 'bucketName'
+    });
  }
}
```

スタックAをデプロイします。
```bash
cdk deploy StackA
```

スタックBをデプロイします。
```bash
cdk deploy StackB
```


### パラメータストアを利用する

最後に、スタックAの作成時にバケット名をパラメータストアに登録し、スタックB作成時にパラメータストアから取得する方法を紹介します。

Stack APPのCDKファイルは以下になります。
```diff_javascript:bin/cdk.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { StackA } from '../lib/stack_a';
import { StackB } from '../lib/stack_b';

const app = new cdk.App();

new StackA(app, 'StackA', {
});
new StackB(app, 'StackB', {
-  bucket: cdk.Fn.importValue(`bucketName`)
});
```

S3バケットを作成するCDKの変更点は以下になります。
```diff_javascript:lib/stack_a.ts
import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';
+import * as ssm from '@aws-cdk/aws-ssm';

export class StackA extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const bucket = new s3.CfnBucket(this, 'CreateBucketA', {
      bucketName: 'bucket-export-test'
    });

-    new cdk.CfnOutput(this, 'BucketName', {
-      value: 'bucket-export-test',
-      exportName: 'bucketName'
-    });
+    const param = new ssm.StringParameter(this, 'BucketName', {
+      stringValue: 'bucket-export-test',
+      parameterName: '/bucket/test',
+    });
  }
}
```

Lambdaを作成するCDKの変更点は以下になります。
```diff_javascript:lib/stack_b.ts
import * as cdk from '@aws-cdk/core';
import { Role, ServicePrincipal, ManagedPolicy } from '@aws-cdk/aws-iam';
import { Function, AssetCode, Runtime } from '@aws-cdk/aws-lambda';
+import * as ssm from '@aws-cdk/aws-ssm';

export class StackB extends cdk.Stack {
-  constructor(scope: cdk.Construct, id: string, props: StackBProps) {
+  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const executionLambdaRole = new Role(this, 'LambdaRole', {
      roleName: 'lambdaSecureExecutionRole',
      assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
      managedPolicies: [
      ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
      ]
    });
    new Function(this, 'ExportTestFunction', {
      functionName: 'export-test-function',
      runtime: Runtime.NODEJS_14_X,
      code: AssetCode.fromAsset('src'),
      handler: 'index.handler',
      role: executionLambdaRole,
      environment: {
-        BUCKET_NAME: props.bucket
+        BUCKET_NAME: ssm.StringParameter.valueForStringParameter(this, '/bucket/test')
      }
    });
  }
}

-export interface StackBProps extends cdk.StackProps {
-  readonly bucket: string;
-}
```

スタックAをデプロイします。
```bash
cdk deploy StackA
```

スタックBをデプロイします。
```bash
cdk deploy StackB
```


## おまけ

AWS CLIコマンドでもエクスポート値を取得することが出来るので、以下に記載します。

- 対象のスタック名の全てのエクスポート値を取得
```bash
# StackAのエクスポート値
aws cloudformation describe-stacks \
  --stack-name StackA \
  --query "Stacks[0].Outputs[]"

[
    {
        "OutputKey": "BucketA",
        "OutputValue": "bucketa",
        "ExportName": "bucketNameA"
    },
    {
        "OutputKey": "BucketB",
        "OutputValue": "bucketb",
        "ExportName": "bucketNameB"
    }
]
```

- 対象のスタック名の指定したエクスポート値を取得
```bash
# StackAのエクスポート名：bucketNameAを取得
aws cloudformation describe-stacks \
  --stack-name StackA \
  --query "Stacks[0].Outputs[?ExportName=='bucketNameA'].OutputValue" \
  --output text

bucketa
```

- 指定したエクスポート値を取得
# エクスポート名：bucketNameの値を取得
```bash
aws cloudformation list-exports \
  --query "Exports[?Name=='bucketNameA'].Value[]" \
  --output text

bucketa
```


## おわりに

今回はスタック間のAWSリソース情報の受け渡しについて説明させて頂きました。
CloudFormationやCDKを利用する際はよく使うので、ご参考になれば幸いです。