[番外編] CDK奮闘記

CDKではリソースごとにタグを付与することが難しい。L2コンストラクトでは複数リソースを抽象化してまとめているけど、それによってL2コンストラクトにタグを付けるとVpcだけではなく、SubnetやRouteTableなどL2コンストラクトに含まれるすべてのリソースに対してタグが付与されてしまう。これは自動命名でも同様で、Stack/Constructなどのような名前(Nameタグ)がすべてのリソースに適用されてしまう。

前提として、Stack/Construct/Constructなどのような/
で区切れたものをCfn側がNameタグとして採用する。コンストラクトツリーは以下のようになっており、ツリー構造に従ってNameタグが作成されてしまう。RouteTableなどはL2のPublicSubnetコンストラクト内のリソースなのでSubnetと同様のNameタグが付与されてしまう。

継承クラス図はこんな感じ。
L1コンストラクト:CfnResourceを継承したコンストラクト
L2コンストラクト:Resourceを継承したコンストラクト

そもそもAWSコンソール上において各リソースに定義されるリソース名が「Nameタグによるもの」か「固有の名前か」で揺らいでいることも大きな問題となっている。SubnetやRouteTableなどのリソースは「Nameタグによるもの」だが、Lambda関数などは「固有の名前」となっており、名前の形式が異なる。

固有の名前(ECSとか)の場合にはL2にも名前定義するパラメータが与えられている気がする。

ほぼすべてのリソースに名前を適切に付与しようと思ったら、以下のどれかかな。個人的には2。
なお、高レベルと低レベルとの違いは子ノードとして多くのL2コンストラクトを持っているか否か(まったく厳密な定義ではなく、自分の中だけでの定義)。多くのL2コンストラクトを持っていると何回もエスケープハッチで潜らないといけないからコードが複雑になってしまう。
- 高レベルL2コンストラクト(ec2.Vpc)+エスケープハッチ
- 低レベルL2コンストラクト(ec2.Subnet)+エスケープハッチ
- L1コンストラクト

運用時に参照するサービスのみ適切に命名して、それ以外はCDKに従うという方針も割とアリなのかもね。今までの慣習があるから難しいかもしれないけど、現実的なラインだとは思う。

この手があったか!
L2 ConstructであるVpc
インスタンスのプロパティ(属性)であるpublicSubnets
を使用してforループを実装することで短いコードで複数のSubnetまで潜ることが可能となる。
// escape hatch to attach Name tag to the public subnets
for (const subnet of this.vpc.publicSubnets) {
const publicSubnet = subnet.node.defaultChild as ec2.CfnSubnet;
publicSubnet.addPropertyOverride('Tags', [
{
Key: 'Name',
Value: `${this.vpc.node.id}-Public-${subnet.availabilityZone}`,
},
]);
}

ただ、これを実現するためには各L2コンストラクトのコンストラクトツリーを正確に把握する必要があるよな...どのリソースがどのコンストラクトに含まれているかということを。これをチーム全体に浸透されるのは相当な難しさがあると思うんだよな...
Terraformと比較して、良いと思う部分は必要なリソースをとりあえずは作成してくれるところ。良くも悪くもあまりAWS詳しくない人でもデプロイはできてしまう。ただ、詳細な設計までしようと思ったら逆に相当詳しくないと整然とした設計は難しいかな。

やっぱりリソース全体の設計をキチッと決める形式とCDKは絶望的に相性が悪い。
ガバクラでCDKが使用されている以上、ここはCDKに合わせて変わっていくしかないと思う。

とりあえずVPC部分は良い感じになったな!

基本的には全体に命名する事はせずに、運用時に参照するリソースに対してのみ命名する
アプリの実行基盤のリソース確認:ECS, EC2, RDS, Lambda, CloudWatch Metrics
ログの確認:CloudWatch Logs, S3
CI/CDの状況確認:CodePipeline, CodeBuild, CodeDeploy

環境分割
- /bin/sample.tsにおいて、Stackを環境ごとに明示的に分割する
npx cdk ls
としたときに構築しようとしているStackが全て表示される点は分かりやすくていい。
ただ、CDKが推奨しているのはContext形式。理由は後で調べる。

触る必要があるファイルはconfigだけであって欲しいよね。

命名のことを考えたら、Subnetは別コンストラクトで作成がいいかもね

開発環境ではALBがいるサブネット以外はシングルAZになると思うから、そういった個別の条件分岐にもこうすれば対応できる。

Fn.Cidr
[(Fn.select(2, Fn.cidr(
${props.vpc.vpcCidrBlock}, 3, '0'))).split('/')[0]]

L1Constructからテンプレートを生成する流れ
シンセサイズすると、各 Stack は自分の下のツリーをさがして、CfnElement を継承しているコンストラクトを集めます。 つまり、テンプレートの断片を出力できるコンストラクト全般を集めます。 そうして集めた断片を整理し、テンプレートとして出力します。
このとき CfnResource や L1 の CfnBucket など、細かい具体的なクラスを気にする必要はありません。 CfnElement を継承したクラスであれば_toCloudFormation
メソッドで断片を返す決まりになっているからです。

新しく出てきたものがいくつかありますが、VpcId 欄の !Ref はとても重要な組み込み関数です。
どのVPC内にサブネットを作成するかを VPC ID で指定しますが、CloudFormation で VPC が作られないと ID は分かりませんよね。そんな時は、同テンプレート内で設定した Logical ID (リソースの論理ID)を !Ref で参照してあげることで、VPC ID が自動的に当てはまるのです。

albのあたりは何を使用してターゲットグループやECSサービスを作成/紐付けするのか分かりづらい。
Listener.addTargets()を使用することでTG作成+サービスとの紐付けまで一貫して実装可能。

別リソースで自動で作成されて、個別に作る必要がないパターンもあるから厄介
大きいリソースから作成するべきなのかな

IGrantableは「許可を受ける側」が実装するインターフェイスか

命名がどうなるのかはまとめておこうかな。

NodeはConstructクラスで定義されている。
当たり前といえば当たり前だよな。全てのコンストラクトがNodeプロパティ(Nodeクラスのインスタンス)を持っていないといけないんだから。