OSS としての AWS CDK へのコントリビューションや CDK の Construct ライブラリの実装では、いわゆるアプリケーション開発の実装のお作法と少し異なる点があるのでそれについて書いてみました。
目次
想定読者
- AWS CDK コントリビューター
- 自作 Construct ライブラリ開発者
- TypeScript 以外の言語での使用に対応するために projen などのツールを使用して Construct Hub へ公開する人
- その他 AWS CDK のコアなファン
- 上記 2 点に興味がある人
- CDK 自体の仕組みや CDK のエコシステムに興味がある人
AWS CDK 実装の制約
CDK と TypeScript
AWS CDK はいくつかのプログラミング言語を用いて 実装することができる IaC ツールですが、TypeScript を使用して CDK 実装をするユーザーは非常に多いかと思われます。
というのも、OSS である AWS CDK 自体が TypeScript で実装されていたり、公式ドキュメントにも TypeScript のサンプルコードが多いなどの理由が挙げられます。(私個人としての観測範囲になりますが、登壇やブログなどコミュニティに出ている資料においても TypeScript を使用されているものが多い印象です。)
TypeScript ならではの実装方法は非推奨
TypeScript には便利な記法がいくつかあり、実際に CDK のコードを書く際にそれらを駆使して開発される方も多いかと思われます。
しかし、そんな TypeScript ならではの記法には、"CDK コントリビュートや自作 Construct の Construct Hub などへの公開において"非推奨とされているものがあります。
※あくまで上記の領域においての話であり、普段 CDK を使用してプロダクトの開発をされる場合はその限りではありません。ガンガン使って問題ありません。
例えば Union 型は、ある変数が複数の型(もしくは値)のいずれかを持つことを許可する型で、TypeScript で CDK を書かれる際によく使用される方も多いのではないでしょうか。
しかしこの Union 型は、CDK コントリビュートにおいて非推奨とされています。
export interface MyConstructProps { readonly myValue: 'VALUE_A' | 'VALUE_B'; readonly myType: MyTypeA | MyTypeB; } export class MyConstruct extends Construct { constructor(scope: Construct, id: string, props: MyConstructProps) { // ... } }
Union 型の代替案
Union 型が CDK コントリビュートにおいて非推奨な理由は後ほど説明しますが、代替案としては以下のような「Union-Like Class」と呼ばれる実装テクニックで解決します。
※CDK のデザインガイドラインにも「Union 型は非推奨」な旨とその代替案が記載されています。
Union-Like Class の例:
- InlineCode、AssetCode、S3Code の 3 種類の型から成り立つ Union 型にしたい
- Code という abstract クラスを作成し、それらの型はこのクラスを継承させる
- その abstract クラスに、各クラスのインスタンスを返す fromXxx という static メソッドを作成する
fromXxxという命名でなくても OK
- Union 型として指定したいプロパティにその abstract クラスの型を指定し、fromXxx メソッドで各型のインスタンスを生成して渡す
export abstract class Code { public static fromInline(code: string): InlineCode { return new InlineCode(code); } public static fromAsset(assetPath: string, options?: s3_assets.AssetOptions): AssetCode { return new AssetCode(assetPath, options); } public static fromBucket(bucket: s3.IBucket, key: string, objectVersion?: string): S3Code { return new S3Code(bucket, key, objectVersion); } } export class InlineCode extends Code { // ... } export class AssetCode extends Code { // ... } export class S3Code extends Code { // ... } export interface MyConstructProps { readonly code: Code; // InlineCode | AssetCode | S3Code } new MyConstruct(scope, 'MyConstruct', { code: Code.fromAsset('...'), // AssetCode型を渡す場合 });
また、'VALUE_A' | 'VALUE_B' のような具体値から成る Union 型のものは、単にenum 型として定義することで代替可能です。
export enum MyEnum { VALUE_A = 'VALUE_A', VALUE_B = 'VALUE_B', } export interface MyConstructProps { readonly myValue: MyEnum; // 'VALUE_A' | 'VALUE_B' } new MyConstruct(scope, 'MyConstruct', { myValue: MyEnum.VALUE_A, });
※通常 TypeScript でアプリケーションを開発する際には、enum 型は使用しない・推奨されないことが多いですが、CDK コードにおいてはその限りではありません。その理由はここでは触れませんが、次章がヒントになるかと思います。
※CDK 実装 TIPS として「Enum-Like Class」というものもあります。ざっくりいうと、ただの enum 要素に加えてユーザーが任意の値を指定できるようにする CDK で頻出のパターン(もともと DDD の文脈でもあったり)です。以下をご覧下さい。
export class Cpu { /** * 1 vCPU */ public static readonly ONE_VCPU = new Cpu('1 vCPU'); /** * 2 vCPU */ public static readonly TWO_VCPU = new Cpu('2 vCPU'); /** * Custom CPU unit * * @param unit custom CPU unit */ public static of(unit: string): Cpu { return new Cpu(unit); } /** * * @param unit The unit of CPU. */ private constructor(public readonly unit: string) {} } new MyConstruct(scope, 'MyConstruct1', { cpu: Cpu.ONE_VCPU, }); new MyConstruct(scope, 'MyConstruct2', { cpu: Cpu.of('4 vCPU'), });
多言語対応ツール jsii
jsii とは
上記の章で、Union 型のような TypeScript の記法は、CDK コントリビュートなどにおいて非推奨とされていることを説明しました。
その理由を説明する前に、AWS CDK はいくつかのプログラミング言語で記述可能ですが、どのようにして多言語対応を実現しているのかを説明します。
具体的にいうと、TypeScript で書かれた AWS CDK 本体(もしくは Construct ライブラリ)を、どのようにして TypeScript 以外の言語でも使用できるようにしているのでしょうか。
その答えは、jsii というツールにあります。
jsii は AWS CDK において、TypeScript で定義されたコード(モジュール)を、他の言語のコードで使用できるように変換するために使われるツールです(AWS CDK 以外のコードにも使用できます)。
jsii の公式ドキュメントの例を挙げますが、以下のような TypeScript で定義したものを、各言語で使用できるように変換してくれるものになります。
- TypeScript
export class Greeter { public greet(name: string) { return `Hello, ${name}!`; } }
var greeter = new Greeter();
greeter.Greet("World"); // => Hello, World!
- Go
greeter := NewGreeter() greeter.Greet("World") // => Hello, World!
final Greeter greeter = new Greeter(); greeter.greet("World"); // => Hello, World!
const greeter = new Greeter(); greeter.greet('World'); // => Hello, World!
greeter = Greeter() greeter.greet("World") # => Hello, World!
TypeScript ならではの実装方法が非推奨な理由
上記の jsii の役割から察した方もいるかもしれませんが、TypeScript ならではの実装方法が他の言語では対応していない場合があるからです。
CDK はいくつかのプログラミング言語で記述可能ですが、例えば先ほど挙げた Union 型の例に関して述べると、Go などの言語では Union 型は存在しません。
この場合、jsii は各言語用に変換する際にエラーにするのではなく、非可逆な緩い型に変換します。
具体的には、TypeScript では複数クラスから成り立つ Union 型のものが、他の言語ではただのオブジェクト型や TypeScript でいう any のような型に変換されてしまいます。('VALUE_A' | 'VALUE_B' のような具体値から成る Union 型のものは string 型に変換されます。)
※厳密には「利用可能な最も汎用的な参照型」に変換されます。
※詳細は jsii のドキュメントを参照。
- TypeScript
public myMethod(myType: MyTypeA | MyTypeB) { ... }
public void myMethod(Object myType) { ... }
public void MyMethod(object myType) { ... }
- Go
func (this *Sample) myMethod(myType interface{}) { ... }
jsii の仕組み
AWS CDK のコードは、jsii によって TypeScript から他の言語への変換が行われるため、それを利用してユーザーは様々な言語で CDK を書くことができるというお話をしました。
では、それはどのような仕組みによって成り立っているのでしょうか。
まず、AWS CDK 本家や Construct ライブラリ側で定義された TypeScript の Construct コードが、jsii によって各言語用のライブラリに変換されます(これはユーザー側でなくライブラリ提供者側で行われています)。
そしてそれぞれの言語で実際に CDK コードを書く際、jsii によって生成されたライブラリを使用(インストール/インポート)し、各言語の記述方法に沿って CDK コード内でその Construct を呼び出します。
その際、各ライブラリ内には、AWS CDK 本家や Construct ライブラリ側で記述された Construct コード(実態は Javascript コード)がバンドルされていて、その Construct の実際の処理は各 CDK プロジェクトの実装言語のプロセスでなく、javascript のコードによる処理が実行されます。
つまりそれらのライブラリは、ユーザーが CDK プロジェクトで CDK コードを記述している各言語のホストとは別プロセスとして node(nodejs) ランタイムで実行されます。そのため、jsii ライブラリに依存するコードを実行するためには、node ランタイムが使用可能である必要があるのです。
※ CDK CLI の実行にそもそも node ランタイムが必要ではあるのですが、通常 CDK CLI を通さずに pytest などから実行できる CDK のユニットテストでも node ランタイムが必要になるということになります。

これは結局、先ほど述べたような 「Union 型を使わない」などの各言語での使用を想定してライブラリ作成者側が定義した型を通して、実際には Javascript 側の内部処理に値が渡されているだけになります。
この点から、その Construct への入力と出力の型が各言語で使用できる形になってさえいれば、実際の処理、つまり内部実装では各言語での使用を想定する必要はないということがわかります。
そのため、内部の処理に関しては TypeScript ならではの書き方をしても(基本的には)問題はありません。
(実際に、AWS CDK 本家の Construct コードでも、内部の処理には TypeScript ならではの書き方がされているケースがあります: ユーザー定義型ガード関数、ジェネリクス、etc...)
※jsii の仕組みの詳細(ランタイムアーキテクチャ)については jsii のドキュメントを参照。
まとめ
CDK コードを TypeScript で記述することが多いですが、CDK コントリビュート(AWS CDK 本家コード)や自作 Construct ライブラリを公開する際は、TypeScript ならではの実装方法は非推奨とされているお話をしました。
またその理由として、jsii という多言語対応ツールの存在とその仕組みについて述べました。
ただし、普段 CDK を使用してプロダクトの開発をされる場合はその限りではないので、気にせずガンガン使って問題ありません。しかし、今後 CDK コントリビュートをされる方や、自作 Construct を多言語での使用も想定してライブラリとして公開する方は知っておくと良い話かなと思います。