初めてのAWS CDKで検討したことや学んだことのまとめ

記事タイトルとURLをコピーする

AWS CDK に触る機会があったので、どのような流れで検討を進めたかという軸で、学んだことや個人的にベストプラクティスだと感じたものをつらつらと書いていきます。

AWS CDK 初心者向けの記事なので、AWS CDKを始める第一歩として読んでいただければ幸いです。

AWS CDK を学ぶ上で読んだ公式ドキュメント

まずは、AWS CDK 使わなければいけないと決まってから実際に読んで、勉強になった公式ドキュメントを書き記します。

AWS CDK ハンズオン

AWS CDK の公式ハンズオンです。 最後までやれば、AWS CDK でリソース構築できる最低限の知識は身に付くと思います。

※ハンズオンの内容は aws-cdk v1 を基にしているので、aws-cdk v2 を利用する場合は読み換えてあげる必要があります※

Best practices for developing cloud applications with AWS CDK

AWS CDK の公式ベストプラクティス集です。 AWS CDK の設計する際に非常に参考になると思います。

AWS CDK を採用するかを決めるポイント

CloudFormationやマネコンでのデプロイなど色々な選択肢がある中で、AWS CDK を採用する場合、検討すべきポイントをピックしました。
以下のいずれか、または複数に対して YES と答えられるのであれば AWS CDK を採用する価値は十分あるのではと考えています。(上に行くほど重要度は高いイメージ)

  • L2コンストラクトを利用するか?
  • 開発メンバーのいずれかがAWS CDK の前提となる十分な知識・経験を持ちうるか?

L2コンストラクトを利用するか?

AWS CDKを採用するかどうか決めるうえで、L2コンストラクトを利用するか否かは非常に重要な要素だと思っています。 なぜならば、複数リソースをスピーディーに構築することが可能だからです。

L2 コンストラクトとは、AWS より提供される AWSリソース群を抽象化したより高レベルなコンストラクトです。
L2コンストラクトを利用すると、記載するコード量を大幅に減らしてリソース定義できます。

例えば、AWSでよくある 「VPC×1, PublicSubnet×2, PrivateSubnet×2, NGW×2, IGW×1」のNW構成を、以下コードで定義可能です。

import * as ec2 from '@aws-cdk/aws-ec2';

const vpc = new ec2.Vpc(this, 'Vpc', {
  maxAZs: 2,
});

参考: Vpc コンストラクト https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ec2.Vpc.html

また、L2コンストラクト同士は、参照をインスタンスで渡すだけで実現できるので、この点でもコード量がグンと減ります。

import * as ec2 from '@aws-cdk/aws-ec2';

const vpc = new ec2.Vpc(this, 'Vpc', {
  maxAZs: 2,
});
const sg = new ec2.SecurityGroup(this, 'Sg', {
  vpc: vpc, //ここで Vpc のインスタンスを渡す
})


ただし、L2 コンストラクトはデメリットも孕んでいます。

まず、抽象化されているが故、細かいパラメータ制御が出来ません。最悪、設計したパラメータが設定できない可能性があります。

また、一つのコンストラクトでは多くのリソース群が抽象化されているので、想定外のリソースをデプロイする可能性があります。

従ってL2コンストラクトを利用する場合、

  • その L2 コンストラクトがどのリソースをデプロイするのか?
  • その L2 コンストラクトは必要な機能を設定できるのか?

という観点で確認する必要があります。 もしこの中で満たせない要件があれば、その部分を妥協するもしくは、L1, L3 コンストラクト, 自作コンストラクトのいずれかを検討しましょう。

ちなみに L1 コンストラクトは、CloudFormationのリソース定義とニアリーイコールなので、最悪、コード量がほとんど代わらない可能性があります。基本的には、for文やオブジェクト指向で書けるなどCloudFormationよりやれることは多いので、さすがに CDK の方がコード量が少なくなると思いますが。

チームでAWS CDKの知見がないのであれば、CloudFormationを再検討してもよいかもしれません。(ただし、どこかで誰がか苦労することを考えると、やるべきかもしれませんが...。)

従って、以上より L2 コンストラクトを採用するか否かは AWS CDK の採用を決定する上で重大な要素の一つと考えています。

開発メンバーのいずれかがAWS CDK の前提となる十分な知識・経験を持ちうるか?

前提として、CDKテンプレートを定義するために、以下要素が必要と感じます。

  • AWSリソース全般に対する基礎知識
  • (あると望ましい)Cfnテンプレートを使った構築経験
  • プログラミング開発経験
  • (あると望ましい)静的型付け言語の開発経験

どれか一つが欠けていると実装までのハードルがグンと上がります。
そもそも単純な実装にも時間がかかりますし、
例えば、エラーが発生した場合、それが「プログラミングレイヤー, CDKレイヤー, Cfnレイヤー」のうちどのレイヤーで発生しているのか特定するのが大変です。

なお、4つめの静的型付け言語の開発経験は、動的型付け言語を利用する前提ならば不要です。

開発メンバーのいずれも満たせていない条件があるかつ、開発完了までに十分な時間を確保できないのであれば、AWS CDK の採用は見送るべきかもしれません。

AWS CDK を用いた開発を始めるまえに検討すべきポイント

AWS CDK の採用が決定後、開発を始めるまでに検討すべきポイントについて記述します。

AWS CDK でどの言語を採用すべきか?

AWS CDK は複数言語で一般公開されているため、どのプログラミング言語を採用するか?が最初の検討ポイントになるかと思います。

AWS CDK は、JavaScript、TypeScript、Python、Java、C# が一般公開されており、開発者プレビューでは Go がサポートされています。

引用元: https://aws.amazon.com/jp/cdk/faqs/#:~:text=Q%3A%20AWS%20CDK%20%E3%81%A7%E3%81%AF%E3%81%A9%E3%81%AE,%E3%81%8C%E3%82%B5%E3%83%9D%E3%83%BC%E3%83%88%E3%81%95%E3%82%8C%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99%E3%80%82

個人的には TypeScript がオススメ ただしTypeScriptは静的型付け言語なので、動的型付け言語しか扱ったことない方は注意ください。
僕は、はじめてのCDK, はじめての静的型付け言語だったため、地獄を見ました。
ただ、静的型付け言語は非常に可読性が高く、型に対する理解度も上がるので、やってよかったなあと感じてはいます。

どの IDE を採用するか?

基本的には慣れ親しんだ IDE を利用するで問題ないと思います。

もし、採用するIDEを迷っているならば、Cloud9 がオススメです。
AWS CDK に最低限必要なパッケージや git などが最初からそろっているため、スピーディかつ、ローカル環境を汚さず、開発できます。
リモート環境なので、複数人との共有を簡単に行える点も素晴らしいです。

※Cloud9 は t3.small 以上のインスタンススペックがおすすめです。
t3.micro で構築した際、頻繁にCPU使用率が100%に達したためです。※

なお、VSCodeでやる場合は、 AWS Toolkit for Visual Studio Code エクステンションを利用すると開発が楽になります。

参考: https://aws.amazon.com/jp/visualstudiocode/

スタック分割の設計思想とスタック参照

AWS CDK では、スタック設計が全体のディレクトリ構成に大きく影響するため、非常に重要な検討ポイントとなります。

検討するにあたっては、スタック設計における下記のAWSドキュメントが勉強になりました。

ライフサイクルと所有権によるスタックの整理

引用元: https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/best-practices.html#organizingstacks

上記と今までの経験から、まずはサービス単位でスタック分割を検討し、さらにライフサイクルが異なる機能があればスタック分割するのがベストだと感じました。

ライフサイクルが異なる機能とは、例えばメインロジック層とストレージ層などの場合です。
サービスがクローズしても、ストレージ層は別サービスで参照する、監査などの観点でしばらく残すなど、ライフサイクルが異なる可能性があります。このような場合は影響範囲を分ける狙いで、ライフサイクルによってスタック分割すべきかもしれません。

ただし、個人的にはAWS CDK におけるスタック分割は最小限にとどめるべきだと考えています。 理由として AWS CDK はクロススタック参照と相性が悪く、リソースの「デッドロック」を発生させる可能性があるためです。

リソースのデッドロックとは、クロススタック参照を解消するような更新デプロイをCDKから行う時、CloudFormationがエクスポートしたリソースを削除できず、デプロイ失敗する事象です。
回避する方法はありますが、運用が複雑化するリスクを孕むので、なるべく避けたい事象です。
詳細については、以下を確認ください。

https://www.endoflineblog.com/cdk-tips-03-how-to-unblock-cross-stack-references


以上から、スタック分割を検討する場合は、なるべく分割しないことを念頭に置きつつ、影響範囲を分けたいなど理由がある場合は分割を行うというフローで進めるのがベストだと思いました

AWS CDK のベストプラクティスなフォルダ構成

AWS CDK のフォルダ構成も重要な検討ポイントです。可読性や開発スピードに関わるので、最初に検討すべきだと思います。

私の中でのベストプラクティスな構成は以下に落ち着きました。(重要そうなファイルにのみコメント記載)

.
├── bin //app定義ファイルを格納するフォルダ
├── cdk.json
├── cdk.out
├── config //環境差分を管理するファイル群を格納するフォルダ
│   └── dev.ts
├── jest.config.js
├── lib
│   ├── constructs //自作constructを格納するフォルダ
│   │   └── myVpc.ts
│   └── hogeStack.ts //stack定義ファイル
├── node_modules
├── package.json
├── package-lock.json
├── README.md
├── test
└── tsconfig.json

基本的には、cdk init で構築されるフォルダ構成に従って構築するのがベストだと感じているので、そのまま従っています。

ただし、2点異なるところがあります。



1点目が config ディレクトリを新規作成している点です。

├── config //環境差分を管理するファイル群を格納するフォルダ
│   └── dev.ts

AWS CDKでは、環境ごとに動的に変化するパラメータ(以下、環境差分)を管理する仕組みとして、 cdk.json に記述していく方法があります。 ただし、環境差分が大量にある場合、ファイルが肥大化し、可読性が大きく下がります。

従って、私は環境ごとに動的に変化するパラメータを config モジュールにより管理していました。 config モジュールを使用すると、環境ごとにファイル分割ができるので、可読性を保つことが可能です。

参考: https://www.npmjs.com/package/config

なお、「どのファイルをconfigもジュールが参照するか」は、環境変数 NODE_ENV で制御します。例えば、dev.ts を参照させたい場合、cdkコマンド実行前に以下コマンドを打ちます。

$ export NODE_ENV=dev



2点目が lib 配下に constructs フォルダを新規作成してる点です。

├── lib
│   ├── constructs //自作constructを格納するフォルダ
│   │   └── myVpc.ts

このフォルダは、L1やL2コンストラクトを抽象化した、自作コンストラクトのファイル群を格納します。

リソースが少ない場合は、スタック定義ファイル(ここだと hogeStack.ts)に直接記載してもよいですが、記載量が増えると可読性が下がります。

そのため、スタック定義ファイルから外だしし、下記のようにスタック定義ファイルにてインポートします。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { myVpc } from './constructs/myVpc' //constructsフォルダからインポート
import config = require('config')

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

    new myVpc(this, 'myVpc', config.get('myVpc')) //インポートした myVpc をインスタンス化
  }
}



この構成によって、可読性を確保しつつ、迷いの少ない開発が出来ました。

テンプレートへのテスト

AWS CDK では、作成した Cfnテンプレートに対してテストが可能です。よって、テストをやるかやらないか事前に決めておく必要がございます。

テスト設計は大きく2種類に分かれます。詳細は参考URLをご確認ください。

  • Snapshot Testing
  • Fine-grained Assertions

参考: https://pages.awscloud.com/rs/112-TZM-766/images/CDK%E3%81%A7%E3%82%82%E3%83%86%E3%82%B9%E3%83%88%E3%81%8C%E3%81%97%E3%81%9F%E3%81%84.pdf

私の場合は、Fine-grained Assertions を基にテストを行いました。

より細かく言うと、「作成した Cfnテンプレートに対して、意図した数だけリソースが生成されているか?」という観点でテストを行いました。
例えば以下です。

//VPCが一つだけリソース生成されていることを確認する
template.resourceCountIs(type:'AWS::EC2::VPC', count:1) 

このテストならば、短時間で、直感的にテストを実装できます。

また、L2コンストラクトを採用する場合は、想定外のリソースが出来ていないか確認する契機にもなります。
実際に Bucket コンストラクトをデプロイした際に、カスタムリソース用Lambdaをデプロイされていることに気付きました。(※1)

※1.Bucketコンストラクトで autoDeleteObjects を true でデプロイすると、バケット内のオブジェクトを削除するカスタムリソース用Lambdaがデプロイされます。 https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-s3.Bucket.html

テスト設計することで、テスト駆動開発もできるようになるので、ぜひ検討してみてください。

その他Tips

最後に AWS CDK の実装に学んだことを記載します。思いついたらまた追加します。

fromxxx メソッドは新規リソースを参照できない

L2コンストラクトでは、fromxxx から始まるメソッド群が提供されています。
例えば、Vpcコンストラクトだと、fromLookup や fromVpcAttributes が提供されています。

参考: https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ec2.Vpc.html#static-fromwbrlookupscope-id-options

これらはすでに環境内にデプロイされたリソースを参照するメソッドのため、これからデプロイ予定のリソースを参照することができません。

にもかかわらず、私はL1コンストラクトからL2コンストラクトを生成する目的で、この fromxxx メソッドを利用していました。
当然、L1コンストラクトはデプロイ前なので、参照できずエラーとなりました。

意外と気づかずハマってしまう人もいるかもしれないので、今回記載しておこうと思います。

なお、結局、L1コンストラクトからL2コンストラクトを生成する方法は不明のままなので、分かり次第追記します。(スタックを分けて、L1コンストラクト分をあらかじめデプロイするくらいしか思いつきませんでした。)

終わりに

以上、CDKについて色々と学んだことなどを書いてみました。

CloudFormationよりサクサク書けるのと、書かないとどんどん忘れてしまいそうなので、積極的に利用していきたいと思います。

以上ご覧いただきありがとうございました。

菅谷 歩 (記事一覧)