レイヤー 2 コンストラクト
AWS CDK オープンソースリポジトリaws-cdk-lib と呼ばれ、AWS サービスごとに 1 つのパッケージに大まかに分割されていますが、常にその状態であるとは限りません。前述したように、L1 コンストラクトは、ビルドプロセス中に自動生成されます。つまり、リポジトリ内に存在するあらゆるコードは、L2 コンストラクトであり、これらは、L1 コンストラクトが抽象化されたものです。
パッケージには、TypeScript タイプ、列挙型、インターフェイスのコレクションに加え、機能を追加するヘルパークラスも含まれていますが、こうした要素はいずれも、L2 コンストラクトに利用します。どの L2 コンストラクトも、インスタンス化された際に、自身のコンストラクタ内にある対応する L1 コンストラクトを呼び出します。作成された L1 コンストラクトには、レイヤー 2 から次のようにアクセスできます。
const role = new Bucket(this, "amzn-s3-demo-bucket", {/*...BucketProps*/}); const cfnBucket = role.node.defaultChild;
L2 コンストラクトでは、デフォルトのプロパティ、便利なメソッド、その他のシンタックスシュガーを取り込み、L1 コンストラクトに適用します。これにより、CloudFormation でリソースを直接プロビジョニングするために必要な反復と冗長性の大部分を取り除くことができます。
どの L2 コンストラクトでも、対応する L1 コンストラクトが内部に構築されます。しかし、L2 コンストラクトは、L1 コンストラクトを継承することはありません。L1 コンストラクトも L2 コンストラクトも、Construct という特別なクラスを継承します。AWS CDK バージョン 1 の Construct クラスは、開発キットに組み込まれていましたが、バージョン 2 では、独立したスタンドアロンパッケージConstruct クラスを継承するクラスはいずれも、L1、L2、L3 コンストラクトのいずれかです。次の表に示すように、L2 コンストラクトは、このクラスを直接継承しますが、L1 コンストラクトは CfnResource というクラスを継承します。
L1 継承ツリー |
L2 継承ツリー |
|---|---|
L1 コンストラクト → クラス CfnResource → → 抽象クラス CfnRefElement → → → 抽象クラス CfnElement → → → → クラス Construct |
L2 コンストラクト → クラス Construct |
L1 コンストラクトも L2 コンストラクトも Construct クラスを継承しているのに、なぜ L2 コンストラクトは L1 を継承しないのでしょうか。Construct クラスとレイヤー 1 間にあるクラスでは、CloudFormation リソースをそのまま反映したものとして、L1 コンストラクトが特定の位置に固定されるからです。こうしたクラスには、ダウンストリームクラスに含まれる必要のあるメソッドが含まれています。_toCloudFormation がその例ですが、このメソッドは、CloudFormation 構文が直接出力されるよう強制するものです。L2 コンストラクトは、こうしたクラスをスキップし、Construct クラスを直接継承します。そのように、L2 コンストラクタ内で L1 を個別に構築することで、L1 コンストラクトに必要なコードの多くを柔軟に抽象化できるのです。
前のセクションでは、CloudFormation テンプレートの S3 バケットと、L1 コンストラクトとしてレンダリングした同じ S3 バケットを並べて比較しました。この比較では、プロパティと構文がほぼ同じであることがわかり、L1 コンストラクトのコードは、CloudFormation コンストラクトよりも、3 ~ 4 行だけ短くなっていました。今度は、同じ S3 バケットをそれぞれ記述した L1 コンストラクトと L2 コンストラクトを比較してみましょう。
S3 バケットの L1 コンストラクト |
S3 バケットの L2 コンストラクト |
|---|---|
|
|
ご覧のとおり、L2 コンストラクトのサイズは、L1 コンストラクトの半分未満です。L2 コンストラクトでは、このような統合を実現するために多くの手法を使用します。そうした手法の一部は 1 つの L2 コンストラクトに適用しますが、それ以外は、複数のコンストラクトで再利用可能なため、クラスとして独立させ再利用します。L2 コンストラクトでは、複数の方法で CloudFormation 構文を統合します。これについては、以下のセクションで説明します。
デフォルトのプロパティ
リソースプロビジョニングのコードを最も簡単に統合する方法とは、最も一般的なプロパティ設定をデフォルトとして扱うことです。AWS CDK では、強力なプログラミング言語にアクセスできますが、CloudFormation ではそれが行えないため、多くの場合、こうしたデフォルト設定には、その性質上、条件が設定されます。これにより、AWS CDK コードから、数行を削除できる場合があります。CloudFormation の設定は、コンストラクトに渡される他のプロパティ値から推測できるからです。
構造体、型、インターフェイス
AWS CDK は複数のプログラミング言語で使用できますが、ネイティブ言語は TypeScript であるため、L2 コンストラクトを構成する型の定義には、言語の型システムを使用します。そうした型システムの掘り下げは、このガイドの範囲外です。詳細については、TypeScript ドキュメントtype とは、特定の変数が保持するデータの種類を表すもので、こうしたデータは、string などの基本的なものから、object などの複雑なものまで多岐にわたります。TypeScript オブジェクトの型は、TypeScript interface によっても表すことができ、struct は、インターフェイスの別名とも言えます。
TypeScript では、構造体という用語を使用しませんが、AWS CDK API リファレンスを見ると、構造体がコード内で TypeScript インターフェイスと同じ概念で使用されていることがわかります。同時に、API リファレンスでは、特定のインターフェイスをインターフェイスと呼んでいます。構造体とインターフェイスは同じ概念を持つにもかかわらず、AWS CDK ドキュメントでは、なぜそれらを区別しているのでしょうか。
AWS CDK では、L2 コンストラクトで使用するオブジェクトを表すインターフェイスを構造体と呼んでいます。これには、インスタンス化中に L2 コンストラクトに渡すプロパティ引数のオブジェクト型も含まれます。例えば、S3 バケットコンストラクトの BucketProps や DynamoDB Table コンストラクトの TableProps、AWS CDK 内で使用するその他の TypeScript インターフェイスなどです。つまり、それが AWS CDK 内の TypeScript インターフェイスであり、名前の先頭に I の文字が付いていないものを AWS CDK では構造体と呼びます。
逆に、AWS CDK でインターフェイスという用語が使用されるのは、プレーンオブジェクトによって特定のコンストラクトまたはヘルパークラスを適切に表現するのに必要な基本要素を表す場合です。つまり、インターフェイスによって、L2 コンストラクトのパブリックプロパティを記述します。AWS CDK のインターフェイス名はいずれも、既存のコンストラクトまたはヘルパークラスの名前にプレフィックス I を付けて表します。すべての L2 コンストラクトに、Construct クラスが継承され、対応するインターフェイスも実装されます。つまり、L2 コンストラクトの Bucket には、IBucket インターフェイスが実装されます。
静的メソッド
L2 コンストラクトのすべてのインスタンスは、対応するインターフェイスのインスタンスでもありますが、その逆が成り立つことはありません。この点は、構造体を調べ、必要なデータ型を確認する際に重要となります。構造体に bucket というプロパティがあり、そのプロパティにデータ型 IBucket が必要な場合、IBucket インターフェイスにリストしているプロパティが含まれるオブジェクト、または L2 Bucket のインスタンスのいずれかを渡すことができます。この場合は、どちらでも機能しますが、その bucket プロパティに L2 Bucket が必要な場合、そのフィールドに渡すことができるのは Bucket インスタンスのみです。
こうした区別は、既存のリソースをスタックにインポートする際に非常に重要となります。そのスタックのネイティブリソースには L2 コンストラクトを作成できますが、スタック外で作成したリソースの参照が必要な場合は、その L2 コンストラクトのインターフェイスを使用しなければなりません。なぜなら、L2 コンストラクトの作成時にそのスタック内にリソースが存在しない場合、新しいリソースが作成されるからです。既存のリソースを参照するには、その L2 コンストラクトのインターフェイスに準拠するプレーンオブジェクトを使用する必要があります。
実践上、これを簡単にするために、ほとんどの L2 コンストラクトには、一連の静的メソッドが関連付けられており、こうしたメソッドによってインターフェイスが返ります。静的メソッドは通常、from という単語で始まります。これらのメソッドに渡す最初の 2 つの引数は、標準的な L2 コンストラクトに必要な scope および id の引数と同じものです。ただし、3 番目の引数には、props ではなく、インターフェイスを定義する小さいプロパティサブセット (場合によっては 1 つのプロパティのみ) を指定します。そのため、L2 コンストラクトを渡す際には、多くの場合、インターフェイスの要素のみが必要となります。これにより、可能な限りインポート済みのリソースを使用できるようになります。
// Example of referencing an external S3 bucket const preExistingBucket = Bucket.fromBucketName(this, "external-bucket", "name-of-bucket-that-already-exists");
ただし、インターフェイスに過度に依存してはなりません。リソースをインポートしてインターフェイスを直接使用するのは、本当に必要な場合のみにします。インターフェイスには、ヘルパーメソッドといった、L2 コンストラクトの効果を高める各種プロパティが用意されていないからです。
ヘルパーメソッド
L2 コンストラクトは単純なオブジェクトではなくプログラムクラスであるため、これによって、インスタンス化後にリソース設定を操作可能なクラスメソッドを公開できます。それがよくわかる例として、AWS Identity and Access Management (IAM) L2 Role コンストラクトが挙げられます。以下のスニペットに、2 つの方法を示します。ここでは、L2 Role コンストラクトを使用して、同じ IAM ロールを作成しています。
ヘルパーメソッドを使用しない場合:
const role = new Role(this, "my-iam-role", { assumedBy: new FederatedPrincipal('my-identity-provider.com'), managedPolicies: [ ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess") ], inlinePolicies: { lambdaPolicy: new PolicyDocument({ statements: [ new PolicyStatement({ effect: Effect.ALLOW, actions: [ 'lambda:UpdateFunctionCode' ], resources: [ 'arn:aws:lambda:us-east-1:123456789012:function:my-function' ] }) ] }) } });
ヘルパーメソッドを使用する場合:
const role = new Role(this, "my-iam-role", { assumedBy: new FederatedPrincipal('my-identity-provider.com') }); role.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess")); role.attachInlinePolicy(new Policy(this, "lambda-policy", { policyName: "lambdaPolicy", statements: [ new PolicyStatement({ effect: Effect.ALLOW, actions: [ 'lambda:UpdateFunctionCode' ], resources: [ 'arn:aws:lambda:us-east-1:123456789012:function:my-function' ] }) ] }));
インスタンス化後にインスタンスメソッドを使用してリソース設定を操作できるため、L2 コンストラクトを使用すると、以前のレイヤーよりも柔軟性が大幅に向上します。L1 コンストラクトも一部のリソースメソッド (addPropertyOverride など) を継承しますが、そのリソースおよびプロパティ専用に設計されたメソッドは、レイヤー 2 で初めて利用できるようになりました。
列挙型
CloudFormation 構文では、多くの場合、リソースを適切にプロビジョニングするために、多数の詳細項目を指定しなければなりません。しかし、ほとんどのユースケースは、わずか数種類の設定を行うだけで済みます。こうした設定を一連の列挙値で表すと、必要なコード量を大幅に削減できます。
例えば、このセクションで前述した S3 バケットの L2 のコード例では、CloudFormation テンプレートの bucketEncryption プロパティを使用する必要があり、これによって、使用する暗号化アルゴリズムの名前などの詳細をすべて指定しました。AWS CDK では、そうした操作ではなく、BucketEncryption という列挙型を利用できます。これによって、バケット暗号化で最も一般的な 5 つの形式を受け取り、それぞれを単一の変数名で表現することが可能です。
次に、列挙型では対応できないエッジケースについて考えてみましょう。L2 コンストラクトの目標の 1 つは、レイヤー 1 リソースのプロビジョニングタスクを簡素化することです。そのため、あまり使用されない特定のエッジケースは、レイヤー 2 ではサポートされない可能性があります。これらのエッジケースに対応するために、AWS CDK では、addPropertyOverride メソッドを使用して、CloudFormation リソースの基盤となるプロパティを直接操作できるようになっています。プロパティのオーバーライドの詳細については、このガイドの「ベストプラクティス」セクションと、AWS CDK ドキュメントの「Abstractions and escape hatches」セクションを参照してください。
ヘルパークラス
列挙型では、特定のユースケースのリソース設定に必要なプログラムロジックを実現できないことがあります。こうした状況での AWS CDK では、多くの場合、列挙型ではなく、ヘルパークラスが利用できます。列挙型は単純なオブジェクトで、キーと値の一連のペアが使用可能な一方で、ヘルパークラスは TypeScript クラスの機能をすべて備えています。ヘルパークラスは、静的プロパティの公開によって列挙型のように動作しますが、こうしたプロパティ値は、ヘルパークラスのコンストラクタまたはヘルパーメソッド内の条件付きロジックを使用して内部的に設定できます。
前述のとおり、BucketEncryption 列挙型を使用すると、S3 バケットへの暗号化アルゴリズム設定に必要なコードの量を削減できますが、期間を設定する場合は、選択可能な値が多いという理由で、これと同じ戦略の効果は得られません。値ごとの列挙型の作成は、価値に見合わないかなりの労力を伴うからです。そのため、S3 バケットのデフォルト S3 Object Lock 設定には、ヘルパークラスを使用し、これを ObjectLockRetention クラスで表します。ObjectLockRetention は、2 つの静的メソッドで構成されます。1 つは、コンプライアンス維持を、もう 1 つはガバナンス維持を目的とするものです。どちらのメソッドも、ロック設定が必要な期間を表す Duration ヘルパークラスのインスタンスを引数として受け取ります。
もう 1 つの例として、AWS Lambda のヘルパークラスである Runtime が挙げられます。このクラスに関連付けられた静的プロパティは、一見すると、列挙型で処理できそうに思えます。しかし、内部的には、各プロパティ値は Runtime クラス自体のインスタンスを表しているため、クラスのコンストラクタで実行されるロジックは、列挙型では実現できません。