AWS CDK (Python) で別スタックのサービスを参照する

# はじめに

CDKでスタックを作成するとき、インフラ系のサービス（VPC、S3、CloudWatch、・・・）と、アプリケーションやサーバと直接結びつくサービス（Lambda、EC2、・・・）とで、スタックを分けたいと思うことがありました。スタックのデプロイは時間がかかるので、スタックを分けることであまり変更のないスタックはそのままにしつつ、よく変更するアプリケーションやサーバのスタックだけ更新するといったことができるメリットがあります。

他にも無償の範囲で利用できるサービスと、プロビジョニングするだけで費用が発生するサービス（VPCエンドポイントやALBなど）で分けておくと、不必要なときはまとめてスタックを消しておくことでコスト削減につなげるといったことも可能になります。

すでに作成されているサービスを参照するときは、各サービスの`from_lookup`関数を使えば検索できます。しかし、自分のアカウントIDやリージョンを予め指定する必要があるほか、CDKで作成したスタックのテンプレートを他アカウントで使用できないなどのデメリットがあります。できることなら`from_lookup`関数の使用を避ける設計をしたほうが保守性の高いシステムになるのではないかと、個人的には感じています。

ここでVPCの`from_lookup`のリファレンスを読むと、次の記載があります。

> This function only needs to be used to use VPCs not defined in your CDK application. If you are looking to share a VPC between stacks, you can pass the Vpc object between stacks and use it as normal.

> この関数は、CDK アプリケーションで定義されていない VPC を使用する場合にのみ使用する必要があります。スタック間でVPCを共有したい場合は、スタック間でVpcオブジェクトを渡し、通常通り使用することができます。（DeepL翻訳）

https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_ec2/Vpc.html#aws_cdk.aws_ec2.Vpc.from_lookup

公式的にも、CDKで作成したサービスを参照するときは`from_lookup`を使用せず、スタック間でオブジェクトを渡せばいいとあります。

が、じゃあどうすればできるんだよ？というのを探すのに苦労したので、備忘録として残します。

## 結論

スタックごとに作成したサービスのオブジェクトをインスタンス変数として保持しておき、コンストラクタでスタックのオブジェクトごと渡すだけで参照できます。

この方法に限らず、作ったサービスのオブジェクトを何らかの方法で渡せばいいです。

# 実際の書き方

通常通り、Cloud9などのサーバでCDKの開発環境を初期化します。

```shell
mkdir sample_app
cd sample_app
cdk init app --language python
source .venv/bin/activate
python -m pip install -r requirements.txt
cdk synth
# リージョンで初めてCDKを使うときのみ以下を実行
cdk bootstrap
```

ここでは冒頭の例のようにインフラとサーバでスタックを分割し、インフラスタックでVPCを作成し、サーバスタックでEC2を配置してみます。まずはインフラスタックを作成します。

```python:sample_app/infra_stack.py
from aws_cdk import (
    Stack,
    aws_ec2,
)
from constructs import Construct

class InfraStack(Stack):

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

        self.vpc = aws_ec2.Vpc(self, "infra-stack-vpc",
            cidr="10.0.0.0/16",
            enable_dns_support=True,
            enable_dns_hostnames=True,
            max_azs=3,
            nat_gateways=0,
            subnet_configuration=[
                aws_ec2.SubnetConfiguration(
                    name="infra-subnet",
                    cidr_mask=24,
                    subnet_type=aws_ec2.SubnetType.PRIVATE_ISOLATED,
                ),
            ],
        )
```

ここでのポイントは、`aws_ec2.Vpc`のオブジェクトをスタックのインスタンス変数として保持している点です。これにより実際にこのスタッククラスを初期化する`app.py`で`aws_ec2.Vpc`オブジェクトを参照できるようになります。あとは後続のスタックに`InfraStack`のインスタンスをコンストラクタで渡すだけです。

```python:sample_app/server_stack.py
from aws_cdk import (
    Stack,
    aws_ec2,
)
from constructs import Construct
from sample_app.infra_stack import InfraStack

class ServerStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, infra: InfraStack, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)
        
        self.ec2 = aws_ec2.Instance(self, "test-instance",
            vpc=infra.vpc,
            instance_type=aws_ec2.InstanceType.of(
                aws_ec2.InstanceClass.T2,
                aws_ec2.InstanceSize.MICRO
            ),
            machine_image=aws_ec2.MachineImage.latest_amazon_linux(
                generation=aws_ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
            ),
        )
```

```python:app.py
#!/usr/bin/env python3
import os

import aws_cdk as cdk

from sample_app.infra_stack import InfraStack
from sample_app.server_stack import ServerStack

app = cdk.App()

env=cdk.Environment(account=os.getenv('CDK_DEFAULT_ACCOUNT'), region=os.getenv('CDK_DEFAULT_REGION'))
infra = InfraStack(app, "InfraStack",
    env=env,
)
server = ServerStack(app, "ServerStack",
    env=env,
    infra=infra,
)

app.synth()
```
        
最後に、作ったスタックをデプロイします。デプロイに限らず、複数スタックの操作をするときは、コマンドの最後に`--all`をつける必要があります。

```shell
cdk synth --all
cdk deploy --all
```

なお、`--all`の代わりにスタック名を指定すると、個別のスタックごとに操作することもできます。

```shell
cdk destroy ServerStack
```

# おわりに

CDKは個別関数レベルのリファレンスは結構整理されていて見やすいですが、使い方のサンプルが少ないのが難点です。