【AWS CDK (Python)】 クロススタック参照の方法

## はじめに
本記事は、私自身の備忘録を兼ねてAWS CDKをこれから始める方の一助になればと思い、AWS CDKの使い方等をまとめたものです。
今回は、AWS CDKでスタック間参照する方法を確認しています。
なお、本記事は私自身の経験を基に記載していますが、間違いがあったらすみません。

## スタック分割とクロススタック参照
あるスタックで作成したリソースの情報（ARNなど）を別のスタックから参照することをクロススタック参照やスタック間参照と言います。
スタックは、ライフサイクルや管理主体により分割する方がよいとされています。しかし、分割した際にスタック間でリソース情報を参照する必要が出てきます。例えば、VPCとEC2のスタックを分けた場合、EC2のスタックでVPCスタックにあるsubnetの情報を参照する必要が出てきます。
こうした場合に、参照されるスタックではexport、参照するスタックではimportすることで必要な情報をスタック間で参照することが可能です。

## 環境
本記事は以下の環境を使用して記載しています。
* AWS Cloud9
* AWS CDK：2.80.0
* Python： 3.10.11
* Node.js： 16.20.0

また、以下の記事に基づいてAWS CDKの環境を作成しています。
* [【AWS CDK/Python】 AWS CDKの始め方](https://qiita.com/takataka987/items/4448dbf06d8c70173305)

## クロススタック参照の方法
今回は、あるスタックで作成したS3バケットのバケット名とARNを別のスタックから参照してみます。
参照の方法がいくつかあるので、方法ごとに見ていきましょう。

* [aws_cdk.CfnOutput/aws_cdk.Fn.import_valueを使う方法](#aws_cdkcfnoutputaws_cdkfnimport_valueを使う方法)
* [aws_cdk.Stack.export_value/aws_cdk.Fn.import_valueを使う方法](#aws_cdkstackexport_valueaws_cdkfnimport_valueを使う方法)
* [Stackのアトリビュートに追加する方法](#stackのアトリビュートに追加する方法)

### aws_cdk.CfnOutput/aws_cdk.Fn.import_valueを使う方法
まず、aws_cdk.CfnOutputとaws_cdk.Fn.import_valueを使用する方法です。
以下のように参照される側のスタックでS3の作成とaws_cdk.CfnOutputを使用してバケット名とARNをexportします。
```python:cdk_app/cdk_export_stack.py
from aws_cdk import (
    Stack,
    aws_s3 as s3,
    RemovalPolicy,
    CfnOutput
)
from constructs import Construct

class CdkExportStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        s3bucket = s3.Bucket(
            self,
            "MyS3Bucket",
            bucket_name = "cdk-test-bucket-yyyymmdd",
            versioned=True,
            removal_policy=RemovalPolicy.DESTROY
        )

        # ARNをexport.        
        CfnOutput(
            self,
            "exportvalue_CfnOutput_arn",
            value=s3bucket.bucket_arn,
            description="S3Bucket ARN",
            export_name="s3bucket-arn-CfnOutput"
        )

        # バケット名をexport.        
        CfnOutput(
            self,
            "exportvalue_CfnOutput_name",
            value=s3bucket.bucket_name,
            description="S3Bucket Name",
            export_name="s3bucket-name-CfnOutput"
        )
```

参照する側では、aws_cdk.Fn.import_valueを使用してバケット名とARNをimportします。（確認のため、importしたバケット名とARNをパラメータストアに格納します。）
```python:cdk_app/cdk_import_stack.py
from aws_cdk import (
    Stack,
    aws_ssm as ssm,
    Fn
)
from constructs import Construct
import json

class CdkImportStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # ARN、バケット名のimport.
        value={
            "ARN": Fn.import_value("s3bucket-arn-CfnOutput"),
            "NAME": Fn.import_value("s3bucket-name-CfnOutput")
        }

        # 確認用：パラメータストアへの格納.
        cfn_parameter = ssm.CfnParameter(
            self,
            "put_parameter_CfnOutput",
            type="String",
            value=json.dumps(value, indent=4),
            description="test export/import value.",
            name="/test/parameter1",
        )
```

なお、app.pyでは、`依存元スタックオブジェクト.add_dependency(依存先スタックオブジェクト)`のようにスタック間に依存関係を追加し、スタック作成順序を制御しています。
```python:app.py
#!/usr/bin/env python3
import os
import aws_cdk as cdk
from cdk_app.cdk_export_stack import CdkExportStack
from cdk_app.cdk_import_stack import CdkImportStack

app = cdk.App()

CdkExportStack = CdkExportStack(app, "CdkExportStack",
    )
CdkImportStack = CdkImportStack(app, "CdkImportStack",
    )
# CdkExportStackが先に作成されるよう依存関係を追加
CdkImportStack.add_dependency(CdkExportStack)

app.synth()
```

ではデプロイしてみます。
```console
(.venv) user_name:~/environment/cdk-app (master) $ cdk deploy --all
```

マネージメントコンソールからCloudFormationのスタックの出力を確認してみます。ARN、バケット名がexportされていることが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/e0123284-d10e-c3dd-12d5-35af70e6eadc.png)

パラメータストアでも確認してみます。ARN、バケット名共に格納されてますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/a31bb6b1-db81-734b-addd-fac0b749da64.png)

### aws_cdk.Stack.export_value/aws_cdk.Fn.import_valueを使う方法
次に、aws_cdk.Stack.export_valueとaws_cdk.Fn.import_valueを使用する方法です。
以下のように参照される側のスタックでaws_cdk.Stack.export_valueを使用してバケット名とARNをexportします。ほぼCfnOutputと同じですね。
```python:cdk_app/cdk_export_stack.py
from aws_cdk import (
    Stack,
    aws_s3 as s3,
    RemovalPolicy,
)
from constructs import Construct

class CdkExportStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        s3bucket = s3.Bucket(
            self,
            "MyS3Bucket",
            bucket_name = "cdk-test-bucket-yyyymmdd",
            versioned=True,
            removal_policy=RemovalPolicy.DESTROY
        )

        # ARNをexport.        
        Stack.export_value(
            self,
            exported_value=s3bucket.bucket_arn,
            name="s3bucket-arn-export-value"
        )

        # バケット名をexport.        
        Stack.export_value(
            self,
            exported_value=s3bucket.bucket_name,
            name="s3bucket-name-export-value"
        )
```

参照する側も先ほどと同じです。
```python:cdk_app/cdk_import_stack.py
from aws_cdk import (
    Stack,
    aws_ssm as ssm,
    Fn
)
from constructs import Construct
import json

class CdkImportStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # ARN、バケット名のimport.
        value={
            "ARN": Fn.import_value("s3bucket-arn-export-value"),
            "NAME": Fn.import_value("s3bucket-name-export-value")
        }

        # 確認用：パラメータストアへの格納.
        cfn_parameter = ssm.CfnParameter(
            self,
            "put_parameter_export_value",
            type="String",
            value=json.dumps(value, indent=4),
            description="test export/import value.",
            name="/test/parameter2",
        )
```

デプロイしてマネージメントコンソールでCloudFormationのスタックの出力とパラメータストアを確認してみます。先ほどと同様に確認できます。先ほどと違いがあるとするとaws_cdk.Fn.import_valueを使用する場合は説明（description）の設定ができないことでしょうか。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/203ba9ef-9221-fa43-c424-c8088f88f79c.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/fb8a9c10-94fd-7fc3-87ce-a559d19abb29.png)

### Stackのアトリビュートに追加する方法
最後に、Stackのアトリビュートに追加する方法です。
以下のように参照される側のスタックでStackのアトリビュートにバケット名とARNを追加します。一番シンプルですね。
```python:cdk_app/cdk_export_stack.py
from aws_cdk import (
    Stack,
    aws_s3 as s3,
    RemovalPolicy,
)
from constructs import Construct

class CdkExportStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        s3bucket = s3.Bucket(
            self,
            "MyS3Bucket",
            bucket_name = "cdk-test-bucket-yyyymmdd",
            versioned=True,
            removal_policy=RemovalPolicy.DESTROY
        )
        
        # ARNをexport.        
        Stack.s3bucket_arn = s3bucket.bucket_arn
        # バケット名をexport.        
        Stack.s3bucket_name = s3bucket.bucket_name
```

参照する側でもStackのアトリビュートを参照します。
```python:cdk_app/cdk_import_stack.py
from aws_cdk import (
    Stack,
    aws_ssm as ssm,
)
from constructs import Construct
import json

class CdkImportStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # ARN、バケット名のimport.
        value={
            "ARN": Stack.s3bucket_arn,
            "NAME": Stack.s3bucket_name
        }

        # 確認用：パラメータストアへの格納.
        cfn_parameter = ssm.CfnParameter(
            self,
            "put_parameter_s3bucket_arn",
            type="String",
            value=json.dumps(value, indent=4),
            description="test export/import value.",
            name="/test/parameter3",
        )
```

デプロイしてマネージメントコンソールでCloudFormationのスタックの出力とパラメータストアを確認してみます。先ほどと同様に確認できます。ただ、この方法だとexport名の指定も説明の設定もできないので、パッと見た目が分かりづらい状態になってしまいます。CloudFormation Templateでもこのexport名になっているので、見た目で分かりづらいのが難点でしょうか。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/1c40204d-314b-5fa1-c84b-97edf7fc0be1.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/d702679c-f83c-43aa-5252-65d789fde114.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/845f7e90-13c2-dcc7-02f2-4be64e4f7fb1.png)

## まとめ
AWS CDKでのクロススタック参照の方法を確認しました。3つの方法は、どれもあまり変わらないので好みで使用してもらえればと思います。（説明を付けたいならCfnOutput、見た目気にせずとにかくシンプルが好みならStackのアトリビュートに追加ですかね。）
最後まで読んでいただいてありがとうございます。
少しでも参考になれば幸いです。
#### 参考文献
* [AWS Cloud Development Kit (AWS CDK) v2デベロッパーガイド](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/home.html)
* [AWS CDK Python Reference](https://docs.aws.amazon.com/cdk/api/v2/python/index.html)