最初のStackを騙せ！cdk deployを騙せ！

# はじめに

タイトルの元ネタは特に説明しません。今回はAWS CDKで複数のStackを作った時に発生するdeployエラーの回避方法について書きます。

# 発生しうるdeployエラー

CDKを実装する時に、設計思想的な観点からもリソース数制限の観点からもStackを分ける実装をおそらくすることは結構あると思います。この時、`cdk deploy`時に以下のようなエラーが発生することが度々ありました。

```
Export Stack:XXXX cannot be deleted as it is in use by StackXXXX
```

これが起こってしまうのは大体以下のようなStackを実装した場合だと思います（[コード全体](https://github.com/ufoo68/sampleStack)）。

```typescript:sampleStack/lib/stackA.ts
import * as cdk from '@aws-cdk/core'
import * as lambda from '@aws-cdk/aws-lambda'

export class StackA extends cdk.Stack {
  public readonly handler: lambda.Function

  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)
    
    this.handler = new lambda.Function(this, `${id}Handler`, {
      runtime: lambda.Runtime.NODEJS_12_X,
      code: lambda.Code.fromAsset('lambda'),
      handler: 'index.handler',
    })
  }
}
```

```typescript:sampleStack/lib/stackB.ts
import * as cdk from '@aws-cdk/core'
import * as lambda from '@aws-cdk/aws-lambda';
import * as apigateway from '@aws-cdk/aws-apigateway'

interface StackBProps extends cdk.StackProps {
  handler: lambda.Function
}

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

    new apigateway.LambdaRestApi(this, `${id}Endpoint`, {
      handler: props.handler,
    })
  }
}
```

```typescript:sampleStack/bin/sample_stack.ts
import * as cdk from '@aws-cdk/core'
import { StackA } from '../lib/stackA'
import { StackB } from '../lib/stackB'

const app = new cdk.App()
const { handler } = new StackA(app, 'StackA')
new StackB(app, 'StackB', { handler })
```

このスタックではlambdaのリソースを`StackA`に実装して、API Gatewayのリソースを`StackB`に実装しています。そして`StackA`で定義したリソースを`StackB`で呼び出しています（実際はこの程度であれば同じスタックで定義すると思いますが一応例として、、）。
このとき`StackB`をデプロイすると必ず`StackA`もデプロイされるはずです。`StackB`は`StackA`に依存する形になっています。`StackA`がデプロイされると以下のようなOutputsが表示されるはずです。

```
StackA.ExportsOutputFnGetAttStackAHandlerxxx = arn:aws:lambda:region:xxxxx:function:StackA-StackAHandlerxxxxx
```

CDKではStack間の変数やインスタンスの受け渡しを行うと、それをCloudFormationに変換した時に[Outputsの機能](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html)を利用して内部的に処理しているようです。
これはCDKを実装しているときには特に意識することはないのですが、例えば`StackA`を以下のように変更したときに面倒なことになります。

```typescript:sampleStack/lib/stackA.ts
...
    this.handler = new lambda.Function(this, `${id}Lambda`, { // IDを変更
      runtime: lambda.Runtime.NODEJS_12_X,
      code: lambda.Code.fromAsset('lambda'),
      handler: 'index.handler',
    })
...
```

変更したのは単に`lambda.Function`の第２引数のIDの文字列だけなのですが、このIDはOutputsのキー名に関係します。おそらくOutputsが以下のように変わるはずです(単に`Hadler`が`Lambda`に変わるだけです)。

```
StackA.ExportsOutputFnGetAttStackALambdaxxx = arn:aws:lambda:region:xxxxx:function:StackA-StackALambdaxxxxx
```

このこと自体は特に大した問題ではありませんが、デプロイ時には必ず`StackA`->`StackB`という順番でのデプロイになってしまうので、「StackBで使うはずのOutputsの値がなくなっている」と勝手にCloudFormationが判断してしまってデプロイを失敗させてしまいます。

# 考えた解決方法

本当はCDKのデプロイについての設定（順番とか）が充実していたらこんな面倒な対処は必要ないのですが、現状その機能はなさそうなのでこちらでなんとかするしかありません。
一番手っ取り早いのは`StackA`を一旦destroyすることなのですが、あまりやりたくない場合もあると思います（CloudFrontとか使い出すと削除とデプロイにものすごく時間がかかります）。
CDKには`CfnOutput`というClassが存在します。これはCloudFormationのOutputsが使える機能です。つまりこれで擬似的にOutputsを生成すればデプロイエラーを回避できるのではと思って`StackA`に以下を加えました。

```typescript:sampleStack/bin/sample_stack.ts
import * as cdk from '@aws-cdk/core'
import * as lambda from '@aws-cdk/aws-lambda'

export class StackA extends cdk.Stack {
  public readonly handler: lambda.Function

  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)
    // 実際の値を設定
    const region = '';
    const accountId = '';

    this.handler = new lambda.Function(this, `${id}Lambda`, {
      runtime: lambda.Runtime.NODEJS_12_X,
      code: lambda.Code.fromAsset('lambda'),
      handler: 'index.handler',
    })

    // 擬似的なOutputsの作成
    new cdk.CfnOutput(this, 'handlerOutput', {
      exportName: 'tackA:ExportsOutputFnGetAttStackAHandlerxxx',
      value: `arn:aws:lambda:${region}:${accountId}:function:StackA-StackAHandlerxxxxx`,
    })
  }
}
```

この`CfnOutput`は`exportName`をキー名に、`value`をその値にあてはめます（どちらも変更前のOutputsの値と完全に一致させることが重要です）。
すると案の定ビンゴで、`StackA`の変更のデプロイを無事に行うことができました。この追加実装はデプロイの一時回避用なので、デプロイ後は消しても問題ありません。

# さいごに

完全にこのタイトルで書きたいがために書いた記事です。一応このタイトルの元ネタ通り、過去（変更前のStack）と未来（変更後のStack）の辻褄を合わせることで悲しい悲劇を回避することができるので同じ問題に困った時はぜひ試してみてください！