はじめに
タイトルの元ネタは特に説明しません。今回はAWS CDKで複数のStackを作った時に発生するdeployエラーの回避方法について書きます。
発生しうるdeployエラー
CDKを実装する時に、設計思想的な観点からもリソース数制限の観点からもStackを分ける実装をおそらくすることは結構あると思います。この時、cdk deploy
時に以下のようなエラーが発生することが度々ありました。
Export Stack:XXXX cannot be deleted as it is in use by StackXXXX
これが起こってしまうのは大体以下のようなStackを実装した場合だと思います(コード全体)。
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',
})
}
}
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,
})
}
}
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の機能を利用して内部的に処理しているようです。
これはCDKを実装しているときには特に意識することはないのですが、例えばStackA
を以下のように変更したときに面倒なことになります。
...
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が以下のように変わるはずです(単に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
に以下を加えました。
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)の辻褄を合わせることで悲しい悲劇を回避することができるので同じ問題に困った時はぜひ試してみてください!