いつかまたパタゴニアに

主にソフトウェア開発周りで気づいたことなどをまとめています

Stageを活用したCDKスタック定義

AWS CDK Advent Calendar 2024 25日目です!

はじめに

CDKでアプリケーション定義を行う際に、開発/本番など環境毎に分けてデプロイを行うケースは非常に多いと思います。 環境の切り替え方法としてよくあるのは、contextなどで環境名を渡し、スタックへの引数とするケースです。

実装イメージ

const app = new cdk.App();
const stage = app.node.tryGetContext('stage') || 'dev';
new MyStack(app, `MyAppStack-${stage}`);

コマンド

npx cdk deploy --context stage=prod

このようなスタックレベルでの切り替えとは別に、CDKが提供するStageクラスを用いることもできます。 Stageを活用する記事が意外とヒットしなかったので、自分なりのやり方をまとめてみようと思います。

皆様のご意見もお待ちしています!

Stageのキホン

まずは基本の使い方です。アプリ用Stageをlib/stage.tsに作成し、Stack定義を並べていきます。

import { Stage, StageProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import { MainStack } from "./main-stack";

export class AwesomeStage extends Stage {
  constructor(scope: Construct, id: string, props?: StageProps) {
    super(scope, id, props);

    // 今回はMainStackだけ定義
    new MainStack(this, "MainStack");
  }
}

bin/app.tsにて、環境ごとのStageを定義します。

import { AwesomeStage } from "../lib/stage";

const app = new App();

// 開発環境
const devStage = new AwesomeStage(app, "Dev");
// 本番環境
const prodStage = new AwesomeStage(app, "Prod");

たったのこれだけです。

それでは、開発環境(Dev stage)をデプロイしてみましょう。以下のようにDev/MainStackを指定してデプロイします。

❯ npx cdk deploy Dev/MainStack
arn:aws:cloudformation:ap-northeast-1:123456789012:stack/Dev-MainStack/2b51a110-91db-11ef-bec2-0a932978447d

✨  Total time: 56.82s

無事にDev/MainStackがデプロイされました。

Dev Stage内に複数スタック定義がある場合、Dev/*で全てデプロイ出来ます。実際にはこちらの方が活用例が多そうですね。

❯ npx cdk deploy Dev/*
arn:aws:cloudformation:ap-northeast-1:123456789012:stack/Dev-MainStack/2b51a110-91db-11ef-bec2-0a932978447d

✨  Total time: 56.82s

デプロイに失敗するケース

以下の場合、デプロイに失敗します。

Stage指定を行わない

❯ npx cdk deploy
Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`
Stacks: Dev/MainStack

複数stageの一括デプロイとはならないのですね。

--allで指定

上記エラーメッセージを信じて--allでデプロイするとコケます。

❯ npx cdk deploy --all

No stack found in the main cloud assembly. Use "list" to print manifest

スタックあるはずなんですが....

Stage名*を指定

❯ npx cdk deploy Dev*

No stacks match the name(s) Dev*

Dev/*のスラッシュを忘れないようにしましょう。

環境毎のパラメータ設定

Stageで環境を分けると、環境毎に異なるパラメータを渡したくなります。 パラメータの渡し方は色々ありますが、以下のようにTypeScriptファイルに定義をまとめ、型安全に引っ張り出したいですよね。

パラメータの取得イメージ

const config = getConfig(stageName);

そんなgetConfigの実装例

const environments = ["Dev", "Prod"] as const;
export type Environment = (typeof environments)[number];

type ConfigParameters = {
  vpcId: string;
  cpu: number;
};

interface ISystemConfig {
  getSystemConfig(): ConfigParameters;
}

class DevelopConfig implements ISystemConfig {
  getSystemConfig(): ConfigParameters {
    return {
      vpcId: "vpc-012345",
      cpu: 256,
    };
  }
}

class ProductionConfig implements ISystemConfig {
  getSystemConfig(): ConfigParameters {
    return {
      vpcId: "vpc-abcdef",
      cpu: 2048,
    };
  }
}

const SystemMap = new Map<Environment, ISystemConfig>([
  ["Dev", new DevelopConfig()],
  ["Prod", new ProductionConfig()],
]);

export const getSystemConfig = (environment: Environment): ConfigParameters => {
  const system = SystemMap.get(environment);
  if (system == null) {
    throw new Error(`Invalid environment, got: ${environment}`);
  }
  return system.getSystemConfig();
};

CDKにおいてはbin/app.tsでパラメータ取得&Stackに渡してデプロイ内容を切り替えるのが王道ですが、今回はStage内でパラメータを取得してみます。 すると、以下の記述だけで各環境毎のパラメータ切り替えが実現できます。

bin/app.tsの実装

// 開発環境
const devStage = new AwesomeStage(app, "Dev");
// 本番環境
const prodStage = new AwesomeStage(app, "Prod");

stage.tsの実装

import { Stage, StageProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import { MainStack } from "./main-stack";
import { Environment, getSystemConfig } from "./config";

export class AwesomeStage extends Stage {
  // idの型をstringではなくEnvironment("Dev" or "Prod")にする
  // これにより、AwsomeStageクラスを呼び出すときにDevやProd以外の文字列を弾くことができる
  constructor(scope: Construct, id: Environment, props?: StageProps) {
    super(scope, id, props);

    // Stage毎に異なる設定を取得する 
    // idとしてbin/app.tsで"Dev"または"Prod"が渡されるので、それをもとにパラメータを取得する
    const config = getSystemConfig(id);

    // パラメータをStackへの引数として渡す
    new MainStack(this, "MainStack", {
      vpcId: config.vpcId,
      }
    );
  }
}

特定環境のパラメータ取得&適用をstage定義に押し込むことができるので、bin/app.tsがすっきりして良いなと思っています。

(まあ、この作業をstage定義の中で行うか、bin/app.tsで行うかの違いだけなんですが、、、)

Stage配下のConstruct内でパラメータを取得したいとき

CDK Appの基本原則からは外れますが、なんらかの事情でConstruct内から環境に対応したパラメータを取得したくなることもあるかもしれません。 そんなときは、Stage.of(this).stageNameからstage名を取得することができます。

const config = getSystemConfig(Stage.of(this)?.stageName as Environment);

さいごに

環境ごとのCDKアプリケーションのデプロイ内容切り分けについて、Stageクラスの活用例を紹介してみました。 公式ドキュメントでの実装例も謎にPipelinesのStage載ってたりと情報不足感が否めないので、ぜひ皆さんの素敵な使い方をコメントいただけると嬉しいです。

docs.aws.amazon.com

あっという間に年末ですね!この記事を読んで頂いた方が、来年も素敵な一年を過ごせることを願っています。 メリークリスマス!