Open15

CDKのStageを試してみる

hirenhiren

まずCDK initした素の状態から一部アンコメントしてSQSだけ作成し、デプロイ

bin/cdk-stage-test.ts
#!/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' },
});
lib/cdk-stage-test-stack.ts
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

コンストラクトツリーの状態

hirenhiren

ステージを作成

lib/my-stage.ts
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のスタック生成箇所を削除し、ステージ生成に変更

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が最上位の階層に作成され、ツリーが一階層深くなる

hirenhiren

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
hirenhiren

同じスタックを環境毎に使いまわしたい場合、bin/app.ts で静的/動的に複数作ったり分岐したりするよく見るパターンか、CDK の Stage を活用するパターンがありそうですが、どのようなケースで使い分けると良いのでしょうか?また、CDK の Stage を活用した実装で困った事はありますか?逆もあれば是非聞きたいです。

質問↑投げてたら回答↓返ってきてた

https://go-to-k.hatenablog.com/entry/cdk-stage-and-dynamic-static-stack

StackProps にはstackNameというプロパティがあるため、これを明示的に指定する(今までのスタック名を指定する)と、スタック名に${stageName}-というプレフィックスが付くのを防ぐことができ、スタックの新規作成にならず今までのスタックを維持することができます。

興味深い記載があったので検証する

hirenhiren

Stageで作成したスタックを削除

$ cdk destroy Dev/CdkStageTestStack
hirenhiren
lib/my-stage.ts
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以外は変更されていない(リソースの置換は発生していない)

hirenhiren

Stageを後から導入しても問題無い事は分かった
Stageをやめても問題無いか確認したい

Stageを利用していなかった状態↓にコードbin/cdk-stage-test.tsを戻して再度デプロイ
https://zenn.dev/link/comments/82a310689cc08c

$ cdk deploy

コンストラクトツリーは元に戻った

変更セットもcdk:pathを戻しただけで置換は発生していない

問題無さそう

hirenhiren

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

hirenhiren

Stageのコンストラクタをインターセクション型(Intersection Types)で拡張すれば Props 渡し出来た!

bin/parameter.ts
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",
};
bin/cdk-stage-test.ts
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,
});
lib/my-stage.ts
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,
    });
  }
}
lib/cdk-stage-test-stack.ts
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',
    });
  }
}
lib/hoge-stack.ts
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',
    });
  }
}
hirenhiren

stageとstack両方にenvを指定していたが、Stage側での指定は不要だった
優先度はStack>Stageっぽい

Stackもしくは環境変数で欠落している(指定していない)アカウントやリージョン指定があれば、Stageの指定を使用する
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Stage.html#env

bin/cdk-stage-test.ts
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,
});
lib/my-stage.ts
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,
    });
  }
}
hirenhiren

stageNameに値を指定するとidとは別にステージ名を設定出来る
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Stage.html#stagename

bin/cdk-stage-test.ts
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)
hirenhiren

StageかStackどちらかでNameを指定する方法で既存スタックからStageを追加・削除した場合でもStackの置き換えや再作成は発生しない。
ただし、L2,L3コンストラクトが自動でStackを作成している場合はこの影響を回避できない

例えば、CloudFrontのEdgeFunctionは、本来バージニアリージョン用Stackとして別で定義しなければいけないLambda@EdgeをCloudFrontと一緒に東京リージョン用のStackに定義する事ができるが、内部ではバージニアリージョンにデフォルトではedge-lambda-stack-${region}というCFnStackを自動で作成して対応する

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront.experimental.EdgeFunction.html

これを使用している状態で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のパスワード再作成されて、パスワードが変わってしまう