こんにちは。自称ソフトウェアエンジニアの橋本 (@hassaku_63)です。
本記事は CDK と TypeScript の初級者向けです。今回の題材に限らず割と頻出テクニックだと思うので、手探りに実装している人はぜひ参考にしてください。
この記事では「dev 環境では試行錯誤するために DeletionPolicy, UpdateReplacePolicy を "DELETE" に強制セットする方法」を紹介します。
また、最後のセクションでは「慣用句」的な表現を覚え、共有することの重要性について私見を述べます。
前提: 用語の定義
dev, stg, prod のような、その環境の位置付けを定義する名称を私たちはよく使います。いわゆる「環境」とか「ステージ」のように呼ばれることが多い概念だと思います。この記事では一貫して「ステージ」と呼称します。
この記事で実装するもの
開発プロジェクトががっつり検証中のフェーズにあるとき、CloudFormation Stack ごと気楽に削除して再デプロイしたくなる場合があると思います。
たとえば CloudWatch LogGroup や DynamoDB Table, ECR Repository など、CDK 的にデフォルトで RETAIN ポリシーが設定されているリソースがあります。こうしたリソースを構築した場合、cdk destroy の実行時にエラーを生じてしまう(もしくは残存リソースになってしまう)場合があります。Stack ごと破壊して試行錯誤を回したい開発初期のフェーズでは、これはあまりうれしくありません。
よって、検証開発を行っている「ステージ」でのみ、DeletionPolicy と UpdateReplacePolicy を "Delete" にセットすることで、CloudFormation リソースの削除が問題なく行えるようにします。
これを CDK の Aspects という仕組みを利用して実装してみます。
一方で、上記の属性を "Delete" にセットする扱いは、基本的に自分の手元で動かす場合以外はどのようなステージでも避けた方がよいものです。うっかり本番環境にこの設定が適用されてしまうような事態は避けたいです。よって、ここでもう1つの要件を加味します。すなわち、いわゆる「個人検証用途」に対応するステージでのみ DeletionPolicy と UpdateReplacePolicy の変更を行うような振る舞いを追加します。本記事ではこのステージを "dev" とします。
前フリ: Aspects の紹介
CDK Aspects は construct tree の任意のノード(およびその子ノード全体)に対して何らかの処理を適用するユースケースで利用できます。このあたりは CDK 公式ドキュメントの "Conpects" に記述があります。
Aspects の最も典型的な使用例はタグの一括付与でしょう。CDK でも非常に典型的なユースケースとして見ているのでしょう、Aspects の実装として Tags クラスが標準提供されています。CDK App あるいは Stack に対して、タグの付与が可能なリソースタイプすべてに特定のタグを付与する記述が、CDK では1行で実装可能です。この方法は CDK Concepts の Tags のページでも紹介されています。以下、ドキュメントから利用方法を引用し紹介します。
Tags.of(myConstruct).add('key', 'value');
この実装により、myConstruct という Construct とその配下にあるタギング可能なリソースに key=value
のリソースタグが付与されます。個別リソースに記述する方法よりも簡素で、かつ漏れがないのが非常に嬉しいポイントですね。myConstruct の部分には CDK App や Stack のインスタンスを入れるのが典型的です。
Aspects の応用はそれこそいくらでもありますが、私個人はデプロイ前に機能する「バリデーション」を実行する目的で利用するのが一番典型的で、かつ CDK の旨味を良く引き出せる使い方だと考えています。是非覚えましょう。
Aspects の背後にある設計パターン
Aspects は GoF デザインパターンの Visitor を応用したものです*1。
原典*2では、ユースケースとして AST(抽象構文木)のような木構造の走査を行うユーティリティが紹介されています。CDK の Construct tree も木構造を有してます。また、それぞれのノード (construct) に対する振る舞いを実装したいというユースケースにも適合します。
Visitor パターンを実装した Aspects ユーティリティの提供は非常に合理的です。
(2024/09/18) 追記
CDK で採用されている GoF デザインパターンの話...ということであれば、AWS Dev Day 2023 での後藤さん (@365_step_tech) の登壇をご紹介せねばなりません(書いてる途中はすっかり失念しておりました)。デザインパターンについてもっと知りたくなった方は、ぜひ以下の記事を見てみてください。リンク先の記事末尾から、AWS Dev Day 2023 の登壇資料もご確認いただけます。
実装
bin/
配下で CDK App を定義しているモジュールで、次のように実装します。
いくつかポイントがありますので、サブセクションで解説します。
Stage の型定義と型ガードの実装
対応する実装箇所は次のブロックです。
const availableStages = ['dev', 'stg', 'prod'] as const; type Stage = typeof availableStages[number]; // 'dev' | 'stg' | 'prod' // 型ガード関数。この関数の戻り値は boolean function isValidStage(stage: string): stage is Stage { return availableStages.includes(stage as Stage); } // context key 'stage' の値を取得。 // この例では、デプロイ時に cdk deploy -c stage=dev のように指定することでステージの値を与える想定 const stage = app.node.tryGetContext('stage'); if (!isValidStage(stage)) { throw new Error(`Invalid stage: ${stage}`); } // これ以降では、変数 stage が Stage 型として推論される
TypeScript の型ガードを用いることで、Context から受け取ったステージ引数が想定するステージ (dev/ stg/ prod) のいずれかであることを保証できます。 実際に使ってみるとわかりますが、エディタ上で型推論が働くのが非常にありがたいです。TypeScript を用いるうえでは、型推論が機能するように書くことが非常に重要です。型ガード関数は基礎テクニックですので、必ず覚えておきましょう。
型ガードについては日本語でアクセスできる情報源がすでにありますので、詳しくはそちらに譲ります。以下のページを参考にしてください。
型ガード | TypeScript Deep Dive 日本語版
型ガード関数 (type guard function) | TypeScript入門『サバイバルTypeScript』
今回提示した型ガード関数 isValidStage
では、戻り値の宣言に stage is Stage
と記述しています。こう書くことで、「戻り値が true であれば、stage 引数は "Stage" 型として推論してよい」と明言していることになります。あくまでこれは「TypeScript のコンパイラにそう伝える」というだけの話なので、型ガード関数の中身の妥当性は書き手が保証する必要がある点に注意してください。
このセクションで示した内容には、型ガード関数以外にもポイントがあります。以下の記述です。
const availableStages = ['dev', 'stg', 'prod'] as const; type Stage = typeof availableStages[number];
availableStages[number]
の部分が、知らないと意味不明に見えるかもしれません。これは Indexed Access Types(インデックスアクセス型)と呼ばれます。Indexed access types に関するドキュメントは、以下のページをご参考ください。
TypeScript: Documentation - Indexed Access Types
インデックスアクセス型 (indexed access types) | TypeScript入門『サバイバルTypeScript』
typeof availableStages[number]
と記述することで、 availableStages
の配列変数の各要素の値を typeof して得られる型が左辺の Stage 型の定義となります。
直前の availableStages の宣言に as const
が入ってる点もポイントです。こう書くことで、availableStages の型推論は dev, stg, prod の3つの文字列要素のみを持つ readonly な配列型として認識されます。試しにコードを手元にコピペして、as const を削除して型推論がどうなるかを確認してみると良いでしょう。availableStages は string の配列型 string[]
として推論され、それによって Stage 型の推論結果も期待に沿わず string 型として推論されてしまいます。
自作 Aspects "EnforceDeletionPolicy" の実装
dev ステージでのみ適用したい「ポリシー」を実装します。これは IAspects インタフェースを継承するクラスとして実装します。
class EnforceDeletionPolicy implements cdk.IAspect { public visit(node: IConstruct): void { // すべての CloudFormation リソースに適用 if (node instanceof cdk.CfnResource) { // DeletionPolicy および UpdateReplacePolicy を "Delete" に上書きするメソッド node.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY); } } } // dev ステージの場合だけ適用 if (stage === 'dev') { // CDK App とその配下の construct ノードに対して EnforceDeletionPolicy を適用する cdk.Aspects.of(app).add(new EnforceDeletionPolicy()); }
visit メソッドで、自前適用したいロジックを実装する必要があります。パラメータの node は、現在走査されている construct を示します。今回の実装では、次のようにしています。
- visit に渡されたすべての construct のうち、CloudFormation リソースであるもの (CfnResource) すべてに DeletionPolicy = "Delete" および UpdateReplacePolicy = "Delete" を適用
- (Context 経由で渡された)デプロイステージが "dev" である場合のみ、CDK App 全体にこの Aspects を適用する
Aspects.of(...)
で渡した construct が、その Aspects の適用範囲になります。ここに入る値は App や Stack オブジェクトである場合が多いと思います。今回の例では App を渡しています。
まとめ & おまけ
同じことを CloudFormation だけで実装しようとするのは、なかなか骨が折れるのではないかと思います。ちょっとしたコードの書き方や CDK のイディオムさえ知っておけば、少ない行数でやりたいことを明瞭に表現できる CDK のことが、私は好きです。
「人力で」気をつける必要はありません。CDK に任せましょう。
最後に、補足的なパートをいくつか添えます。
おまけ(1) - removalPolicy の設定だけでは不十分なリソースもある
例えば ECR リポジトリの場合、すでにイメージが push されている場合は削除をブロックする仕様があります。removalPolicy の設定だけでは、期待通りに cdk destroy がすべてを片付けてくれません。
ECR Repository に関しては、emptyOnDelete
というプロパティを非デフォルトの true に指定すれば良いです。これを指定すると、push 済みのイメージが存在する場合でも強制的にレポジトリを削除できます。
class Repository (construct) · AWS CDK
これを先ほどの Aspects に組み込んだ実装例は次のようになります。
class EnforceDeletionPolicy implements cdk.IAspect { public visit(node: IConstruct): void { if (node instanceof cdk.CfnResource) { node.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY); } // 追記↓ if (node instanceof CfnRepository) { node.emptyOnDelete = true; } } }
リソース自身がステートフルである場合はこうした個別の考慮が必要になる場合もありますので、詳しくはそれぞれの Construct クラスのリファレンスをご確認ください。
おまけ(2) - CDK とバリデーション
ことバリデーションという文脈で CDK を語るとき、国内にはすでに素晴らしい先駆者がいらっしゃます*3。その方が AWS に寄稿された記事をこの場でご紹介します。
この記事では、バリデーションという目的に対して Aspects を含む複数の手段が使えることを学べます。「CDK の旨味を引き出す実装」にお困りであれば、まずはこの記事を見てみましょう。バリデーションは非常に実務的・実践的なトピックであり、そのメリットもわかりやすいです。バリデーションが実装できるようになると、間違いなくあなたの CDK ライフの質は向上します。
おまけ(3) - 共通語彙やイディオムを学び、利用しよう
最後に、「共通語彙」や「イディオム」を学び、共有し、利用することに関する意義について私見を添えて締めたいと思います。
Visitor パターンの存在や CDK Construct tree, Aspects の概念を知っておけば、Aspects を用いた実装が出現した際に読み手が推測できる情報量は格段に増えます。Construct tree に対して適用したいなにかしらの処理があるらしい、という意図は一瞬で伝わります。これこそが適切な抽象化の力であり、語彙を育てる意義だと思います。
共通語彙を使うことで一度に共有できる情報量・コンテキストが豊かになれば、短い言葉でより多くのことを、より正確に語れるようになります。専門家が適切な場面で適切に専門用語を用いることで、簡潔に・正確に・本質的な(すなわち効率的な)議論を行うのと同じです。日頃のコミュニケーションが正確で効率的に行われることは、チームの生産性向上に直結しています。齟齬なく効率的に日々のコミュニケーションが取れるチームは、それだけ仮説検証のサイクルを早く、深く回せます。結果として「正しいものを正しく作る*4」が圧倒的にやりやすいのです。
チームビルディングや引き継ぎの場面において、よく「初心者にもわかるように」という趣旨の意見が出たりします。その心構えは一見立派に見えますが、その裏では上記のような生産性とのトレードオフを選択しているとも言えます。誰にでも...それこそ初心者レベルでも分かるような語彙しか使わない/使えない状況は、チームの生産性の上限を引き下げる可能性があるのです。初心者に対するケアを重視したい場面も当然にあると思います。しかし、その背後にあるトレードオフも含めて選択したのだという自覚は持っておく方が良いでしょう。
また、語彙を適切に共有することでチームの生産性が上がるのは、何も CDK に限ったことではありませんね。言葉の解釈が合わなかった、あるいは不一致であることに気付かぬまま話を進めてしまったことで生じる停滞や不都合は、読者のみなさまも数多くご経験されていると思います。こういった観点からも、適切な共通語彙やイディオムを適切に学び、構築し、共有し、使用することに意義があると言えます。
願わくば、自らの専門性によって価値提供を行う集団においては「誰もがすぐ参画できる敷居の低い環境の整備」よりも「高い要求水準に到達しやすくなる支援活動」により多くの投資をしたいものです。この記事を書く目的にはこうした意識も含まれています。本記事が社内的にも、社外の CDK ユーザーにとっても、「慣用句」を学ぶ一助になると幸いです。
(2024/09/17) 追記
AWS さんの公式ブログからも、非常に実践的な Aspects の応用例の記事が出ています。ぜひこちらも参考にしてみてください。
CDK Aspectsを利用してベストプラクティスに従ったインフラストラクチャを構築する | Amazon Web Services ブログ
*1:GoF デザインパターンは20年以上前に提唱された考え方であり、今でもなお有用なパターンが多数存在します。Visitor はその一つです。余談ですが、私は Observer が最推しです
*2:邦訳版: オブジェクト指向における再利用のためのデザインパターン 1999/10/1, Erich Gamma 著
*3:なお、執筆者の後藤さんはバリデーションや国内に限定せずとも CDK 界隈のトップランナーです
*4:言葉の出典: 正しいものを正しくつくる プロダクトをつくるとはどういうことなのか、あるいはアジャイルのその先について 2019/6/14, 市谷 聡啓 著