はじめに
皆様こんにちは、ソーシャル経済メディア「NewsPicks」(Media Infrastructureチーム)エンジニアの北見です。
AWS
における有力な IaaC
の選択肢として、CDK
が挙げられます。
今回は、私が CDK
におけるバッドプラクティスだと思っている「スタック間参照」について説明させて頂きます。
既に CDK
を運用している方にとっては釈迦に説法ですが、お付き合い頂けると嬉しいです。
スタック間参照すると何が問題になるか?
スタック間参照とは、以下の様にとあるスタックのリソースを別のスタックで参照することを指します。
例えば、デプロイ用のイメージがpushされる ECR
を管理する StorageStack
と、アプリケーションを動作させる基盤を管理する ApplicationStack
に分けたとして、両者をスタック間参照するコードを見てみましょう。
export class StorageStack : extends cdk.Stack { readonly applicationRepository: ecr.Repository; constructor(scope: Construct, id: string){ super(scope, id); this.applicationRepository = new ecr.Repository(this, "applicationEcr", { repositoryName: "application-ecr", removalPolicy: props.accountProps.storageRemovalPolicy, imageScanOnPush: true, }); } }
export class ApplicationStack : extends cdk.Stack { constructor(scope: Construct, id: string, applicationEcr: ecr.Repository){ super(scope, id); const taskDefinition = new ecs.FargateTaskDefinition(...) const applicationContainer = taskDefinition.addContainer( "applicationContainer", { image: ecs.ContainerImage.fromEcrRepository(applicationEcr), ... } ); }
const storageStack = new StorageStack(app, "StorageStack"); const applicationStack = new ApplicationStack(app, "ApplicationStack", storageStack.applicationRepository);
一見問題なさそうに思えるこのコードは、applicationRepository
リソースを削除したり名前を変更した場合に問題を引き起こします。
this.applicationRepository = new ecr.Repository(this, "applicationEcr", { // 変更後 repositoryName: "application-ecr-new", removalPolicy: props.accountProps.storageRemovalPolicy, imageScanOnPush: true, });
cdk deploy 23:41:12 | UPDATE_ROLLBACK_IN_P | AWS::CloudFormation::Stack | StorageStack Export StorageStack:ExportsOutputRefapplicationEcrXXXXXXXXXXXXXXXX cannot be deleted as it is in use by ApplicationStack
なんと cdk deploy できません。
CDK
のコード上では依存関係は無くなっているのですが、CDK
が自動で作っている Output
が削除されることとなり、 cdk deploy
に失敗してしまいます。
スタック間参照を行わないリソースの参照方法
では、そもそもどのようにスタック間参照を行わないようにリソースを管理・参照すれば良かったのでしょうか?
解決策1. 名前・IDを使ってリソースを参照する
CDK
にはリソースを参照するために、様々な関数が用意されています。
たとえば ECR
の場合、ecr.Repository.fromRepositoryName を使えば
export class ApplicationStack : extends cdk.Stack { constructor(scope: Construct, id: string){ super(scope, id); const taskDefinition = new ecs.FargateTaskDefinition(...) // fromRepositoryNameによる名前を使ったリソース参照 const applicationEcr = ecr.Repository.fromRepositoryName(this, "applicationEcr", "application-ecr") const applicationContainer = taskDefinition.addContainer( "applicationContainer", { image: ecs.ContainerImage.fromEcrRepository(applicationEcr), ... } ); }
のように、名前によるリソース参照が可能です。
この方法を使えば、スタック間参照による cdk deploy
の問題を引き起こしません。
もちろん、
applicationRepository
をシンボルセーフに参照することはできない命名ベースの参照になるので、ルール化による認知コストがかかる
というデメリットはあるのですが、cdk deploy
できなくなるよりはマシです。
解決策2. そもそもスタックを分割しない
「そもそも不必要にスタックを分割しない」というのも優れた解決方法の1つです。
const storageStack = new StorageStack(app, "Storage"); const applicationStack = new ApplicationStack(app, "Application", storageStack .applicationRepository);
例としてコードを示しておきながらですが、ECR
について、そもそもこの両者は分割する必要があるのでしょうか?
デプロイ環境ごとに対応する ECR
を置くことを前提とすれば、ECR
の管理は ApplicationStack
で行ってしまって良さそうです。
勿論 StorageStack
に対して ApplicationStack
が1対多の場合は分割した方が良いのですが、まずは「本当にスタック分割が必要になるか?」と問いかけることで「実は不要に分割してた」というパターンを極力減らすことができます。
もし現状でスタック間参照をしてしまっている場合の回避方法
とはいえスタック間参照の副作用に気づけず、現状の CDK
のコードがスタック間参照してしまっているケースもあるでしょう。
ここまででご紹介しているコードを例に、スタック間参照を引きはがす方法を説明します。
export class StorageStack : extends cdk.Stack { readonly applicationRepository: ecr.Repository; constructor(scope: Construct, id: string){ super(scope, id); this.applicationRepository = new ecr.Repository(this, "applicationEcr", { repositoryName: "application-ecr", removalPolicy: props.accountProps.storageRemovalPolicy, imageScanOnPush: true, }); } }
export class ApplicationStack : extends cdk.Stack { constructor(scope: Construct, id: string, applicationEcr: ecr.Repository){ super(scope, id); const taskDefinition = new ecs.FargateTaskDefinition(...) const applicationContainer = taskDefinition.addContainer( "applicationContainer", { image: ecs.ContainerImage.fromEcrRepository(applicationEcr), ... } ); }
const storageStack = new StorageStack(app, "Storage"); const applicationStack = new ApplicationStack(app, "Application", storageStack.applicationRepository);
1. StorageStack
側で exportValue を使い、コード変更してもスタック間の依存関係に変更がないかのように CDK
を騙す
export class StorageStack : extends cdk.Stack { readonly applicationRepository: ecr.Repository; constructor(scope: Construct, id: string){ super(scope, id); this.applicationRepository = new ecr.Repository(this, "applicationEcr", { repositoryName: "application-ecr", removalPolicy: props.accountProps.storageRemovalPolicy, imageScanOnPush: true, }); // 変更箇所。 // ECRではrepositoryArnとrepositoryNameを出力すればエラー回避できますが、リソースの種類によってexportValueする必要のある値は異なります。 this.exportValue(this.applicationRepository.repositoryArn); this.exportValue(this.applicationRepositoryrepositoryName); } }
これを追加することによって、StorageStack
の applicationRepository
の名前やIDを変更しても、Output
から削除されなくなる...と CDK
に認識させることができます。
2.リソースの参照を名前・ID解決する関数による方法で置き換える
次に ApplicationStack
内部で利用している applicationEcr
を名前解決する方法で取得するようにします。
export class ApplicationStack : extends cdk.Stack { // 変更箇所。StorageStackからECRを受け取らないように変更 constructor(scope: Construct, id: string){ super(scope, id); const taskDefinition = new ecs.FargateTaskDefinition(...) // 変更箇所。ecr.Repository.fromRepositoryNameを利用した、名前によるリソース解決 // ECRに限らず、CDKにはリソースを名前によって解決する便利な関数が多数実装されている const applicationEcr = ecr.Repository.fromRepositoryName(this, "applicationEcr", "application-ecr") const applicationContainer = taskDefinition.addContainer( "applicationContainer", { image: ecs.ContainerImage.fromEcrRepository(applicationEcr), ... } ); }
// 変更箇所。スタック間参照がなくなっている const storageStack = new StorageStack(app, "Storage"); const applicationStack = new ApplicationStack(app, "Application");
3. cdk deploy
する
この段階で一度 cdk deploy
する必要があります。
4. exportValue
を削除し、再度 cdk deploy
する
export class StorageStack : extends cdk.Stack { readonly applicationRepository: ecr.Repository; constructor(scope: Construct, id: string){ super(scope, id); this.applicationRepository = new ecr.Repository(this, "applicationEcr", { repositoryName: "application-ecr", removalPolicy: props.accountProps.storageRemovalPolicy, imageScanOnPush: true, }); } }
export class ApplicationStack : extends cdk.Stack { constructor(scope: Construct, id: string){ super(scope, id); const taskDefinition = new ecs.FargateTaskDefinition(...) const applicationEcr = ecr.Repository.fromRepositoryName(this, "applicationEcr", "application-ecr") const applicationContainer = taskDefinition.addContainer( "applicationContainer", { image: ecs.ContainerImage.fromEcrRepository(applicationEcr), ... } ); }
このコードを cdk deploy
すれば、スタック間参照がなくなった CDK
のコードが AWS
上に反映されます。
注意しなければいけないのは、StorageStack
で宣言している ECR
の名前が変更されたとき、ApplicationStack
の ecr.Repository.fromRepositoryName
も変更が必要という点です。
これはどうしても必要なので、諦めましょう。
おわりに
実はスタック間参照していても、問題なく運用出来ているケースがあるのかも知れません。
ですが、 cdk deploy
を失敗させる原因をわざわざ作るリスクを上回るメリットは現状ないと思っています。
回避策として exportValue
を紹介しましたが、CDK
を騙すのはそもそも本質的に価値ある作業ではないので、このようなワークアラウンドがないのが理想です。
こういった諸々の事情を考えると、例え拡張の予定がなかったとしても
「CDKでスタック間参照してはならない」
という合言葉が、シンプルで運用が楽になるルールなのではないでしょうか?
CDK
の運用経験が既にある方にとっては釈迦に説法な内容でしたが、「しない方が良いですよ」とまで踏み込んでいる記事はあまり見かけず、注意喚起のニュアンスも込めてお話させて頂きました。
ここまで読んで下さり、ありがとうございました。