Open7

AWS CDKの `new Construct(を継承したクラス)` が具体的に何をしているのか調べてみる

bun913bun913

CDKでコードを記述するにあたり、ふと以下のようなコードを書いていた。

例えば Code PipelineのL2コントラストを使って、ステージを作る場合以下のようなコードを書くと思います。(コードはなんとなくドキュメントを見ながら書いているので、動作するわけではありません。)

//  パイプラインを作成
const pipeline = new Pipeline(this, 'Pipeline', {});
// パイプラインに追加するステージを作成
const prodStage = new Stage(this, 'Prod', {});

pipeline.addStage(prodStage);

ここでちょっと前から気になっていたのが、addStage の関数の動作です。

最近いくつかプログラムの設計・書き方に関する書籍を読んでいて、割と共通して書かれていることが「極力変数・クラスのインスタンス・インスタンス変数などは不変になるようにしなさいよー」ということです。

今回で言えば、 addStage はめっちゃ pipeline に変更加えてないか?ということです。

CDKのことを何も知らない身からしたら、以下のようなコードが本来副作用の少ない(意図しない変更を起こしにくい)コードなのではないか?などと考えていました。

const pipeline = new Pipeline(this, 'Pipeline', {});
const prodStage = new Stage(this, 'Prod', {});
// ↑までは一緒
// ↓addStageが直接 pipelineに変更を加えるのではなく、新しく Pipelineクラスのインスタンスを返しているイメージ
// これで pipelineには変更が加わってないし、意図しない副作用が混入しないのでは?(などと考えていました)
const addedPipeline = pipeline.addStage(prodStage);

ただし、当然これだと CodePipeline のクラスインスタンスをもう一回 new することりなります。

CDKの場合、 同じConstructにおいて同じIDで newできないことは知っているし、最終的にCloudFormatioのテンプレートを出力する際にそれだと都合が悪いこともなんとな〜くわかっています。

// これは無理だよね?という例
const hoge = new Vpc(this, 'vpc', {})
const fuga = new Vpc(this. 'vpc', {})

じゃあ Construct を new することで、具体的に何が行われているのか?
なぜ同じConstruct(スコープが正しいかな?)において、それができないのか、Constructを new するということで具体的に何をしているのか調べてみようと思いました

bun913bun913

最近ちょっとずつ OSSやお世話になっているライブラリなんかにコントリビュートするようになってから、ソースコードを読む・PullRequestを出すことに抵抗が少なくなってきたので、面倒がらずにコードを読んでみることにしました。

まずは上で例に挙げている CodePipeline のクラスを見てみましょう。

ソースはこちら

export class Pipeline extends PipelineBase {

CDKでは ~Base を継承して Constructのクラスを定義しています。デザインガイドラインにもそれが書かれているのですが、今回は特に触れないでおきます。

で、この PipelineBase というか HogeBase が継承しているのがこの Resource クラスです。

そしてこの Resource クラスが継承しているのが Construct になりますね。

やっと本筋っぽいものが現れ始めました。

bun913bun913

Construct は別のリポジトリで管理されていますので、今度はそちらの方を見てみました。

以下のようにクラスが定義されていますね。

Constructconstructor(初期化)で行われているのは以下の処理ですね。

constructor(scope: Construct, id: string) {
    this.node = new Node(this, scope, id);

    // implement IDependable privately
    Dependable.implement(this, {
      dependencyRoots: [this],
    });
  }

this.node に Nodeというクラスをセットしていますな。

ということでNodeを見てみます。

constructor で以下のようなことをやっていますね。

public constructor(private readonly host: Construct, scope: IConstruct, id: string) {
    id = id ?? ''; // if undefined, convert to empty string

    this.id = sanitizeId(id);
    this.scope = scope;

    if (scope && !this.id) {
      throw new Error('Only root constructs may have an empty ID');
    }

    // add to parent scope
    scope?.node.addChild(host, this.id);
  }

「IDをsanitazeid で無害化してセットして〜」とかやっているのもわかりますが、多分重要なのはここの部分な気がしますね。

scope?.node.addChild(host, this.id);

あ〜なんか、どんどん Nodeというのに子を追加しているっぽいことがわかってきました。雰囲気で。

ここら辺でなんかこれどっかで図をみたぞ。というのを思い出してきました。

bun913bun913

ここで完全に、 やまたつさんがCDKのドキュメントを翻訳しつつ、わかりやすくご意見を書いてくれていたBookを思い出しました。

https://zenn.dev/yamatatsu/books/aws-cdk-documentation-jp/viewer/02-concepts-constructs

もう完全に答えが書いてありました。

こちらは上記Bookから引用させていただいています。

つまり Constructを new するたびに、このConstruct Treeの葉を形作っていくということをしていたわけですね。

で・・・ Node は文字通りグラフ理論でいうところのノードという意味合いだったのかなぁなどと理解してきました。

https://dev.classmethod.jp/articles/graph-theory/

CostructのTreeはつまり有向グラフであり、Nodeにより親・子のへのアクセスというか繋がりを保持しているということだと理解しました。

bun913bun913

さらに constructs のリポジトリのREADMEを見ていると以下のように記載されています。

https://github.com/aws/constructs

What are constructs?
Constructs are classes which define a "piece of system state". Constructs can be composed together to form higher-level building blocks which represent more complex state.

Constructs are often used to represent the desired state of cloud applications. For example, in the AWS CDK, which is used to define the desired state for AWS infrastructure using CloudFormation, the lowest-level construct represents a resource definition in a CloudFormation template. These resources are composed to represent higher-level logical units of a cloud application, etc.

和訳するとこんな感じらしいです

コンストラクトは、「システム状態の一部」を定義するクラスです。コンストラクトを組み合わせることで、より複雑な状態を表す上位のビルディングブロックを形成することができます。

appも stackもいわば Constructの集合であり、さらに Constructは子のConstructを持つことができて・・・木構造を構成するということかな。

bun913bun913

自分の理解をなんとか言語化してみると・・・

「Construct を new することで、Construct Treeの葉を形作り木構造を定義していっている。そしてnodeは親や子へのつながりを保持している。」

同じスコープ(Construct)内で同IDのConstructクラスのインスタンスをnewできないのは「同じIDを持つ葉は同一スコープ内で存在できない」というように自分の中で腑に落ちました。
(現実でも同じ両親の兄弟に同じ名前をつけると色々と都合が悪いですよね)

以下コードも pipelineというConstructの下に、新たな葉を追加していると考えれば感覚的に納得ができた気がします。

pipeline.addStage(prodStage);

そもそもCodePipeline などのコンストラクトをドメインオブジェクトのように捉えていたことが誤りのような気がしました。

「CDKで扱っているConstruct(を継承しているクラス達)はドメインオブジェクトや値オブジェクトではなく、木構造の葉なんだ。」という認識でいたほうが自分の中で収まりが良い気がしました!

bun913bun913

俺がCDKでしているのはパラメーターを持った葉を使って、木を作っていたのかなぁ。

おまけ

ここで理解の役に立ったグラフ(有向グラフ)の知識は、AtCoderに参加していく中で習得した知識でした。(存在自体は応用情報技術者などの資格勉強でも知っていましたが、実際に実装したのは競プロの中だけ)

そもそも私の普段の仕事ではあまりプログラムを書くこと自体があまりないのですが、その知識がまさかこのような場面で役に立つとは・・・