AWS CDK Tipsシリーズの記事です。
はじめに
AWS CDKのL2 Constructは、CloudFormationのリソースを抽象化した概念です。
複雑なインフラを簡単に定義できるため非常に便利な存在ですが、時に抽象化が原因でリソースを扱いづらくなる場合があります。
例えば、L2 Constructの props
では設定できないCloudFormationの設定値が存在するような場合です。
本記事では、上記のような場合に、CDKの機能を用いてそれらの設定値を変更する方法を紹介します。
問題となるケース
例として、 aws-apigatewayv2.WebSocketApi
というL2 Constructを使い、Lambda Authorizerが設定されたWebSocket APIを定義する事を考えます。
WebSocketApi
Constructは、現状 props
など直接的なインターフェースからAuthorizerを設定することができません。 Issue #13869
CloudFormationで設定できることは確認できています。
これを解決する最高の方法は、上記のIssueを解決するPull Requestを送る方法ですが、それほど時間もないので簡単な方法で一旦間に合わせたいです。
このような時、どうすれば良いでしょうか?
上記はかなり特有の状況ですが、CDKを使っていると似たような状況に出くわした方も多いのではと思います。
一般化すると、CDKのL2 Constructから直接設定できないがL1 Constructでは設定できる値をどう設定するか? という問題になります。
以下では、そのようなケースに普遍的に適用可能な方法を紹介します。
対処方法
CDKでは、findChild
などのメソッドを使うことで、L2 Constructの内部で定義されているL1 Constructにアクセスできます。
下記に例を示します。
import * as cdk from "@aws-cdk/core";
import * as agw from "@aws-cdk/aws-apigatewayv2";
export class ExampleStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// WebSocketApiの定義(通常通り)
const api = new agw.WebSocketApi(this, `WebSocket`);
// Construct treeからCfnRoute (L1 Construct)を取得
const connectRoute = api.node.findChild("$connect-Route").node.defaultChild as agw.CfnRoute;
// 取得したCfnRouteの設定値を編集
connectRoute.authorizationType = "CUSTOM";
connectRoute.authorizerId = createAuthorizer().ref;
}
// Authorizerを返す関数 (省略)
createAuthorizer(): agw.CfnAuthorizer {
...
}
}
これにより、本来 WebSocketApi
からは設定できなかった $connect
ルートの authorizationType
や authorizerId
を設定しています。
解説
上記のコードで何をやっているのかを解説します。
Constructツリー
CDKでは、Constructはツリーを形成します。
Constructを定義する際は、下記のように 第1引数で scope
を指定します。
この scope
で指定したConstructを親となり、定義したConstructが子としてツリーに追加されていきます。
class Construct {
constructor(scope: Construct, id: string);
}
CDKのアプリケーションで定義されるConstructは、全てこのツリーの中に入っています。
rootノードは App
(よく new cdk.App()
と書くやつです)で、そこから Stack
があり、その中の Construct
と続きます。
L2 ConstructはL1 Constructを束ねるツリーとみなせます。
このため、あるL2 Constructの子ノードを見れば、その中で定義されているL1 Constructが必ず存在するわけです。
ツリーのノードとしてのインターフェースは、Constructの node
プロパティ で取得できます。
下記のコードでは、findChild
関数によりこのツリーを辿って所望のL1 Constructを取得しています。
const connectRoute = api.node.findChild("$connect-Route").node.defaultChild as agw.CfnRoute;
defaultChild
プロパティは、あるConstructに最大で1つ存在する特別なノードです。そのノードの id
が Resource
または Default
のノードが defaultChild
で取得されます。id
については次の節でまとめます。L2 Constructに直に対応するL1 Constructが割り当てられる場合が多いようです。(WebSocketRoute
→ CfnRoute
、 Bucket
→ CfnBucket
など)
defaultChild
プロパティから取得できるという意味では特別ですが、その他の扱いは通常のノードと変わりません。
ツリー内ノードのID
上記で findChild
関数や defaultChild
プロパティの意味を説明しました。
それでは、 findChild
関数の引数に渡している $connect-Route
とは何でしょうか?
これは、Constructを定義する際に渡している、第2引数 id
です。
class Construct {
constructor(scope: Construct, id: string);
}
このIDはツリーのあるノードの直接の子の間でユニークなため、あるConstructに対して id
を指定して findChild
すると、一意のConstructを取得することができます。
所望のConstructに設定されている id
を知るためには、コードを読むのが正攻法です。
(例えば、上記WebSocketの例だと この辺りを読むとわかります。)
より簡単な方法として、実際に生成されたテンプレートのMetadataを見るという方法もあります。
websocketapiconnectRoute50EA91CF:
Type: AWS::ApiGatewayV2::Route
Properties:
...
Metadata:
aws:cdk:path: ExampleStack/WebSocket/$connect-Route/Resource
この場合、下記のように対応します
-
ExampleStack
: Stackのid
-
WebSocket
:WebSocketApi
Constructのid
-
$connect-Route
:WebSocketApi
Constructの中のWebSocketRoute
Constructのid
-
Resource
:WebSocketRoute
Constructの中のCfnRoute
Constructのid
ツリーと実際のリソースの対応
先程でてきたコードを上の対応に沿って書き直すと、下記のようになります。
const api = new agw.WebSocketApi(this, `WebSocket`);
const connectRouteL2 = api.node.findChild("$connect-Route") as agw.WebSocketRoute;
const connectRoute = connectRouteL2.node.defaultChild as agw.CfnRoute;
defaultChild
は id
が Resource
か Default
のノードになることに注意してください。
取り出してきたchild
の型は IConstruct
になるため、 as
で必要な型にキャストすると良いでしょう。
addOverride
メソッド
今回は以下のコードでL1 Constructのプロパティを編集しましたが、まれにこれでは変更が反映されない場合があります。L2 Construct内で、この変更の後に更にプロパティが上書きされる場合があるためです。
connectRoute.authorizationType = "CUSTOM";
connectRoute.authorizerId = createAuthorizer().ref;
そのような場合は、 addOverride
系メソッド を利用しましょう。このメソッドを使うと、CloudFormationのプロパティ名を指定して任意のプロパティを上書きできます。上記の方法とは異なりTypeScriptの補完は効かないため、プロパティ名を間違えないように注意しましょう。
connectRoute.addOverride("Properties.AuthorizationType", "CUSTOM");
// Properties用のバージョンもある
connectRoute.addPropertyOverride("AuthorizationType", "CUSTOM");
まとめ
上記の方法で、任意のL2 Constructから、所望のConstructを取り出すことができます。
これにより、L2 Constructの抽象性とL1 Constructの柔軟性、どちらのメリットも享受することが可能です。
抽象化を恐れることなくCDKを使っていきましょう!