CDKのStageを試してみる

まずCDK initした素の状態から一部アンコメントしてSQSだけ作成し、デプロイ
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { CdkStageTestStack } from '../lib/cdk-stage-test-stack';
const app = new cdk.App();
new CdkStageTestStack(app, 'CdkStageTestStack', {
env: { account: '123456789012', region: 'ap-northeast-1' },
});
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as sqs from 'aws-cdk-lib/aws-sqs';
export class CdkStageTestStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const queue = new sqs.Queue(this, 'CdkStageTestQueue', {
visibilityTimeout: cdk.Duration.seconds(300)
});
}
}
$ cdk deploy
コンストラクトツリーの状態

ステージを作成
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Stage } from 'aws-cdk-lib';
import { CdkStageTestStack } from './cdk-stage-test-stack';
// Define the stage
export class MyAppStage extends Stage {
constructor(scope: Construct, id: string, props?: cdk.StageProps) {
super(scope, id, props);
// Add both stacks to the stage
new CdkStageTestStack(this, 'CdkStageTestStack');
}
}
appファイルbin/cdk-stage-test.ts
のスタック生成箇所を削除し、ステージ生成に変更
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { MyAppStage } from '../lib/my-stage';
const app = new cdk.App();
// Create the development stage
new MyAppStage(app, 'Dev', {
env: {
account: '123456789012',
region: 'ap-northeast-1'
}
});
// Create the production stage
new MyAppStage(app, 'Prod', {
env: {
account: '098765432109',
region: 'ap-northeast-1'
}
});
スタック一覧
$ cdk list
Dev/CdkStageTestStack (Dev-CdkStageTestStack)
Prod/CdkStageTestStack (Prod-CdkStageTestStack)
デプロイ
対象を指定しないと怒られる
$ cdk deploy
Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`
Stacks: Dev/CdkStageTestStack · Prod/CdkStageTestStack
Dev環境のみデプロイ
$ cdk deploy Dev/*
そもそも別のスタックとして作成される
CdkStageTestStackが最上位の階層に作成され、ツリーが一階層深くなる

Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify
--all
対象を指定しなかったとき↑のように怒られたので、--all
付けたけど失敗した
$ cdk deploy --all
No stack found in the main cloud assembly. Use "list" to print manifest

同じスタックを環境毎に使いまわしたい場合、bin/app.ts で静的/動的に複数作ったり分岐したりするよく見るパターンか、CDK の Stage を活用するパターンがありそうですが、どのようなケースで使い分けると良いのでしょうか?また、CDK の Stage を活用した実装で困った事はありますか?逆もあれば是非聞きたいです。
質問↑投げてたら回答↓返ってきてた
StackProps にはstackNameというプロパティがあるため、これを明示的に指定する(今までのスタック名を指定する)と、スタック名に${stageName}-というプレフィックスが付くのを防ぐことができ、スタックの新規作成にならず今までのスタックを維持することができます。
興味深い記載があったので検証する

Stageで作成したスタックを削除
$ cdk destroy Dev/CdkStageTestStack

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Stage } from 'aws-cdk-lib';
import { CdkStageTestStack } from './cdk-stage-test-stack';
// Define the stage
export class MyAppStage extends Stage {
constructor(scope: Construct, id: string, props?: cdk.StageProps) {
super(scope, id, props);
// Add both stacks to the stage
new CdkStageTestStack(this, 'CdkStageTestStack', {
+ stackName: `CdkStageTestStack`,
});
}
}
スタック一覧
()内のスタック名が変わっている
$ cdk ls
Dev/CdkStageTestStack (CdkStageTestStack)
Prod/CdkStageTestStack (CdkStageTestStack)
デプロイ
$ cdk deploy Dev/*
元の同名スタックが更新され、ツリーの親にスタックが増えている
cdk:path以外は変更されていない(リソースの置換は発生していない)

Stageを後から導入しても問題無い事は分かった
Stageをやめても問題無いか確認したい
Stageを利用していなかった状態↓にコードbin/cdk-stage-test.ts
を戻して再度デプロイ
$ cdk deploy
コンストラクトツリーは元に戻った
変更セットもcdk:pathを戻しただけで置換は発生していない
問題無さそう

参考

Stageからenv以外のparameterをpropsでStackに渡すことは出来るのか?

StageのInitializerにも、StagePropsにも任意の値が入り込める余地は無さそう

Stageのコンストラクタをインターセクション型(Intersection Types)で拡張すれば Props 渡し出来た!
import { Environment } from "aws-cdk-lib";
export interface AppParameter {
env: Environment;
envName: string;
visibilityTimeout?: number;
TopicName?: string;
}
export const devParameter: AppParameter = {
env: {
account: "123456789012",
region: "ap-northeast-1",
},
envName: "Dev",
visibilityTimeout: 100,
TopicName: "DevTopic",
};
export const prodParameter: AppParameter = {
env: {
account: "098765432109",
region: "ap-northeast-1",
},
envName: "Prod",
visibilityTimeout: 500,
TopicName: "ProdTopic",
};
import * as cdk from 'aws-cdk-lib';
import { MyAppStage } from '../lib/my-stage';
import { devParameter, prodParameter } from './parameter';
const app = new cdk.App();
new MyAppStage(app, 'Dev', {
env: devParameter.env,
parameter: devParameter,
});
new MyAppStage(app, 'Prod', {
env: prodParameter.env,
parameter: prodParameter,
});
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { AppParameter } from '../bin/parameter';
import { CdkStageTestStack } from './cdk-stage-test-stack';
import { HogeStack } from './hoge-stack';
export class MyAppStage extends cdk.Stage {
+ constructor(scope: Construct, id: string, props: cdk.StageProps & { parameter: AppParameter }) {
super(scope, id, props);
new CdkStageTestStack(this, 'CdkStageTestStack', {
env: props.env,
parameter: props.parameter,
stackName: `CdkStageTestStack`,
});
new HogeStack(this, 'HogeStack', {
env: props.env,
parameter: props.parameter,
});
}
}
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import { AppParameter } from '../bin/parameter';
interface CdkStageTestStackProps extends cdk.StackProps {
parameter: AppParameter;
}
export class CdkStageTestStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: CdkStageTestStackProps) {
super(scope, id, props);
const queue = new sqs.Queue(this, 'CdkStageTestQueue', {
visibilityTimeout: cdk.Duration.seconds(props.parameter.visibilityTimeout || 300),
});
new cdk.CfnOutput(this, 'EnvironmentName', {
value: props.parameter.envName,
description: 'The environment name for this stack',
});
}
}
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as sns from 'aws-cdk-lib/aws-sns';
import { AppParameter } from '../bin/parameter';
interface HogeStackProps extends cdk.StackProps {
parameter: AppParameter;
}
export class HogeStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: HogeStackProps) {
super(scope, id, props);
const topic = new sns.Topic(this, 'MyTopic', {
topicName: props.parameter.TopicName,
});
new cdk.CfnOutput(this, 'EnvironmentName', {
value: props.parameter.envName,
description: 'The environment name for this stack',
});
}
}

stageとstack両方にenvを指定していたが、Stage側での指定は不要だった
優先度はStack>Stageっぽい
Stackもしくは環境変数で欠落している(指定していない)アカウントやリージョン指定があれば、Stageの指定を使用する
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Stage.html#env
import * as cdk from 'aws-cdk-lib';
import { MyAppStage } from '../lib/my-stage';
import { devParameter, prodParameter } from './parameter';
const app = new cdk.App();
new MyAppStage(app, 'Dev', {
- env: devParameter.env,
parameter: devParameter,
});
new MyAppStage(app, 'Prod', {
- env: prodParameter.env,
parameter: prodParameter,
});
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { AppParameter } from '../bin/parameter';
import { CdkStageTestStack } from './cdk-stage-test-stack';
import { HogeStack } from './hoge-stack';
export class MyAppStage extends cdk.Stage {
constructor(scope: Construct, id: string, props: cdk.StageProps & { parameter: AppParameter }) {
super(scope, id, props);
new CdkStageTestStack(this, 'CdkStageTestStack', {
env: props.env,
parameter: props.parameter,
stackName: `CdkStageTestStack`,
});
new HogeStack(this, 'HogeStack', {
env: props.env,
parameter: props.parameter,
});
}
}

stageNameに値を指定するとidとは別にステージ名を設定出来る
import * as cdk from 'aws-cdk-lib';
import { MyAppStage } from '../lib/my-stage';
import { devParameter, prodParameter } from './parameter';
const app = new cdk.App();
new MyAppStage(app, 'Dev', {
+ stageName: '',
parameter: devParameter,
});
new MyAppStage(app, 'Prod', {
+ stageName: '',
parameter: prodParameter,
});
これを使うと、スタック呼び出し時にstackNameを指定しなくてもスタック名接頭にstageのidが付かなくなる
ただし、この方法でもコンストラクトIDにstageのidが上位パスとして追加されるのは変更不可
$ cdk ls
Dev/CdkStageTestStack (CdkStageTestStack)
Dev/HogeStack (HogeStack)
Prod/CdkStageTestStack (CdkStageTestStack)
Prod/HogeStack (HogeStack)

StageかStackどちらかでNameを指定する方法で既存スタックからStageを追加・削除した場合でもStackの置き換えや再作成は発生しない。
ただし、L2,L3コンストラクトが自動でStackを作成している場合はこの影響を回避できない
例えば、CloudFrontのEdgeFunctionは、本来バージニアリージョン用Stackとして別で定義しなければいけないLambda@EdgeをCloudFrontと一緒に東京リージョン用のStackに定義する事ができるが、内部ではバージニアリージョンにデフォルトではedge-lambda-stack-${region}
というCFnStackを自動で作成して対応する
これを使用している状態でStageを導入するとコンストラクトIDの変更に伴う差分がどうしても発生してしまう
EdgeFunction、stackIdを追加して既存Stack名を指定すると、置き換わるリソースを半分くらいには減らせる
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
const lambda = new cloudfront.experimental.EdgeFunction(this, 'LambdaEdge', {
.
.
.
+ stackId: 'edge-lambda-stack-xxxxxxxxxxxxxxxxx',
});
他にも、L2コンストラクトのaws_ec2.Instanceでも、自動でLaunchTemplateからインスタンスが生成されるが、Stageを導入することでコンストラクトIDの変更からLaunchTemplateNameが置き換わってしまい、LaunchTemplateと、EC2インスタンスも再作成されてしまう
これはL1のCfnInstanceを使ってLaunchTemplateNameを指定すれば防げるが、手間
import * as ec2 from 'aws-cdk-lib/aws-ec2';
const ec2Instance = new ec2.Instance(this, 'Ec2', {
.
.
.
});
また、コンストラクトIDを用いているシークレット名も変更されるため、AWS Secrets Managerに自動登録されるRDSのパスワード再作成されて、パスワードが変わってしまう