TypeScriptのAWS CDKで設定値(Config)を渡す実装方法を考えてみた

AWS CDKではシステム環境(dev,stg,prdなど)の環境ごとに設定値を切り替える方法が何パターンかありますよね。今回TypeScriptを使う際にできるだけ型の恩恵を受ける実装パターンを考えてみました。
2023.05.03

こんにちは。AWS事業本部コンサルティング部に所属している今泉(@bun76235104)です。

AWS CDKを使う時の設定値(Config)の渡し方に迷ったことはありませんか?

私は何度もあります。

以下記事では弊社の佐藤が設定値の渡し方について2つの方法を提示してくれています。

私としても2. 外部ファイルの利用(TSファイル)による方法に以下のようなメリットを感じています。

  • TypeScriptの型の恩恵を受けることができる
  • プログラミング言語によるインフラのプロビジョニングができるAWS CDKにおいて、TSファイルを使う方法は相性が良い(と私は思っています)
    • アプリケーション側の知見をそのままIaCに活かすことができる
    • 無理にJSONファイルやymlファイルにまとめる必要がない

ということで今回シンプルな実装パターンとソースコードに書き込みたくない情報がある場合の実装パターンを考えました。

なお、今回実装した2パターンは以下リポジトリに格納しているため全文を確認したい方はご参照ください。

シンプルなパターン

このパターンは以下のようにシンプルに config.tsを定義します。

config.ts

const arrayEnvironments = ["dev", "prd"] as const;
type Environment = (typeof arrayEnvironments)[number];

type ConfigParameters = {
  VpcProp: {
    Cidr: string;
  };
};

interface ISytemConfig {
  getSystemConfig(): ConfigParameters;
}

class DevConfig implements ISytemConfig {
  getSystemConfig(): ConfigParameters {
    return {
      VpcProp: {
        Cidr: "10.10.0.0/16",
      },
    };
  }
}

class PrdConfig implements ISytemConfig {
  getSystemConfig(): ConfigParameters {
    return {
      VpcProp: {
        Cidr: "10.20.0.0/16",
      },
    };
  }
}

const SystemMap = new Map<Environment, ISytemConfig>([
  ["dev", new DevConfig()],
  ["prd", new PrdConfig()],
]);

export const getSystemConfig = (env: Environment): ConfigParameters => {
  const system = SystemMap.get(env);
  if (system == null) {
    throw new Error("Please specify like `cdk deploy -c env=dev``");
  }
  return system.getSystemConfig();
};

そして以下のように利用する側のファイルから呼び出します。

app.ts

import { getSystemConfig } from "./config";

const env = app.node.tryGetContext("env") || "dev";
const config = getSystemConfig(env);

TypeScriptの型の恩恵を受けながらも、比較的シンプルにまとめる方法です。

この方法ではcdkコマンド実行時のコマンドライン引数によりシステム環境ごとの設定値を取得します。

何も指定しなければdevとなるようになっていて、npx cdk -c env=prdのようにコンテキストとしてシステム環境を切り替えることを想定しています。

※ コマンドライン引数や環境変数による動作の切り替えを好まない場合、Stackやappを明示的に分けるという方法も考えられるかもしれませんが、今回は特に触れておりません。

ソースコードに書きたくない情報を設定値として持たせるパターン(SSM ParameterStoreを利用)

次に上記の方法をベースに「設定値にGit管理したくない(ソースコードに埋め込みたくない)情報や、他のシステムからSSM ParameterStoreを介して受け渡す情報がある場合」を考えてみました。

前提としてParameterStoreは以下のように命名規則を定義しているとします。

  • ${projectName}/${env}/${ParmeterName}
      • sampleProject/dev/DB_PASSWORD
      • sampleProject/prd/DB_PASSWORD

まず最初に、ParameterStoreから値を取得するためのクラスを用意してみました。

secretParameter.ts

import { SSM } from "@aws-sdk/client-ssm";
import { Environment } from "./configSecret";

export class SecretParameter {
  readonly env: Environment;
  readonly project: string;
  readonly region: string;
  readonly client: SSM;

  constructor(env: Environment, project: string, region: string) {
    this.env = env;
    this.project = project;
    this.region = region;
    this.client = new SSM({ region: this.region });
  }

  async getSecureString(name: string): Promise<string> {
    const path = `/${this.project}/${this.env}/${name}`;
    const params = {
      Name: path,
      WithDecryption: true,
    };
    const response = await this.client.getParameter(params);
    if (response.Parameter?.Value == undefined) {
      throw new Error(`Parameter Store ${path} is not found`);
    }
    return response.Parameter.Value;
  }

  async getStringList(name: string): Promise<string[]> {
    const path = `/${this.project}/${this.env}/${name}`;
    const params = {
      Name: path,
      WithDecryption: true,
    };
    const response = await this.client.getParameter(params);
    if (response.Parameter?.Value == undefined) {
      throw new Error(`Parameter Store ${path} is not found`);
    }
    return response.Parameter.Value.split(",");
  }
}

なお、今回ParameterStoreから値を取得するためにaws-sdkを利用しています。

aws-cdkのクラスを使って値を取得できるのですが、SecureString(安全な文字列)の取り扱いが微妙そう(バージョンの指定など)だったので、このようにしました。

参考:AWS CDKでAWS Systems Manager パラメータストア及びAWS Secrets Managerからパラメータを取り込む方法 | DevelopersIO

このクラスを利用するようにconfig.tsを改修します。

config.ts

// 機密情報をパラメーターストアから取得することを意識したパターン
import { SecretParameter } from "./secretParameter";

const arrayEnvironments = ["dev", "prd"] as const;
export type Environment = (typeof arrayEnvironments)[number];

export type ConfigParameters = {
  VpcProp: {
    Cidr: string;
    MaxAzs: number;
  };
};

interface ISytemConfig {
  readonly env: Environment;
  readonly secretParameter: SecretParameter;
  getSystemConfig(): Promise<ConfigParameters> ;
}

class DevConfig implements ISytemConfig {
  readonly env = "dev"
  readonly secretParameter: SecretParameter;

  constructor(project: string, region: string) {
    const secretParameter = new SecretParameter(this.env, project, region);
    this.secretParameter = secretParameter;
  }

  async getSystemConfig(): Promise<ConfigParameters> {
    const maxazs = await this.secretParameter.getSecureString("MaxAzs");
    return {
      VpcProp: {
        Cidr: "10.10.0.0/16",
        // 本来機密情報にはならないパラメーターだと思いますが、ParameterStoreを使いたかっただけです
        MaxAzs: Number(maxazs),
      },
    };
  }
}

class PrdConfig implements ISytemConfig {
  readonly env = "prd"
  readonly secretParameter: SecretParameter;

  constructor(project: string, region: string) {
    const secretParameter = new SecretParameter(this.env, project, region);
    this.secretParameter = secretParameter;
  }

  async getSystemConfig(): Promise<ConfigParameters> {
    const maxazs = await this.secretParameter.getSecureString("MaxAzs");
    return {
      VpcProp: {
        Cidr: "10.20.0.0/16",
        MaxAzs: Number(maxazs),
      },
    };
  }
}

const SystemMap = new Map<Environment, ISytemConfig>([
  ["dev", new DevConfig("config-sample", "ap-northeast-1")],
  ["prd", new PrdConfig("config-sample", "ap-northeast-1")],
]);

export const getSystemConfig = async (env: Environment): Promise<ConfigParameters>  => {
  const system = SystemMap.get(env);
  if (system == null) {
    throw new Error("Please specify like `cdk deploy -c env=dev``");
  }
  return await system.getSystemConfig();
};

非同期処理となっている関係で呼び出し側も少し変更しています。

app.ts

import * as cdk from "aws-cdk-lib";
import { ConfigsSampleStack } from "./stacks/configSampleStack";
import { Construct } from "constructs";
import { getSystemConfig } from "./config";

async function main() {
  const app = new cdk.App();

  const env = app.node.tryGetContext("env") || "dev";
  const project = "config-sample";
  const region = "ap-northeast-1";

  const config = await getSystemConfig(env);
}

main();

これにより、npx cdk deploy -c env=devのようにコマンドを実行することで、無事システム環境ごとに設定値を渡すことができました。

なお、上記では省略していますがdev,prdなどのシステム環境ごとにCloudFormationのスタックを分けるにはスタックに渡すIDに留意しましょう。

  const sampleConfigStack = new ConfigsSampleStack(
    app,
    // dev,prdなど環境ごとにIDを使い分け
    `ConfigsSampleStack-${env}`,
    config,
    {
      env: {
        region: region,
        account: process.env.CDK_DEFAULT_ACCOUNT,
      },
    }
  );

さいごに

今回はAWS CDKをTypeScriptで利用するにあたり設定値(Config)を環境ごとに切り分ける実装パターンを考えてみました。

小規模なプロジェクトであれば、以下記事のcdk.jsonを利用する方法もありだと思います。

また、今回はParameterStoreから取得する値もその他の値も同列に扱いましたが、別途クラスを分けるなどの方法も考えられると思います。

より良い方法があれば追記・更新もしくは追加で記事を書くなど対応していこうかと思います。

この記事が少しでも参考になれば幸いです。以上今泉でした。