Help us understand the problem. What is going on with this article?

最初の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を実装した場合だと思います(コード全体)。

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',
    })
  }
}
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,
    })
  }
}
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もデプロイされるはずです。StackBStackAに依存する形になっています。StackAがデプロイされると以下のようなOutputsが表示されるはずです。

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

CDKではStack間の変数やインスタンスの受け渡しを行うと、それをCloudFormationに変換した時にOutputsの機能を利用して内部的に処理しているようです。
これはCDKを実装しているときには特に意識することはないのですが、例えばStackAを以下のように変更したときに面倒なことになります。

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の第2引数のIDの文字列だけなのですが、このIDはOutputsのキー名に関係します。おそらくOutputsが以下のように変わるはずです(単にHadlerLambdaに変わるだけです)。

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に以下を加えました。

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)

    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:xxxxx:function:StackA-StackAHandlerxxxxx',
    })
  }
}

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

さいごに

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

ufoo68
IoT関係の会社で働いています。学生のときは画像認識を使った研究をしていました。最近はAWSかLINEを使った開発に興味があります。
access
SDNからセンサ、家電、電子書籍まで。ACCESSはあらゆるレイヤのデバイス、サービスを「繋げて」いきます。
http://jp.access-company.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away