【AWS CDK】APP内でStackを分割をする際の注意

## はじめに
AWS CDKのベストプラクティスには、「ステートフルリソースは別スタックにすることを検討すること。ステートフルリソースは削除されたら困るので、Stackの削除保護を個別に設定する」と書かれています。
普通に考えると同一APPの中でもVPCやRDS等間違っても削除したくないリソースは別Stackにして削除保護しようねって理解になり至極まっとうなことを言っているように感じます。
しかし、個人的には同一APP内でStackを分割する際は、注意が必要です。
そのため、基本的にはStack間でObjectの受け渡しする構成は避け、「1APP、1Stack構成でStackを分割しないこと」をお勧めします。
※Stack間でObjectの受け渡ししない場合は、問題ないです。

本記事では、どうして「1AP、1Stack構成でStackを分割しない」方がいいと思うのかご紹介します。
※ 本ブログに記載した内容は個人の見解であり、所属する会社、組織とは全く関係ありません。

## 1APP複数Stack構成でDeployエラーが発生した際の構成
下記リソースをベストプラクティスに従い以下のような構成でAPPを作成しました。
### Deployしたいリソース
* VPC系(VPC,Subnet等)
* ALB
* ECS Cluster
* ECS Fargate

### APP構成
| Stack種別 | 含まれるリソース | 
|:-----------|------------:|
| 共通系(common)       | VPC系、ECS Cluster|
| 個別系(system)       | ALB, ECS Fargate|

共通系(common)StackからVPC、ECS Clusterを個別系(System)Stackに引き渡している。

## Stack間Object引き渡し簡易CDK構成

### APPコード
~~~typescript:app.ts
const common = new TestCommonStack(app, 'CommonStack', {});
new TestSystemStack(app, `SystemStack`,{
   myVpc: common.myVpc,
   myCluster: common.myCluster
})
~~~

### 共通系(common)Object引き渡しイメージ

#### Stackコード
~~~typescript:common-stack.ts
export class TestCommonStack extends cdk.Stack {
  public myCluster: ecs.Cluster;
  public myVpc: ec2.Vpc;
  ~略~
  const common = new Common(this, "Nw");
  this.myVpc = common.myVpc;
  this.myCluster = common.myCluster;
~~~

#### Constructsコード
~~~typescript:common.ts
export class Common extends Construct {
  public myVpc: ec2.Vpc;
  public myCluster: ecs.Cluster;
~~~


### 個別系(System)Object受け取りイメージ

#### Stackコード
~~~typescript:system-stack.ts
export interface TestSystemkProps extends StackProps {
  myVpc: ec2.Vpc;
  myCluster: ecs.Cluster,
}
  ~略~
  new System(this, "System",{
    myVpc: props.myVpc,
    myCluster: props.myCluster
    }
~~~

#### Constructsコード
~~~typescript:system.ts
export interface SystemProps {
  myVpc: ec2.Vpc,
  myCluster: ecs.Cluster
}
~~~
上記イメージでStack間のObject引き渡しを構成。
初動Deployで問題なくDeploy出来ましたが、部分的なリソース削除メンテ(ALB)での変更セットを実行した際にDeployエラーが発生！！
※変更セットをかけるときは、個別系(System)Stackのみ実行

#### エラーキャプチャ
~~~log:error.log
 $ cdk deploy SystemStack

✨  Synthesis time: 10.38s

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

 ❌  CommonStack failed: Error: The stack named CommonStack failed to deploy: UPDATE_ROLLBACK_COMPLETE
    at FullCloudFormationDeployment.monitorDeployment (/home/ec2-user/.nvm/versions/node/v16.20.0/lib/node_modules/cdk/node_modules/aws-cdk/lib/index.js:467:10232)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async Object.deployStack2 [as deployStack] (/home/ec2-user/.nvm/versions/node/v16.20.0/lib/node_modules/cdk/node_modules/aws-cdk/lib/index.js:470:179911)
    at async /home/ec2-user/.nvm/versions/node/v16.20.0/lib/node_modules/cdk/node_modules/aws-cdk/lib/index.js:470:163159

 ❌ Deployment failed: Error: The stack named CommonStack failed to deploy: UPDATE_ROLLBACK_COMPLETE
    at FullCloudFormationDeployment.monitorDeployment (/home/ec2-user/.nvm/versions/node/v16.20.0/lib/node_modules/cdk/node_modules/aws-cdk/lib/index.js:467:10232)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async Object.deployStack2 [as deployStack] (/home/ec2-user/.nvm/versions/node/v16.20.0/lib/node_modules/cdk/node_modules/aws-cdk/lib/index.js:470:179911)
    at async /home/ec2-user/.nvm/versions/node/v16.20.0/lib/node_modules/cdk/node_modules/aws-cdk/lib/index.js:470:163159

The stack named CommonStack failed to deploy: UPDATE_ROLLBACK_COMPLETE
~~~

## Deployエラーが発生した原因
CDKでStack間でのパラメータの引き渡しを行う場合は、CloudFormationのExport/Importが自動生成され構成され自動で制御されます。
つまり、上記CDKコードイメージでは以下のようなCloudFormationの構成となります。
※原因のALBにフォーカスしています。

#### 共通系(common)Stack
~~~json:CommonStack.template.json
 "Outputs": {
  "ExportsOutputRefCommonVpcPublicSubnet1SubnetB69EA86EF613C26B": {
   "Value": {
    "Ref": "CommonVpcPublicSubnet1SubnetB69EA86E"
   },
   "Export": {
    "Name": "CommonStack:ExportsOutputRefCommonVpcPublicSubnet1SubnetB69EA86EF613C26B"
   }
  },
  "ExportsOutputRefCommonVpcPublicSubnet2Subnet804AB69C939865F4": {
   "Value": {
    "Ref": "CommonVpcPublicSubnet2Subnet804AB69C"
   },
   "Export": {
    "Name": "CommonStack:ExportsOutputRefCommonVpcPublicSubnet2Subnet804AB69C939865F4"
   }
  },
~~~

#### 個別系(System)Stack
~~~json:SystemStack.template.json
  "SystemOnlineAlb5C2923BE": {
   "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer",
   "Properties": {
    ~略~
    "Subnets": [
     {
      "Fn::ImportValue": "CommonStack:ExportsOutputRefCommonVpcPublicSubnet1SubnetB69EA86EF613C26B"
     },
     {
      "Fn::ImportValue": "CommonStack:ExportsOutputRefCommonVpcPublicSubnet2Subnet804AB69C939865F4"
     }
    ],
~~~

上記イメージでは、共通系(common)StackからDepolyされる各SubnetID, VPC IDが「CommonStack:ExportsOutputRefCommonVpcPublicSubnet1SubnetB69EA86EF613C26B」等でExportされている。
また、個別系(System)StackでALBを作成する箇所で共通系(common)StackからSubnetIDを「"Fn::ImportValue": "CommonStack:ExportsOutputRefCommonVpcPublicSubnet1SubnetB69EA86EF613C26B」等でImportしています。
この自動生成はすごく便利なんですが、後々のエラーに繋がります。

#### 初動Deploy時のイメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635217/8e549808-5755-2bf9-06a4-d997c7c61de0.png)


Deployエラーが発生する本メンテナンスケースでは、ALBの削除になるため、「個別系(System)StackのALBに関する部分を削除」してくれるだけで問題ありません。

しかし、CDKでは、Export/Importは自動生成され自動制御されているので、まず親スタックになっている「共通系(common)StackでExportしている該当部分の削除」から実施されます。
エラーキャプチャでも個別系(System)Stackだけ、変更セットをかけているのに、共通系(common)Stack変更セットが実行されているのが分かります。
※Outputsセクションの変更は、Mustではないのに。。。

#### 変更セット適用時のイメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635217/278558bc-4361-6471-0bd8-2fbb452e4276.png)

ここがエラーの原因となります。
「共通系(common)StackでExportしている該当部分の削除」をしようとしても「個別系(System)Stackの該当箇所を削除」の削除がされていないので、まだ参照されている状態となります。
そのため、「共通系(common)StackでExportしている該当部分の削除」が出来ず、Deployエラーが発生してしまうという流れです。

## まとめ
以上より、個人的には、1APPの中に複数Stackを構成し、Objectの引き渡しをしている場合、将来的なメンテナンスがやり辛くなるリスクがあると考えています。
※今のところこの事象に出会ったことがあるの、VPC周りが関係する時だけです。引き渡したObjectで別Stackで使用しているパラメータが複数あり、そのパラメータ群で部分的に使わなくなるような変更セット場合に発生する可能性が高そう。。。
そのため、安全に倒し、「1APP、1Stack構成でStackを分割しないこと」お勧めします。
Stackの削除保護が使いづらくなりますが、それは「removalPolicy」でConstructs毎に設定し、代替え出来ます。

## 懸念
「1APP、1Stack構成でStackを分割しないこと」にも懸念があります。
CDKでは、Constructs単位で使いやすいようにコード分割が可能です。
なので、1Stackで構成するリソースが膨らみがちで、気づいたら「CloudFormationの作成できるリソース上限の500を超過してしまった!」なんてケースがあります。

その際は、Stack分割が一番楽ですが、メンテナンス時にDeployエラーが発生するのもつらいです。。。
※大体VPCのStackは分割されると思います。今回のケースのように1つのリソースしか使っていないSubnetとかあると本事象が発生してしまいます。

なので、Lookup Method等で他のAPPが生成したリソース情報を取ってくる形式でAPPごと分割してしまうのがお勧めと考えています。
しかし、APP分割できるかの検討が、Lookupd等で欲しい情報が持ってこれるか検討する必要性があり、APPのDeploy順序関係もでてきてしまうので、別の辛みも出てきます。
個人的にはDeployエラーの方が嫌なので、仕方がないのかなって所感です。。。

※ 本ブログに記載した内容は個人の見解であり、所属する会社、組織とは全く関係ありません。