概要
CDKでスタックに渡す外部パラメータはTypeScriptで定義すると型が付いてよかったりします。
そこで、「ついでに型と一緒に制約もかけてバリデーションしちゃおう」という思いつきでやってみたら意外とよかった話です。
目次
前提
CDKはTypeScriptで書いており、v2を使用しております。
❯ cdk --version 2.31.0 (build b67950d)
また実現の仕方にフォーカスするため、メインでない部分のファイル・コードなどは省略しています。
まとめ
やること
- スタックの外からパラメータ(config)をCDKスタックに渡す
- パラメータはcdk.jsonや環境変数ではなく、TypeScriptファイルで定義することで型の保証をする
- その際にパラメータには、型だけでなく制約もつけてバリデーションもする
- バリデーションにはZodを使い、Zodスキーマから作った型をパラメータに当てる
良いこと
- 型保証だけでなく、バリデーションも一緒に出来てより安全に!
- パラメータ用に別で型を定義せず、バリデーション用に作った型をそのまま使い回せる!
コード
メインとなる4ファイルを例として取り上げます。
- ①Zodスキーマ(制約ルール・型)
- ②外部パラメータ定義
- ③バリデータークラス
- ④スタッククラス
依存関係としては、以下のような感じです。
- ④ -> ①②③
- ③ -> ①②(※直接②には依存しないが、実質②をバリデートするため)
- ② -> ①
①Zodスキーマ(制約ルール・型)
Zodというバリデーションモジュールがあるのですが、文字数・オプショナルから、URLやEmailの定義まで可能です。
また今回の例では、cron式の制約もかけております。(ちょっと雑な書き方ですがあくまでサンプルとして・・・)
そして、Zodスキーマからtypeを生成し、それを後述の外部パラメータ用のtypeとして使い回します。
import { z } from "zod"; export const StackInputSchema = z.object({ incomingWebhookUrl: z.string().url().min(1), scheduleExpression: z.string().startsWith("cron(").endsWith(")"), // cron式 senderAddress: z.string().email().min(1), }); // 【スキーマからtypeを生成】 export type StackInput = z.infer<typeof StackInputSchema>;
②外部パラメータ定義
StackProps
を拡張(継承)したinterfaceを定義することで、CDKスタックに型付きパラメータを注入することができます。
先程Zodスキーマからtypeを生成したので、その型を使い回して外部パラメータに型を当てています。
import { StackProps } from "aws-cdk-lib"; import { StackInput } from "./types/stack-input"; export interface ConfigStackProps extends StackProps { config: StackInput; // <- 【ここ】 } // 【パラメータ自体をここで定義】 export const configStackProps: ConfigStackProps = { env: { region: "ap-northeast-1", }, config: { incomingWebhookUrl: "https://hooks.slack.com/services/********", scheduleExpression: "cron(0 4 ? * MON-FRI *)", senderAddress: "***@***.***", }, };
③バリデータークラス
こちらはバリデーションを行うクラスです。
Zodのparseという機能(StackInputSchema.parse
)を使うことで、Zodスキーマで定義した制約に違反する(バリデートエラー)場合はエラーを吐くことができます。
import { IValidation } from "constructs"; import { StackInput, StackInputSchema } from "../types/stack-input"; export class StackValidator implements IValidation { private stackInput: StackInput; constructor(stackInput: StackInput) { this.stackInput = stackInput; } public validate(): string[] { const errors: string[] = []; try { StackInputSchema.parse(this.stackInput); } catch (e) { errors.push(JSON.stringify(e)); } return errors; } }
このStackValidatorクラスはIValidation
というinterfaceを実装していますが、これはCDKのStackクラスでaddValidation
というメソッドを実行してバリデートを行うために使われるものになります。
addValidationに関しては以下の記事に詳細を書いているので、良かったらぜひご覧ください。
またここではZodによるバリデーションだけでなく、オリジナルのルール・処理に基づいたチェックを実装するのも可能なので、Zodの制約とまとめてハンドリングができてなかなか便利です。
- 例
- cronの時間が深夜帯でないとエラーにする
- 指定するメールアドレスのドメインを制限する
- 特定のパラメータの組み合わせがNGなケースを作る
④スタッククラス
constructorで外部パラメータをpropsから拾ってStackValidatorインスタンスを生成し、addValidationメソッドに渡してあげることで、Zodによる制約のバリデートを実行することができます。
このパターンでバリデートを実装すると、定義したバリデーションがcdk synth時に行われます。
// import省略 export class SampleStack extends Stack { // 【スキーマから作成したtypeのパラメータを変数に】 private stackInput: StackInput; // 【外部パラメータファイルで定義したConfigStackPropsをpropsに当てる】 constructor(scope: Construct, id: string, props: ConfigStackProps) { super(scope, id, props); this.init(props); this.create(); } private init(props: ConfigStackProps): void { this.stackInput = props.config; // 【ここでバリデーションルールがCDKレイヤーに適用される】 const stackValidator = new StackValidator(this.stackInput); this.node.addValidation(stackValidator); } private create() { // 省略
補足
こんな感じで外部パラメータを、型だけでなく制約までつけてより安全にすることができました。
テンプレ化
④のバリデータークラスは、パラメータ自体をStackInput内にカプセル化していて具体的なパラメータの種類に依存しない書き方ができているため、今後CDKを書く場合に、テンプレとして使い回す(コピペ)こともできます。
バリデーション方法
今回StackValidatorクラスを作成してaddValidationというCDKの機能を使ってバリデーションをしていますが、これらを使わず直接スタッククラスのコンストラクタでZodのparseを行うことで制約チェックをすることも可能です。
ただaddValidationで使うIValidationでは、違反した複数のエラーを全て出力出来る(詳細は上記で紹介した記事を参照)ので、コンストラクタで自前でErrorハンドリングするより開発体験が向上するのでおすすめです。
バリデーションのために用意された機能を使うことで、公式のレールに乗っかるという事にもなりそれがまた良かったり。
またバリデーション処理をスタッククラスから切り出すことで、スタックの見通しも良くなり保守性も向上する点もメリットかなと思います。
参考記事
実は内容がかなり似ているのですが、今回思い付いた案のもとになった記事です。(こちらは外部パラメータの扱いには触れておらず、あくまでZodでCDKをバリデートすることのみに焦点を当てています。)
この記事では、実際にバリデートした際のエラーの出力結果なども載せていますので、ご興味があればぜひ。
最後に
CDKで外部パラメータに型をつける。制約をかけてバリデーションする。そしてバリデーションで使った型を外部パラメータに使い回す。
この循環の組み合わせを思いついて早速試したらいい感じだったので、記事にしてみました。
あまり前例がない方法かとは思いますが、一例としてご参考にしていただけたら幸いです。