オブジェクトの広場はオージス総研グループのエンジニアによる技術発表サイトです

クラウド/Webサービス

もっとじっくり AWS CDK のコンセプト

第5回 オウンドとアンオウンド
オージス総研
樋口 匡俊
2024年3月27日

これまで三回にわたり、コンストラクトを L1, L2, L3 の三種類に分けて見てきました。今回は、オウンドとアンオウンドという新たな切り口でコンストラクトを説明します。その前に、まずは前提知識としてポリモーフィズムの話から始めたいと思います。

コンストラクトツリーとポリモーフィズム

ポリモーフィズム (polymorphism, 多態性) は AWS CDK にかぎらないオブジェクト指向の重要な概念です。 その厳密な定義や細かい分類はさておき、ここではポリモーフィズムとは substitutability のこと、つまり何かと何かを置き換えられることとしておきましょう1

substitutability としてのポリモーフィズムは、すでにコンストラクトツリーの形でこの連載に登場しています。

AWS CDK アプリは L1, L2, L3 などそれぞれ実体が異なるコンストラクトをツリー状に組み合わせた構造をしています。 App, Stack などコアフレームワークとしての制約はあるものの、コンストラクトとしての組み合わせは自由です。 あるコンストラクトを別のコンストラクトで置き換えたとしても、相変わらずコンストラクトツリーとしては同じようにあつかえるのです。

コンストラクトツリーとポリモーフィズム

インターフェースとポリモーフィズム

コンストラクトのポリモーフィズムは、これまで何度も登場した共通の親クラス Construct と、そのインターフェース IConstruct によって実現されています。 今回注目したいのは、IConstruct インターフェースの方です。

以下は IConstruct が提供する node プロパティを利用する例です。 BucketQueue という異なる L2 コンストラクトから、同じ node.id を介して ID が取得できることが分かります。

import { Stack } from "aws-cdk-lib"
import { Bucket } from "aws-cdk-lib/aws-s3"
import { Queue } from "aws-cdk-lib/aws-sqs"
import { Construct, IConstruct } from "constructs"

export class SampleStack extends Stack {
  constructor(scope: Construct, id: string) {
    super(scope, id)

    const bucket: IConstruct = new Bucket(this, 'Bucket')
    const queue: IConstruct = new Queue(this, 'Queue')

    console.log('bucket.node.id', '=', bucket.node.id)
    console.log('queue.node.id', '=', queue.node.id)
  }
}

// `cdk synth` の出力結果
//
// bucket.node.id = Bucket
// queue.node.id = Queue

このように実体も実行結果も異なるコンストラクトであっても、同じインターフェースで同じようにあつかうことができます。

L2 のインターフェース

AWS CDK では、こうしたインターフェースを用いたポリモーフィズムがさまざまな場面で登場します。 その代表例が L2 コンストラクトです。

第3回において、L2 はコンストラクトのクラスとともにそのインターフェースがひとつ必要であると説明しました。 上の例に出てきた BucketIBucket インターフェースを、QueueIQueue インターフェースをそれぞれ実装しています。

ふつう L2 のインターフェースは、ARN や各種 ID、Name などリソースのすべての属性 (attribute) をプロパティとして提供します。 また、リソースの細かい設定をしてくれる便利なメソッドも提供します。 それらを利用すれば、L2 のクラスの実体がなんであれ、その L2 に期待される処理の多くは実行できます。

AWS CDK コンストラクトライブラリでは「I + コンストラクト名」という名のインターフェースが多用されていますが、それらの多くは L2 のインターフェースです。 AWS CDK は、L2 のインターフェースとポリモーフィズムを活用して、柔軟で再利用性の高い設計・実装になっているのです。

L2 のインターフェースの例: Bucket と Key

例としておなじみの Bucket コンストラクトを見てみましょう。 以下のコードは S3 バケットのサーバーサイドの暗号化のために、AWS KMS キーを表す L2 コンストラクト Key を設定しています。

const key: IKey = new Key(this, 'Key')
const bucket: IBucket = new Bucket(this, 'Bucket', {
  encryptionKey: key
})

new しているのは Key クラスですが、変数 keyIKey インターフェースですね。 そしてその key を設定した encryptionKey プロパティもまた IKey インターフェースです。

Bucket の内部では KMS キーの ARN を用いた処理が実行されます。 ARN らしきものはどこにも見当たりませんが、問題ありません。 ARN は IKeykeyArn プロパティを用いて参照できるからです。

このバケットの読み取り権限をだれかに付与したい場合、bucket.grantRead(...) のように、IBucketgrantRead メソッドを呼び出します。 grantRead メソッドの内部では、IKeygrant メソッドを呼び出して KMS キーを利用するための設定をしてくれます。

開発者は grant メソッドとは何かなど、細かいことを知っている必要はありません。 KMS キーを表すものが IKey インターフェースのオブジェクトであることと、それを設定するのが encryptionKey プロパティであることを知っていればよいのです。

属性参照よりもオブジェクト参照

この encryptionKey プロパティのように、コンストラクトのプロパティやメソッドの引数としてオブジェクトを設定することをオブジェクト参照 (object reference) といいます。 オブジェクトではなく、かわりに ARN などリソースの属性を設定することを属性参照 (attribute reference) といいます。

AWS CDK コンストラクトライブラリのデザインガイドラインでは、属性参照ではなくオブジェクト参照を用いることを原則としてかかげています。 属性参照にはいくつも欠点があるからです。

例として、encryptionKey プロパティがもしも属性参照だったらと仮定してみましょう。 先ほどのコードは以下のように変わります。

const bucket: IBucket = new Bucket(this, 'Bucket', {
  encryptionKey: 'KMS キーの ARN の文字列'
})

この場合、もしもなにか仕様が変更されて ARN 以外にも KMS キーの属性が必要になったら、そのために Bucket コンストラクトのプロパティを追加・変更しなくてはなりません。 いっぽうオブジェクト参照であれば、IKey インターフェースから必要な属性を取得できます。

また、属性はふつうプリミティブな string 型の文字列です。IKey インターフェースが提供する grant など便利なメソッドは使えないのです。

参照用の L2

先ほどの例では L2 である Keynew していました。 Key のメインの L1 は CfnKey ですので、CloudFormation テンプレートには KMS キーのリソース AWS::KMS::Key が出力されます。

KMS キーを自分自身で、バケットとともに、新たに生成したい場合はこれで問題ありません。 しかし、いつもそうしたいとはかぎりません。

KMS キーの管理者と利用者が分かれており、管理者が作成した既存のキーを利用することもあるでしょう。 また、バケットの作成・更新・削除といったライフサイクルと、KMS キーのライフサイクルとは必ずしも一致しないでしょう。

その場合、IKey インターフェースのオブジェクトとして Key を使うわけにはいきません。 インターフェースにしたがい同じように個々のリソースを表しはするものの、そのリソースを生成はしない、参照用の L2 が必要なのです。

オウンドとアンオウンド

ここで、オウンドとアンオウンドの出番となります。

オウンド (owned) は AWS CDK アプリによる所有を表す用語です。 “own” (所有する) に “ed” がついているので、直訳すれば「所有されている」という意味ですね。

あるアプリで新たに作成したリソースは、いわばそのアプリが所有しているリソース、すなわちオウンドリソースです。 また、そのリソースのコンストラクトはオウンドコンストラクトです。 アプリを書き換え、再びデプロイすれば、オウンドリソースは更新したり削除したりもできます。

オウンドの反対がアンオウンド (unowned) です。 あるアプリにとって、他のアプリが作成したリソースはアンオウンドリソースであり、そのリソースのコンストラクトはアンオウンドコンストラクトです。 また、アプリでなく AWS CLI や AWS マネジメントコンソールで作成したリソースもアンオウンドリソースです。 参照用の L2 は、このアンオウンドコンストラクトということになります。

オウンドとアンオウンドは重要な概念だと思うのですが、なぜか AWS CDK v2 の『Developer Guide』には載っていません。 けれども先述のデザインガイドラインでは一節を割いて説明されています。isOwnedResource というオウンドかどうか判定するメソッドもありますし、GitHub での議論にも出てくる用語ですので、これからこの連載でも使っていきます。

アンオウンドコンストラクトの作り方

アンオウンドコンストラクトは、オウンドの部品を再利用しつつ、大筋では L2 のルールにしたがって作ることになります。

以下は Xxxx というオウンドコンストラクトに対応する、アンオウンドの実装の概要です。

// L2 共通の親クラス `Resource` を継承する
class Import extends Resource implements IXxxx {
  public readonly xxxxId: string;

  constructor(......) {
    super(......);

    this.xxxxId = xxxxId;
  }
  ......
}

// `Xxxx` の基底クラス `XxxxBase` があればそれを継承してもよい
class Import extends XxxxBase implements IXxxx {
  ...
}

IXxxx インターフェースは必ず実装しなければなりません。 インターフェースによるポリモーフィズムを活用し、オウンドとアンオウンドを同じようにあつかうためです。

オウンドであれば constructor の中でメインの L1 をコンポジションするのがふつうですが、アンオウンドに L1 は要りません。 この例のように、主な処理はリソースの属性など必要なフィールドを設定することです。 constructor を書かずにフィールドの初期化だけで済ますこともよくあります。

アンオウンドコンストラクトのクラス名はたいてい Import です2。 AWS CDK のソースコードを “class Import extends” で検索すると多くの実装例が見つかるので、さらにくわしく実装方法を知りたい方はご覧ください。

L2 の from メソッド

上のようなアンオウンドコンストラクトを自作するのは、独自の L2 を作るときぐらいでしょう。 たいていは既存のオウンド L2 が提供する from メソッドを使えば十分です。

from メソッドとは、その名の通り名前の先頭に from が付いたメソッドの総称です。 from メソッドはいわゆるスタティックファクトリーメソッド (static factory method) で、戻り値としてアンオウンドコンストラクトのオブジェクトを生成してくれます。 戻り値の型は IXxxx のようなオウンドと同じインターフェースであり、オウンドかアンオウンドか意識することなく同じようにあつかえます。

from メソッドは各 L2 ごとに異なりますが、主に以下の三種類に分類できます。

1. fromAttribute メソッド

引数として ARN や各種 ID、Name などリソースの属性をとるメソッドです。 属性の数は一つであり、メソッド名は「from + コンストラクト名 (リソース名) + 属性名」という風に属性を明示したものになります。

以下は KeyfromKeyArn メソッドを呼び出して ARN から KMS キーのアンオウンドなオブジェクトを生成する例です。 これで先ほど問題だった、既存の KMS キーをオブジェクト参照できます。

const key: IKey = Key.fromKeyArn(this, 'Key', 'arn:aws:kms:us-east-1:123456789012:key/xxxx')

第三引数が ARN ですね。 設定した文字列は、そのまま IKeykeyArn プロパティから取得できます。

第一引数 this と第二引数 'Key'は、それぞれおなじみのスコープと ID です。 アンオウンドとはいえコンストラクトですので、オウンドと同様にコンストラクトツリーの中に組み込み、必要に応じて他の処理でも利用するわけです。

このほかにも SubnetfromSubnetId メソッドや BucketfromBucketName メソッドなど、さまざまな fromAttribute メソッドがあります。

2. fromAttributes メソッド

さっきと名前が似ていますが、よく見ると「s」が付いていますね。 こちらは一つではなく複数の属性を表すインターフェースを引数にとります。

以下は fromBucketAttributes メソッドを呼び出して、バケットを表すアンオウンドなオブジェクトを生成する例です。

const bucket: IBucket = Bucket.fromBucketAttributes(this, 'Bucket', {
  bucketName: 'example-name',
  bucketWebsiteUrl: 'http://example',
})

3. fromLookup メソッド

Vpc コンストラクトの fromLookupとして、おそらく三種類の中ではもっともよく知られたメソッドかと思います。

上の二種類のメソッドは、リソースの属性値を一つ一つ調べて引数に設定する必要がありました。 生成されるアンオウンドのオブジェクトで利用できる属性やメソッドは、それら設定した値に関するものに限られます。

いっぽうこの fromLookup メソッドは、ID などわずかな情報をもとに、関連する多数の情報を AWS から取得し、アンオウンドのオブジェクトとして返してくれます。 筆者が思うに、これは AWS CDK の中では特殊な処理です。 また、コンテキストというコンストラクトの重要な概念も関係してきますので、くわしい説明は別の回に譲りたいと思います。

おわりに

今回は、オウンドとアンオウンドという新たな切り口を紹介しました。 アンオウンド = 他のだれかのもの・外の世界のものをどうあつかうかというのは、CDK を使い込んでいくとよく突き当たる問題です。 紹介した fromAttribute(s) メソッドは初期からある解決方法ですので、まずはその使い方と背景にある考え方に慣れるのがよいでしょう。


参考資料

説明したことの多くは AWS CDK コンストラクトライブラリのデザインガイドラインにもとづいています。 Bucket と KMS Key の例は、ガイドラインにも同様の例が載っています。 オウンド/アンオウンド、属性参照/オブジェクト参照という用語も、ガイドラインに載っています。


  1. substitutability としてのポリモーフィズムに言及している例を二つ挙げておきます: (1) “This substitutability is known as polymorphism, and it’s a key concept in object-oriented systems.” (GoF『Design Patterns』Chapter 1. Introduction の Specifying Object Interfaces). (2) “Same interface, different implementation. Substitutability.” (https://wiki.c2.com/?PolyMorphism, Last edit February 19, 2010). 

  2. クラス名が Import であるのは、アンオウンドのオブジェクトを参照することを import と呼ぶことがあるからです。しかし import というのは、既存のリソースをスタックの管理下に取り込むことなど別の意味でも使用され、混乱をまねきやすい言葉です。コミュニティからは Pulumi にならって用語を整理しようという意見も出ています。