【AWS CDK】3分でつかむ、ライブラリとしてのAWS CDK

はじめに

田中(博)です。
開発経験があると、稀にいきなりAWS CDKの作業を依頼されることがあったりすると思います。

AWS CDKのHello Worldに関しては公式のワークショップ(※英語です)があります。
また、他社様でも素晴らしい記事がたくさん公開されており、大変参考になります。

しかし、本記事ではHello Worldのさらに手前、AWS CDKに触るまえに知っておくとためになりそうな知識について説明させていただこうかと思います。
これからAWS CDKにチャレンジする方の一助となれば幸いです。

AWS CDKとは?

とはいえ、AWS CDKってそもそも何?という方もいらっしゃると思います。
端的に言うとAWS CDKとはAWS CloudFormationのスタックを出力してくれるライブラリです。
※詳しい内容は公式をご確認ください。

「AWS CloudFormationのスタック」というところがミソです。
AWS CDKではリソースの生成処理をコードとして実装できますが、直接的に出力されるのはスタックです。
スタックによって生成したリソースを一括管理できるのがうれしい点です。

AWS SDKとの違い

各リソースそのものを直接生成するだけであればAWS SDKで対応できます。
しかし、AWS SDKで作成したリソースは基本的に作りっぱなしとなります。
もしリソースを削除したい場合は別途AWSマネジメントコンソールなどから削除する必要がありますね。

一方、AWS CDKで作成したリソースに関してはCloudFormation上のスタックに紐づきます。
このため、リソースの作成を実装しているコードを削除してデプロイすればリソースも削除されます。
※一部例外を除く

実装を更新してデプロイすると、CloudFormationのスタックも更新されます。
スタックの更新により、紐づているリソースは自動で作成/変更/削除されます。
この点で、リソースの管理や集約に特化したAWS SDKのラッパーという見方もできるかもしれません。

CDKとSDKの比較

AWS CDKの癖

ずばり、コードの内容だけではどのようにリソースが展開されるのかほとんど予想ができません。
私はJavaをよく書いていましたが、Javaのライブラリ(JDK)を郊外のショッピングモールだとするなら、AWS CDKは地元の商店街のようです。

実装例で見る、コードと出力内容の差

説明より例を見ていただいたほうがわかりやすいですね。
以下のAWS CDKのコードをご覧ください。
コードを素直に読むとVPCを一つ作成し、EC2インスタンスを一つ作成するシンプルな内容です。

import * as cdk from 'aws-cdk-lib';
import { aws_ec2 as ec2 } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class HelloCdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const vpc = new ec2.Vpc(this, 'vpc');

    const instance = new ec2.Instance(this, 'myInstance', {
      vpc,
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
      machineImage: ec2.MachineImage.latestAmazonLinux2023()
    });
  }
}

AWS CDKには現在のスタックとの差分を確認する便利なコマンドcdk diffがあります。
それを実際に実行し、出力されるリソースの差分は以下の通りです。
(見やすいよう、一部内容を割愛しています)

Resources
[+] AWS::EC2::VPC vpc
[+] AWS::EC2::Subnet vpc/PublicSubnet1/Subnet
[+] AWS::EC2::RouteTable vpc/PublicSubnet1/RouteTable
[+] AWS::EC2::SubnetRouteTableAssociation vpc/PublicSubnet1/RouteTableAssociation
[+] AWS::EC2::Route vpc/PublicSubnet1/DefaultRoute
[+] AWS::EC2::EIP vpc/PublicSubnet1/EIP
[+] AWS::EC2::NatGateway vpc/PublicSubnet1/NATGateway
[+] AWS::EC2::Subnet vpc/PublicSubnet2/Subnet
[+] AWS::EC2::RouteTable vpc/PublicSubnet2/RouteTable
[+] AWS::EC2::SubnetRouteTableAssociation vpc/PublicSubnet2/RouteTableAssociation
[+] AWS::EC2::Route vpc/PublicSubnet2/DefaultRoute
[+] AWS::EC2::EIP vpc/PublicSubnet2/EIP
[+] AWS::EC2::NatGateway vpc/PublicSubnet2/NATGateway
[+] AWS::EC2::Subnet vpc/PrivateSubnet1/Subnet
[+] AWS::EC2::RouteTable vpc/PrivateSubnet1/RouteTable
[+] AWS::EC2::SubnetRouteTableAssociation vpc/PrivateSubnet1/RouteTableAssociation
[+] AWS::EC2::Route vpc/PrivateSubnet1/DefaultRoute
[+] AWS::EC2::Subnet vpc/PrivateSubnet2/Subnet
[+] AWS::EC2::RouteTable vpc/PrivateSubnet2/RouteTable
[+] AWS::EC2::SubnetRouteTableAssociation vpc/PrivateSubnet2/RouteTableAssociation
[+] AWS::EC2::Route vpc/PrivateSubnet2/DefaultRoute
[+] AWS::EC2::InternetGateway vpc/IGW
[+] AWS::EC2::VPCGatewayAttachment vpc/VPCGW
[+] Custom::VpcRestrictDefaultSG vpc/RestrictDefaultSecurityGroupCustomResource
[+] AWS::IAM::Role Custom::VpcRestrictDefaultSGCustomResourceProvider/Role
[+] AWS::Lambda::Function Custom::VpcRestrictDefaultSGCustomResourceProvider/Handler
[+] AWS::EC2::SecurityGroup myInstance/InstanceSecurityGroup
[+] AWS::IAM::Role myInstance/InstanceRole
[+] AWS::IAM::InstanceProfile myInstance/InstanceProfile
[+] AWS::EC2::Instance myInstance

[+] はリソースの追加を表現しています。
AWS::EC2::InstanceなどAWS::から始まっている文字列がリソースのタイプです。
末尾がリソースのパスにあたります。

こう見ると、vpc/から始まるパスのリソースがたくさん出力されていますね。
これらはすべて new ec2.Vpc(this, 'vpc') だけで生成されたリソースです。

ご理解いただけたでしょうか。
プロパティを3つ指定したEC2インスタンスより、プロパティを一つも指定していないVPCのコードのほうがはるかに多くのリソースを自動的に生成するということです。

シンプルに書いたとしても、出力がシンプルになるとは限らない

プロパティを指定しない場合に、どこまでおせっかいを焼いてくれるのかはサービスによって全く異なります。
不用意にデプロイしてしまうと大量のリソースがスタック上に展開される可能性があります。
また、その逆に当然紐づくと思っていたリソースがないというケースもあり得ます。
いずれにしても注意が必要です。

一般的なプロパティでも油断は禁物

コード上で同じような役割に見えるパラメータでも、リソースによって処理が違う場合があります。
わかりやすい例として、名称を付与するパラメータ群があります。

例えば、aws_ec2モジュールのInstanceクラスはinstanceNameプロパティを設定できます。
実際に設定した場合、テンプレート上ではNameタグにインスタンス名が追加されます。

一方、aws_iamモジュールのRoleクラスでもroleNameプロパティが設定できます。
設定するとテンプレート上ではNameタグが出力…されません
代わりに、コードと同名のRoleNameプロパティが生成されます。

このように、一見して役割が似ているように思えても、挙動まで同じとは限りません。
思い込みで実装してしまわないようにしましょう。
実装したらcdk synthコマンドでテンプレートの出力を確認することをおすすめします。

振り返ってみると、AWSマネジメントコンソールにおいても各サービスにおける操作感は微妙に異なりますよね。
その傾向がAWS CDKにも受け継がれているということなのかもしれません。

ただし、柔軟性は高い

このような状況があるためか、AWS CDKは非常に柔軟な実装が可能です。

比較的ハードルが低い手法としては、エスケープハッチがあります。
これはAWS CDKにおけるリフレクションのようなものです。

AWS CDKでは、AWSマネジメントコンソールで設定可能なプロパティを一部網羅していないクラスがあります。
そのようなクラスでも、より低級なレイヤーのクラスにキャストすることで設定できる場合があります。

※参考 アブストラクションとエスケープハッチ
https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/cfn_layer.html

それでも対応できない場合は、カスタムリソースという手法も取れます。
端的に言うとSDKのコードでリソースを操作する処理をCDKのコードに埋め込むことができます。
(管理が複雑になりますので、カスタムリソースは最終手段的な位置づけになると思います)

まとめ

開発経験がある場合、AWS CDKはとっつきやすいソリューションです。
しかし、その挙動はとっつきやすいとは言い難い複雑さを持っています。
とにかくトライアンドエラーで覚えていきましょう。

私もAWS CDKにより馴染めるよう、努力していきたいです。

Last modified: 2023-09-02

Author