KAKEHASHI Tech Blog

カケハシのEngineer Teamによるブログです。

CDK の node と少しだけ仲良くなる

こんにちは、おくすり連絡帳 Pocket Musubiというサービスを開発している石井です。

私たちのチームはフロントエンド担当とかバックエンド担当とか敷居を区切らずに、エンジニアのモチベーションでフロントもバックエンドも実装するフルスタックよりなエンジニアで構成されています。 もちろんその中で得意分野は各自ありまして、私はインフラを得意(?)というかインフラが好きなエンジニアになります。 インフラの開発ではTerraformServerless等ありますが、最近ではもっぱらCDKを使っています。

さてそんな CDK ですが、開発していると稀にnodeってでてきませんか?

Node.js、、、ではなくて、CDK のClass Nodeのほうです。 CDK の実装してるときに欲しいプロパティにアクセスできないときとかに、下記 ↓ な感じで突然登場するアレです。

hoge.node.default_child

これがいったい何者で、何ができるんだろ?ってなったときに、資料が少ない気がするので、今回筆をとってみました。 ただ私も資料が無いなかで書いているところがあるので「node に対して私のイメージを共有してみる」という感じになりますが、この記事が少しでも CDK の理解の助けになれば幸いです。

nodeとはなにか?

これを説明するために、まずコンストラクトコンストラクトツリーについて説明させてください。

CDK のコンストラクトは公式の言葉を借りると「アプリの基本的な構成要素」とか説明されてます。なんだかよくわからないですが、ここでは AWS のリソースをまとめたコンポーネントみたいなものだと思ってください。 コンストラクトの中身は AWS のリソースが 1 つだけ入っているものであったり、複数リソース入っているものがあったりします。

次にこのコンストラクトですが、絶対に別のコンストラクトの下層に紐付いている必要があります。図にすると下記 ↓ な感じです。

この図ではB コンストラクトA コンストラクトの下層に紐付いています。 また 1 つのコンストラクトに複数のコンストラクトを紐付けることができるので、コンストラクトは下記 ↓ な感じのツリー構造になっていきます。

このツリー構造をコンストラクトツリーとか呼ばれているようです。

コンストラクトツリーの補足として、ツリーの最上段にはAppを置く必要があり、その直下にはStackが紐づく必要があります。なのでコンストラクトは Stack より下層に紐付いていく感じです。

なので最終的にはこんな ↓ 感じになります。

話もどりまして、では node とはなんでしょうか?

一般的なツリー構造のノードと定義は一緒です。 なので上の図だと角丸四角の要素が node です。そしてコンストラクトと住み分けるとすると node はコンストラクトに紐付いていて、コンストラクトツリーでの場所を示しています。

CDK で node を使うときはいつか?

node はコンストラクトツリーでの場所を示しているので、node  がわかると対象のコンストラクトを取得することできます。また各 node は自分の上位の node や下位の node へアクセスできます。 これを利用し、あるコンストラクトの node を経由して、別のコンストラクトへアクセスして設定を変更する、、、とかができます。 これは CDK で特定のコンストラクトの設定を変更したいのだけれども、そのコンストラクトを取得する属性や関数がなくて取得できない場合に利用できます。

コンストラクトツリーとかを見ながら node を使ってみる

実際に node がどんな感じで使われて、どんな情報が取得できるのか見てみます。 サンプルで ↓ のような、S3 バケットをデプロイするだけのコードを用意してみました。

import aws_cdk as cdk
from aws_cdk import aws_s3 as s3

app = cdk.App()
stack = cdk.Stack(app, "HogeStack")

bucket = s3.Bucket(stack, "HogeBucket")

app.synth()

ツリーの最上位の App を定義して、その直下に stack を紐付けて、さらにそこにバケットのコンストラクトを紐付けています。 ではこれのツリーを見てみようと思います。

下記のようなコードで、最上位の node である App の持ってる node を全て出力できます。

print([node.to_string() for node in app.node.find_all()])

出力結果は下記な感じでした。

[
    "<root>",
    "HogeStack",
    "HogeStack/HogeBucket",
    "HogeStack/HogeBucket/Resource [AWS::S3::Bucket]"
]

絵にするとこんな感じです。ツリーというか一直線上ですね。

ちなみに"HogeStack/HogeBucket/Resource [AWS::S3::Bucket]"だけ表示の毛色が違いますが、これは"HogeStack/HogeBucket/Resource"だけがL1 コンストラクトと呼ばれるものだからだと思います。またここでは L1 コンストラクトの説明は省略いたしますが、興味があれば調べていただければと思います。

次に、このツリーを少しだけ伸ばします。

さっきのサンプルは S3 のバケットを作成するだけでしたが、追加でバケットの前に CloudFront をいれてみます。 下記のようにコードを追加してみます。

# ... 省略

from aws_cdk import aws_cloudfront as cloudfront
from aws_cdk import aws_cloudfront_origins as origins

distribution = cloudfront.Distribution(
    stack, # ← 紐付ける先の上位のコンストラクトを指定
    "HogeDistribution",
    default_behavior=cloudfront.BehaviorOptions(origin=origins.S3Origin(bucket)),
)

# ... 省略

そしてツリーを出力してみると、以下のようになりました。

[
    "<root>",
    "HogeStack",
    "HogeStack/HogeBucket",
    "HogeStack/HogeBucket/Resource [AWS::S3::Bucket]",
    "HogeStack/HogeBucket/Policy",
    "HogeStack/HogeBucket/Policy/Resource [AWS::S3::BucketPolicy]",
    "HogeStack/HogeDistribution",
    "HogeStack/HogeDistribution/Origin1",
    "HogeStack/HogeDistribution/Origin1/S3Origin",
    "HogeStack/HogeDistribution/Origin1/S3Origin/Resource [AWS::CloudFront::CloudFrontOriginAccessIdentity]",
    "HogeStack/HogeDistribution/Resource [AWS::CloudFront::Distribution]",
]

長いですね、、、 絵にすると下記の感じです。追加分は黄色で表してみました。

HogeDistributionHogeStackに紐付けただけですが、S3 バケット側にも新しいリソースができてますね。

次にこの状態からHogeStack/HogeDistribution/Origin1/S3Origin"へアクセスをしてみます。 ちなみに現状では関数や属性とかを利用して、このリソースにアクセスすることができません。なので node を利用せざる得ません。

コードは下記な感じになります。

# ... 略

oai: cloudfront.OriginAccessIdentity = distribution.node.find_child("Origin1").node.find_child("S3Origin")
print(oai.to_string()) # > HogeStack/HogeDistribution/Origin1/S3Origin

# ... 略

変数distributionの node は"HogeStack/HogeDistribution"なので、そこに紐づく"Origin1"にアクセスして、さらにそこに紐づく、、、という感じでアクセスします。

完全に余談ですが、なぜ"HogeStack/HogeDistribution"にアクセスしたいのか?というと S3 バケットに下記のようにポリシーを追加したいからです。

# ... 略

from aws_cdk import aws_iam as iam

policy = iam.PolicyStatement(
    effect=iam.Effect.ALLOW,
    principals=[iam.CanonicalUserPrincipal(oai.cloud_front_origin_access_identity_s3_canonical_user_id)],
    resources=[bucket.bucket_arn],
    actions=["s3:ListBucket"],
)
bucket.add_to_resource_policy(policy)

# ... 略

これを追加することで S3 に存在しないエンドポイントがリクエストされた場合に Client に404 Not Foundを返すようになります。このポリシーがない場合は403 Forbiddenになります。

おわりに

今回は CDK の理解を深めるために、node についての私の理解を共有させていただきました。

コンストラクトツリーは簡単に出力できますし、眺めているだけで結構発見とかあるので、実装が煮詰まったときに出力してみるとなにかしらの突破口が見つかるかもしれません。

CDK は自分もまだまだわかってないことがほとんどですが、これからも頑張って勉強していこうと思います。

以上です。最後までお読みいただきありがとうございました!