AWS コンストラクトライブラリは AWS CDK アプリを開発するためのさまざまなコンストラクトを提供しています。アプリの開発者にとって最も重要なのは、L1, L2, L3 という三種類のコンストラクトです。その中から今回は AWS CloudFormation テンプレートを生成する仕組みの一部である L1 コンストラクトについて説明します。
ふたたび、シンプルなアイデア
前回説明したとおり、AWS CDK の基本的なアイデアは CloudFormation のテンプレートを書くかわりにソースコードを書くことです。
そのシンプルなアイデアをこえた抽象化こそが AWS CDK の真価ではあります。 しかし AWS CDK アプリをシンセサイズして出力されるクラウドアセンブリには、テンプレートが含まれています。 抽象化の裏では、コードからテンプレートを生成する仕組みがはたらいているのです。
今回とりあげる L1 コンストラクトは、そんなテンプレート生成の仕組みの一部をなすものです。
L1 と継承、is-a 関係
テンプレート生成の仕組みは、オブジェクト指向におけるクラスの継承を活用して設計されています。 継承を用いると、クラスとクラスの関係を端的に表現することができます。
ふつう、あるクラス X から継承したクラスは X(の一種)であると解釈されます。 「… は … である」を英語にすると「… is a …」ですので、これを is-a 関係 といいます。
L1 コンストラクトは、Construct
というクラスを何度か継承した先にあります。
「L1 はコンストラクトである」という is-a 関係が、継承を用いて表現されているわけです。
このような話になじみのないかた、某情報処理の試験でしか見たことがないというかたは、AWS CDK を手がかりに色々調べてみるとよいでしょう。 AWS CDK を開発したチームが求めたものは、JSON や YAML には無いこうしたオブジェクト指向の表現力であり、それらを読みとり自分でも表現することは、AWS CDK を使いこなすことにつながっていくからです。
L1 の継承しているクラスを知ることは、L1 の理解を深めることにつながります。
これから L1 の親クラスからそのまた親クラスへと Construct
クラスまでたどりながら、L1 とは何であるか、L1 を含むテンプレート生成の仕組みはどのようなものか、見ていくことにしましょう。
L1 コンストラクトとは
L1 コンストラクトとは、CloudFormation テンプレートの個々のリソースを表すクラス群のことです。 別名として CloudFormation リソースコンストラクトとも呼ばれます。
“L1” というのは “Layer 1” の略で、一番下のレイヤー、層という意味です。 よく “Level 1” の略ともいわれますが1、意味するところは同じです。 L2 や L3 とくらべて抽象化の度合いがもっとも低く、AWS CDK アプリの基礎となるコンストラクトということです。
テンプレートは JSON または YAML 形式のファイルです。 次のように、いくつかのセクション (section) というまとまりに分けて記述します。
{ "AWSTemplateFormatVersion" : "...", "Description" : "...", "Metadata" : { ... }, "Parameters" : { ... }, "Rules" : { ... }, "Mappings" : { ... }, "Conditions" : { ... }, "Transform" : { ... }, "Resources" : { ... }, "Outputs" : { ... } }
これらのうち Resources
セクションには、EC2 インスタンスや S3 バケットなど CloudFormation で作成したいリソースを定義します。
L1 が表すリソースとは、具体的にはこの Resources
セクションに書く JSON の断片のことです。
L1 のクラスとインターフェース
Resources
セクションでは、次のような形式でリソースを定義します。
L1 が表す断片というのも、これと同じ形式です。
"Resources" : { "XXXX" : { "Type" : "XXXX::XXXX::XXXX", "Properties" : { ... } } }
Type
はリソースタイプという、リソース一つ一つに定められた文字列です。
それぞれのリソースタイプには、プロパティという設定項目が定められており、それらは Properties
で指定できます。
リソースタイプとプロパティの仕様は公開され、日々更新されています。 人間が読みやすいリファレンスとしてはもちろん、プログラムで処理しやすい JSON 形式のファイルとしても公開されています。
その仕様とツールを用いて L1 は自動生成されます。 生成されるのは、リソースタイプ一つにつきクラスが一つと、そのプロパティを表すインターフェースが一つです。
命名規則があり、L1 のクラス名は CfnXXXX
のように先頭に Cfn
を付けることになっています。
XXXX
の部分はリソースタイプの末尾からとってくるリソース名です。
プロパティを表すインターフェース名は、CfnXXXXProps
のように末尾に Props
を付けます。
たとえば S3 バケットのリソースタイプ AWS::S3::Bucket
に対応する L1 は CfnBucket
クラスであり、プロパティを表すインターフェースは CfnBucketProps
です。
L1 の例:CfnBucket
例としてその CfnBucket
コンストラクトの使い方を見てみましょう。
import * as core from 'aws-cdk-lib/core' import * as s3 from 'aws-cdk-lib/aws-s3' const app = new core.App() const stack = new core.Stack(app) new s3.CfnBucket(stack, 'Bucket', { bucketName: 'SampleBucket' })
前回のおさらいになりますが、これは基本的な AWS CDK アプリのコードですね。
コアフレームワークの App
と Stack
を使って、コンストラクトツリーを伸ばしています。
Stack
は CloudFormation のスタックを表すコンストラクトです。
Stack
の下には CfnBucket
が伸びています。
bucketName
というのは、S3 バケットの名前を指定するプロパティです。
使っているインターフェースは CfnBucketProps
ですが、ふつうはこんなふうにインターフェース名は書かずにすませます。
このアプリをシンセサイズすると、次のような断片を含む CloudFormation テンプレートが出力されます2。
"Resources": { "Bucket": { "Type": "AWS::S3::Bucket", "Properties": { "BucketName": "SampleBucket" } } }
CfnBucket
に対応するリソースタイプ AWS::S3::Bucket
がきちんと出力されていますね。
プロパティ名 BucketName
の先頭が大文字ですが、先ほどの bucketName
との違いはそれくらいです。
このように L1 でコードを書くことは、Resources
セクションを書くこととあまり違いはありません。
TypeScript としてのエラーチェックやコード補完はききますが、リソースタイプのかわりのクラス名といい、一つ一つ書かなければならないプロパティといい、テンプレートさながらです。
CfnResource コンストラクト
L1 は個々のリソースごとにクラスが分かれていますが、それらはひとくくりに、Resources
セクションに定義するリソースの一種であるとも言えます。
そのことを表すのが CfnResource
コンストラクトです。
CfnResource
は Resources
セクションのリソース全般を表すクラスです。
以下の図のように、すべての L1 は CfnResource
を継承 (TypeScript の extends
) しており、is-a 関係を表現しています。
CfnResource
の書き方は、L1 よりもさらに Resources
セクションの書き方に近いです。
次の例は、先ほどと同様の S3 バケットを定義するコードです。
new CfnResource(stack, 'Bucket', { type: 'AWS::S3::Bucket', properties: { BucketName: 'SampleBucket' } })
type
はリソースタイプの文字列そのものですね。
string
型であればなんでも書けます。
properties
は { [name: string]: any }
というインデックス型です。
よってこれもなんでも書けます。
このなんでも書ける性質は、L1 が存在しないときには便利です。 リソースの仕様は公開されているのにまだ L1 は自動生成されていないときや、仕様にはないリソースを自作したときなどです。
なんでも書けるなら L1 は要らないかというと、そうではありません。
CfnResource
には、L1 のようなエラーチェックやコード補完がほとんどききません。
そのためふつうは L1 のほうが効率よく品質の高いコードを書けるはずです。
テンプレートのコンストラクト
L1 と CfnResource
コンストラクトで Resources
セクションは書けるとして、AWS CDK の基本的なアイデアを実現するには他のセクションも書けなければなりません。
AWS CDK は、テンプレートの各セクションに対応するコンストラクトを提供しています。 以下はその一覧です。
セクション | コンストラクト |
---|---|
AWSTemplateFormatVersion | Stack (templateOptions) |
Description | Stack (templateOptions) |
Metadata | Stack (templateOptions) |
Parameters | CfnParameter |
Rules | CfnRule |
Mappings | CfnMapping |
Conditions | CfnCondition |
Transform | Stack (templateOptions) |
Resources | CfnResource |
Outputs | CfnOutput |
これらを使うと、各セクションの細かいところまで自由に書くことができます。 具体的なコードの書き方は、公式リファレンスを参照してください。
CfnElement コンストラクト
上の一覧の Stack
以外のコンストラクトは、L1 のようにあたまに Cfn
が付いていますが L1 ではありません。
どれもテンプレートの要素を表す CfnElement
コンストラクトを継承したクラスです3。
テンプレートの要素を表すとはいえ、CfnElement
は TypeScript の抽象クラスです。
JSON 形式のテンプレートにするには、なんらかの変換処理が必要になります。
以下はそのための抽象メソッドです。
public abstract _toCloudFormation(): object;
この _toCloudFormation
は開発者からは見えない (@internal
) 、AWS CDK の内部で呼びだされるメソッドです。
戻り値としてはテンプレートの断片となるオブジェクトを返します。
具体的にどのような断片を返すかは、CfnResource
など継承先のクラスで実装されます。
テンプレート生成の仕組み
CfnElement
コンストラクトは is-a 関係を表現するだけでなく、実際に CloudFormation テンプレートを生成するときにも重要な役割をはたします。
その仕組みは、次のような図で表すことができます。
この図は前回概要を説明した AWS CDK アプリのコンストラクトツリーですね。
Stack
は二つあり、それらの下にいくつか CfnElement
が伸びています。
一つだけ Construct
がありますが、これはその下の二つの CfnElement
をまとめるコンストラクトです。
シンセサイズすると、各 Stack
は自分の下のツリーをさがして、CfnElement
を継承しているコンストラクトを集めます。
つまり、テンプレートの断片を出力できるコンストラクト全般を集めます。
そうして集めた断片を整理し、テンプレートとして出力します。
このとき CfnResource
や L1 の CfnBucket
など、細かい具体的なクラスを気にする必要はありません。
CfnElement
を継承したクラスであれば、_toCloudFormation
メソッドで断片を返す決まりになっているからです。
この仕組みのおかげで、アプリの開発者は柔軟にツリーを組むことができます。
意識が必要なのは、Stack
ごとにテンプレートが出力されることくらいです。
あとは適宜まとめ用の Construct
を使うなどして、あつかいやすい形のツリーを作り、そこに L1 や CfnResource
を含めておけばよいのです。
Construct クラス
L1 など、コンストラクトと呼ばれるものはすべて Construct
クラスを継承しています。
上のようなツリーが作れるのも、ツリーからクラスを集めてまわれるのも、すべてコンストラクトの一種として処理できるからです。
Construct
は AWS に限定されない汎用的なクラスです。
もともと AWS CDK に含まれていましたが、CDK8s など AWS CDK 以外でも利用できるよう、v1 の途中から単独の constructs
パッケージとして配布されるようになりました。
コンストラクトツリーの作り方は簡単で、次のようにインスタンスを生成するときにスコープ (scope) と ID を指定するだけです。
new Construct(scope: Construct, id: string)
scope
には、new
するコンストラクトの親にしたいコンストラクトを指定します。
scope
の下に子をくっつけてツリーを伸ばしていくわけです。
id
には、同じ scope
でユニークな文字列を指定します。
scope
が異なれば、同じ id
を何度でも使えます。
第三引数に XXXXProps
のようなプロパティがありませんね。
プロパティは L1, L2, L3 など AWS CDK のコンストラクトにはたいていありますが、あくまで Construct
クラスを拡張して追加したものであり、コンストラクトに必須のものではありません4。
コンストラクトツリーの例
コードの例を見てみましょう。
import { Construct } from 'constructs' const root = new Construct(undefined as any, 'Root') const a = new Construct(root, 'A') const b = new Construct(root, 'B') const a_a = new Construct(a, 'A') const b_a = new Construct(b, 'A')
ツリーのルートのスコープには undefined
を指定することになっています。
ID はなんでもよいので 'Root'
にしています。
ルートの子コンストラクトは二つで、それぞれ ID は 'A'
と 'B'
です。
どちらもスコープは root
で同じなので、同じ ID を使ってはいけません。
ID が 'A'
のコンストラクトが三つありますが、問題ありません。
それぞれスコープが root
, a
, b
で異なるからです。
図にすると次のようになります。
Node クラス
ツリーの中において、各コンストラクトはノード (node) と呼ばれます。
呼び方だけでなく、実際に Construct
クラスのインスタンスを生成すると、内部では Node
というクラスのインスタンスが一つ生成されます。
Construct
はツリーを作る処理をこの Node
クラスにお任せしています。
また、Node
はツリーに関するさまざまな情報をもっており、Construct
の node
プロパティから取得できます。
たとえば、コンストラクト a
の子ノードたちは a.node.children
で取得できます。
自身からルートまでのすべてのスコープは a.node.scopes
で取得できます。
つまり Node
により、ツリーの上から下まですべての情報にアクセスできるわけです。
先ほど見たテンプレート生成の仕組みにおいて、ツリーからコンストラクトをさがし出せるのも Node
のおかげなのです。
おわりに
今回は L1 が継承しているクラスを上へ上へと Construct
までたどりながら、L1 とテンプレート生成の仕組みについて見てきました。
この仕組みはあくまでも、テンプレートのかわりにコードを書くという AWS CDK の基本的なアイデアを実現したものです。 それをこえた抽象化にこそ、AWS CDK の真価はあります。
よって、ここからさらに L1 を継承するわけにはいきません。
L1 は CfnElement
や CfnResource
を継承しており、テンプレートの要素の一種です。
どこまで継承しても、その枠をのりこえることはできません。
そこで L2 や L3 では、継承とは異なるアプローチがとられるのですが、その説明は次回としたいと思います。
参考資料
公式ドキュメントの L1 コンストラクトの説明は『Constructs』に短くまとめられています。 とりあえず L1 をちょっと使ってみるだけならこれで十分でしょう。
テンプレート生成の仕組みや L1 の継承しているクラスの役割については、特に解説が見当たらなかったので、主に aws-cdk-lib/core モジュールのソースコードを参照しました。
L1 の詳細を知りたい場合は、L1 のコードを見るのもよいですが、L1 の自動生成ツール spec2cdk も参考になります。
以前は cfn2ts
という別のツールが使われていましたが、今年 7 月リリースの v2.88.0 から置きかえられました。
-
公式ドキュメント『Constructs』に「L1, short for “layer 1"」と明記されているのですが、筆者の感覚では "Level” の “L” だと思っている人の方が多いと感じます。そんなふうに AWS CDK の用語や概念には、はっきりしないものがけっこうあります。 ↩
-
細かいことをいうと
Metadata
も出力されるのですが、ここでは省略しています。 ↩ -
実は間に
CfnRefElement
というクラスが入るのですが、今回は省略します。このクラスは使われ方とコメントに合わない点があり、設計を見直す必要があると思われます。 ↩ -
AwsCliLayer
コンストラクトにはプロパティがありません。 ↩