CDKを使いこなすために押さえておきたいポイントを紹介!
はじめに
こんにちは、inoです!入社して早1年が経ちました。
これまでAWS CDKを用いたIaC開発に携わってきましたが、その中で「なるほど、こういう仕組みだったのか」と学びになった場面が何度かありました。
CDKはTypeScriptなどのプログラミング言語でインフラを定義でき、非常に強力なツールです。一方で、内部ではCloudFormationを経由してデプロイする仕組みになっているため、その特性を押さえておくとよりスムーズに開発を進められます。
今回は、自分が実際に経験したケースをもとに、CDK開発で押さえておきたいポイントとその対応策を紹介します。
クロススタック参照の密結合によりデプロイできない
スタック間でリソースを共有している場合に、その参照を外そうとすると Export ~ cannot be deleted as it is in use by ~ というエラーでデプロイが失敗することがあります。CDKが自動的に設定するスタック間の依存関係と、デプロイ順序を意識しておく必要があります。
なぜ起こるのか
CDKでは、あるスタック(参照元スタック)で作成したリソースを別のスタック(参照先スタック)にpropsで渡すだけで、スタック間のリソース共有ができます。
const stackA = new StackA(app, "StackA"); // 参照元スタック
const stackB = new StackB(app, "StackB", {
parameter: stackA.parameterA, // 参照先スタックにpropsで渡すだけ
});このとき、CDKは裏側で参照元スタックにExport用の CfnOutput を、参照先スタックに Fn::ImportValue を自動設定します。
ここで、参照先スタックでこのリソースが不要になり、参照を削除してデプロイしようとすると、エラーになります。
参照を外すと、CDKは参照元スタックのExportと参照先スタックのImportの両方を削除しようとします。CDKは依存関係の上流である参照元スタックから先にデプロイするため、一時的に「参照元スタックのExportは削除されたが、参照先スタックにはまだImportが残っている」という状態になり、CloudFormationがこれを拒否します。
恒久対応:スタックを不必要に分割せず、場合によってSSMパラメータストアを使う
あらかじめ分離が必要だとわかっている場合を除いて、できるだけ同じStackにリソースをまとめておくことが最も簡単な対応になります(ただし、リソース数の上限には注意)。コードを整理したいだけであれば、スタックを分けるのではなくConstructを使ってモジュール化するのが適切です。Constructであれば同一スタック内なので、クロススタック参照の問題は発生しません。
それでもスタック分割が必要な場合は、スタック間の値の受け渡しにSSMパラメータストアを使うことで、CloudFormationのExport/Importによる密結合を避けられます。
応急対応:exportValueで既存のExportを維持する
すでにクロススタック参照が成立している状態で参照を外す必要がある場合、参照元スタックで exportValue を使い、Exportを明示的に残したままデプロイします。
// 参照元スタック側(StackA)でExportを維持する
this.exportValue(this.parameterA);こうすれば参照元スタックのExportが維持されるので、参照先スタックから参照を削除してもエラーになりません。両方のデプロイが終わったら exportValue も削除して大丈夫です。
Constructの階層変更でリソースが再作成される
コードのリファクタリングでConstructの階層やIDを変えると、デプロイ時にリソースが削除・再作成されることがあります。物理名を指定しているリソースではAlready existsエラーが発生し得るため、仕組みを理解しておくことが大切です。
なぜ起こるのか
CDKは、コード内のConstructツリーの階層構造に基づいてCloudFormationの「論理ID」を自動生成します。たとえば、あるリソースの定義場所(スコープ)を別のConstructに移動したり、Constructに渡すIDの文字列を変更したりすると、生成される論理IDが変わります。
CloudFormationはリソースを論理IDで管理しているため、論理IDが変わると「古いリソースの削除」と「新しいリソースの作成」として扱われます。
ここで押さえておきたいのが、CloudFormationの処理順序です。「新しいリソースの作成」→「古いリソースの削除」の順で実行されるため、リソースに物理名(例:バケット名やロール名)を明示的に指定していると、同名のリソースを作成しようとしてAlready existsエラーになります。
対応策:リソースに物理名をつけない
AWS公式のCDKベストプラクティスでは、CDKで作成するリソースに明示的な物理名をつけず、CDKが自動生成する名前を使うことが推奨されています。
より良い方法は、できるだけ名前を指定しないことです。リソース名を省略すると、一意の新しい名前が生成されるので、この種の問題は発生しません。
引用:AWS CDKでクラウドアプリケーションを開発するためのベストプラクティス
物理名がランダムであれば、論理IDが変わってリソースが再作成される場合でも、新しいリソース名が旧リソースとバッティングすることはなく、Already existsエラーは回避できます。
ただし、運用上名前を指定した方が管理しやすい場合などは、必要に応じて明示的に名前をつけることもありだと思います。
手動変更がcdk deployで上書きされない
マネジメントコンソールやCLIでリソースの設定を直接変更した後、cdk deployを実行しても手動変更が元に戻りません。CDKのデプロイの仕組みを知っていれば納得できる挙動なのですが、知らないと「なぜ上書きされないのか」と戸惑うポイントです。
なぜ起こるのか
CDK(CloudFormation)のデプロイの仕組みに関係しています。
cdk deployを実行すると、CDKは「現在のAWS上の実リソースの状態」を見に行くわけではありません。「CloudFormation上のスタック」と、「今回のコードから生成された新しいテンプレート」を比較し、差分があった箇所だけを更新します。
つまり、マネジメントコンソールやCLIで直接リソースの設定値を変更した場合、CloudFormation上のスタックは変わらないため、次のcdk deployでCDKのコード側に変更がなければ「差分なし」と判定され、手動変更がそのまま残ってしまいます。
恒久対応:手動変更を行わない運用ルールの徹底
根本的には「CDKで管理しているリソースには手動変更を加えない」という運用ルールを徹底することが一番簡単な対策です。緊急時にやむを得ず手動変更を行った場合は、速やかにCDKコードにも同じ変更を反映してデプロイし直し、コードと実態の乖離を最小限にとどめるようにします。
応急対応:ドリフトの検出で差分を把握する
すでに手動変更が入ってしまっている場合は、CloudFormationのドリフト検出を使って差分を確認します。
# ドリフト検出の開始
aws cloudformation detect-stack-drift --stack-name MyStack
# 結果の確認
aws cloudformation describe-stack-resource-drifts --stack-name MyStack差分を把握した上で、次のいずれかの方法で整合性を取ります。
- 手動で変更した値をCDKのコードに合わせて元に戻し、CDKのコードと実態を一致させる
- CDKのコード側を実態に合わせて修正し、
cdk deployで反映する - CDK管理外になったリソースについては、
cdk importで改めてCDKの管理下に取り込む
いずれにしても、ドリフトが放置されるとどんどん把握しづらくなるため、定期的にドリフト検出を実行する運用を入れておくと安心です。
CloudFormationのリソース数上限に引っかかる
cdk deployが Maximum number of resources exceeded で失敗するケースです。CloudFormationには1スタックあたりリソース数500個というハードリミットが設けられています。開発環境では余裕があったのに、本番相当の構成にしたタイミングで上限に達するパターンがあります。
なぜ起こるのか
CDKの1スタック=CloudFormationの1テンプレートなので、CloudFormationのクォータがそのままCDKの制約になります。
CloudFormationにはいくつかのハードリミットがあり、1スタックあたりのリソース数上限は500個です。
| 項目 | 上限 |
|---|---|
| スタックあたりのリソース数 | 500 |
| スタックあたりの出力数 | 200 |
| スタックあたりのパラメータ数 | 200 |
上述の「クロススタック参照」を避けるために、とりあえずで1つのスタックにリソースをまとめている場合、今度はこちらの制限が効いてくることがあります。
大規模開発であったり、将来的にリソース数増加が見込まれるシステムの場合は、500個のリソース数上限には注意して設計しておくべきです。
対応策:設計段階でリソース数を試算し、スタック分割を計画する
スタック分割を後から行うと、論理IDの変更によるリソース再作成が発生し、追加コストが大きくなります。そのため、設計段階で将来的なリソース数の増加を見積もり、早い段階でスタック分割の方針を決めておくことが重要です。
分割方式としては、大きく2つのアプローチがあります。
- 独立したスタックに分割:完全に独立してデプロイ可能だが、スタック間の値の受け渡しが煩雑になる
- Nested Stack(親子スタック構成):親スタックからの参照が容易だが、親と一緒にデプロイされるため分離はできない
Nested Stackの場合、子スタック内のリソースは親スタックのリソース数カウントに含まれません。
おわりに
今回紹介した内容はいずれも、CDKの裏側で動いているCloudFormationの仕組みを理解していれば事前に対処できるものばかりです。CDKはプログラミング言語の力を活かしてインフラを効率的に構築できる優れたツールです。その力を最大限に活かすためにも、CloudFormationの特性を理解した上で設計していきたいですね。
この記事が、同じような場面で困っている方の参考になれば幸いです。
