海外のCDKの知見を学ぼう! Advanced AWS CDK: Lessons learned from 4 years of use COM302 参加レポート #AWSreInvent2023

re:Invent2023で参加した「Advanced AWS CDK: Lessons learned from 4 years of use」のレポートです。感想多めです。
2023.12.06

はじめに

re:Invent現地参加組の佐藤智樹です。今回は、AWS DevTools HeroであるMatthew Bonigさんの登壇、「Advanced AWS CDK: Lessons learned from 4 years of use」に参加した際のレポート記事です。彼は4年間CDKの利用歴があり、その中で得た知見をもとにCDK Bookも共著で執筆されています。そんな4年間の中での学びが共有されたセッションでした。

最初の方は日本とも同じ課題や答えについて話されていたと感じたのですが、中盤から後半は日本ではあまり見たことの無い解決策や課題について語られていました。本稿では、日本と共通の課題や自分が初めて見る知見などにフォーカスして記載します。この記事を見て気になった方は、セッションの動画が公開されているので是非ご覧になってみて下さい!

セッション概要

Released in July 2019, AWS CDK has become a powerful infrastructure-as-code tool to help companies build complex systems on AWS. Its greatest strength, using general purpose languages to define your infrastructure, can also be its greatest weakness. This is because there are many different ways to write your code. Should you use a single stack or multiple? How can you use external data to define your infrastructure? How do you test your code? In this session, learn best practices, patterns, and workflows developed over the last 4 years of using AWS CDK.

翻訳

2019年7月にリリースされたAWS CDKは、AWS上で複雑なシステムを構築するための強力なIaCツールとなっています。汎用的な言語を使用してインフラを定義するその最大の強みは、同時に最大の弱点でもあります。これは、コードの書き方が多様であるためです。単一のスタックを使用すべきか、それとも複数を使用すべきか?インフラを定義するために外部データをどのように使用するか?コードをどのようにテストするか?このセッションでは、AWS CDKを使って過去4年間に開発されたベストプラクティス、パターン、ワークフローを学びます。

セッション動画

サンプルコード

全体構成

cdk-4year-agenda

セッション内容

ここからは登壇者の話した内容の概要説明と気になった部分を自分なりにコメントしています。結構感想が多めなので、発表の詳細が気になる場合は、動画をご覧ください。

Basics

今回の話のベースとなる設計について話します。基本となるApp、Stack、Constructの説明からProjenについての話などが展開されました。CDKを使っている人にとって特筆すべき点はなかったのですが、1点Projenの使用率は気になりました。CDK CommunityのSurveyではPJの開始時にProjenを使っているのは、14%でした。これは少ないという話をされてましたが、普段は自分も使っていなかったので逆に結構多くの方が使っているんだなという印象でした。

projen-use-survey

Pipelines

CDK Pipelineについて、StageやWaveデプロイ、デプロイ中にcurlのテストが組み込めることなどの説明がありました。特に気になったのが、StackStepsの部分でした。StackStepsでは、特定の処理をWaveのStackの中に挟むことが出来るので、例えば本番環境だけ承認プロセスを挟むといった処理がスタック外に分かりすくかけて、スタック内部の開発環境と本番環境の差異を減らしてコーディングできる書き方が紹介されていました。筆者としてはこれは知らなかったので、環境ごとに特殊な処置がある場合は純粋にCodePipelineを使うより、シンプルに書きやすくなっていると感じました。

参考

ApplicationStage.ts

...
      const database = new DatabaseStack(this, 'Database', {
        ...props,
        tablePrefix: props.tablePrefix,
      });
      this.stackSteps.push({
        stack: database,
        changeSet: [
          new ManualApprovalStep('ChangesetApproval', {
            comment: 'Please review the changesets',
          }),
        ],
      });
...

参考

PipelineStack.ts

    prodWave.addStage(prodPrimaryStage, {
      stackSteps: prodPrimaryStage.stackSteps,
    });

Project Layout

これも多くの質問が寄せられた内容だったみたいです。CDKのベース実装とは変えて以下のように実装されていました。アカウント名やリージョンを定義するconstant.tsやStackの配下をbackend/front/sharedに分けてその中にconstractを入れている配置でした。これは若干好き嫌いが分かれそうな気はします。

src
├--constant.ts
├--main.ts
├--stacks
|  ├--backend
|  |  └--ApiStack.ts
|  |     └--construct
|  |        └--TriggerTransCoding
|  ├--frontend
|  └--shared
...

Stacks

日本でも定番となっているシングルスタック構成とマルチスタック構成の比較の話でした。シングルスタック構成についての欠点で、リソース数上限の話をしていたのですが、当たることはほぼ無いだろうというコメントだったので海外ではAPI GatewayのEndpointごとにLambdaを作るのは少ないのかも?と感じました。

reinvent-multi-stack

ただ登壇者は経験上、手動でリソースが変更されたりテスト時の消し残しによってよく問題が起きるためマルチスタック構成を選択しているそうです。確かにCDKに精通している人間が常にいる場合はスタックの依存関係問題も慣れたものではあるので問題を起こしづらいのでこちらを選択しているのかと推測しました。

Assets

Asset(CDKにバンドルできるローカルファイル、ディレクトリ、Dockerイメージ)のより良い書き方について話されていました。悪い例としては、以下のようにアセットを記載してしますと開発/検証/本番などで複数のビルドが走ってしまいます。

image: AssetImage.fromAsset(path.join(__dirname,'','website'))

なので、同じAssetを各環境で使えるように、propsでAssetsを渡して上げて別でビルドすることを推奨していました。

image: props.imageAsset,

これは一般的な環境ごとにビルドせず、別でビルドしたAssetsを環境跨いで使いましょうという話でした。

Apps

同じApp()を使う場合でも一緒のアプリになるのではなく、呼び出し方で分離できる話がされていました。ここはprojenを使った書き方で、CDKをStatic パターン1の方法で書いてもallオプションが使えるようなので便利そうに感じました。

const app = new App();

new GitHubSupportStack(app, 'DevGithubSupport', {
  env: {
    account: DEV_ACCOUNT,
    region: PRIMARY_REGION,
  },
});
new GitHubSupportStack(app, 'ProdGithubSupport', {
  env: {
    account: PROD_ACCOUNT,
    region: PRIMARY_REGION,
  },
});
app.synth();
const baseAppCommand: string =
  'cdk -a "npx ts-node -P tsconfig.json --prefer-ts-exts';

project.addTask('cdk:github', {
  exec: `${baseAppCommand} src/GithubSupport.ts"`,
  receiveArgs: true,
});

補足:Static パターン1について

Constructs

Stackに処理を書き続けると肥大化しすぎて保守しづらくなるのでConstructで分割する話や、generateSecretStringでシークレットを定義するとテンプレートに記載されたり、更新時にズレが発生する話がされていました。自分もシークレットを生成する場合は、基本CDKのライフサイクルの外部で定義して、それを組み込むようにしていましたが正しかったように感じます。またどうしてもCDKで管理したい場合でも、独立したスタックなどで管理するほうが良いと思います。

External Data

DynamoDBのデータなどをCDKから参照したい時、以下のコードを悪い例として紹介していました。理由としては、コードの難読化や外部データによって壊れてしまうことが言われていました。

cdk-externaldata-bad-pattern

そこで、SDKで直接呼び出すのではなく、ファイルに一度書き込むスクリプトを用意し、書き込まれたファイルをCDKで読むことを推奨していました。CDKのcontext API(Vpc.fromLookupやstack.availabilityZonesなど)も似たような実装になっています。やはりCDKの中でSDKを読み込むような実装は複雑さがますのであまり推奨はされていない。しかしながら、どうしても必要になった場合はcontext APIを真似たほうが良いです。

Aspects

Aspectを使うことで、テンプレート生成前にすべてのリソースに跨るような変更をかけることができると紹介されました。例えば以下のようにDatadogのAspectsを書くと、L2/L3 Construct内にパラメータがなくてもすべてのLambdaに設定を書くことができます。これはタイミングとしては、Synthesize前に実行されるのでテンプレート生成時には反映されることになります。

参考

export class DatadogAspect implements IAspect {
  constructor(private props: DataDogAspectProps) {
  }

  visit(node: IConstruct): void {
    if (
      node instanceof Function ||
      node instanceof NodejsFunction ||
      node instanceof PythonFunction
    ) {
      this.props.datadog.addLambdaFunctions([node]);
    }
  }
}

Aspectsは以下の記事のようなコンプライアンスのチェックで使う意味合いが強いと思っていました。ややトリッキーな感じもしたのですが、上のように横断的な変更を漏れなく行うことにも使えるのだなと感心していました。まだ知らないことが沢山ありますね。

余談ですが、Aspectsの由来はアスペクト指向プログラミングから来てるようです。これも知らなかった!

また登壇者が作成したもので、ConstructのLogical IDをリファクタリング時に変えて再生成が走らないようにするためlogical-id-mapperというものも作ったそうです。

タグ付けにAspectsを使う例も紹介されてました。これは定番パターンで環境ごとやアプリごとにタグを簡単につけられるので自分もよく使っています。

Tags.of(this).add('env', props.envTag);

最後にはcdk-nagの紹介もありました。

Single-Language benefit

間違った環境変数をLambdaに指定しないために、以下のような例が紹介されました。画像の上2つがLambdaのハンドラーで下2つがCDKのコードです。環境変数の入れ間違えで時間を溶かすことを防ぐために、Lambda側に環境変数の名を記載して、CDKとLambdaから呼び出すように実装しています。

cdk-lambda-env-export

CDKがLambdaを作るのでLambdaの値をCDKが参照するのは奇妙に感じたのですが、値ではなく名前だけなので何時間もTypoで時間を消費するよりはこのような書き方もありなのかと感じました。

再びStack

登壇者はマルチスタック派なので、再度マルチスタックでステートフルなリソースを管理することを推奨していました。後、定期的にリファクタリングしてスナップショットを活用することを推奨しています。リファクタの目安としてConstructが7つより多くなる時はリファクタのサインのようです。 7つの閾値の根拠は示されてなかったのですが、7つ以上になってくると切り分けすぎて複雑化してくるので良い目安なのかとも思いました。

cdk-stack-best-practice

最後のテンプレートの他アカウントでの使い回しは逆になかなか見ない気はしました。

再びConstruct

CDKでよくあるテストの話と作ったConstructの中で作成したConstructはimportしないようにという話がありました。これは確かにConstructを階層化することでパススルーメソッドが増えて、生成されるリソースが逆に追いづらくならないようにという配慮かと感じました。

cdk-construct-best-practice

Testing

テストについて最初は一般的なSnapshot TestとFine-grained Assertions、Integ Testの話でした。一番気になったのは、Assetsに対してspyOnでモックをうまい感じに作る方法が中々見たことがなかったので感心してました。具体的には以下のような内容です。

cdk-assets-mock

Snapshot TestをずっとやっているとAssetsだけ変更をして、CDKのコードは変更していないのにテストが失敗するということがよく起きると思います。その際に以下の記事のようにAssets無視するコードを入れるのが日本では広まっている方法です。

ですが、今回の例でmock化することでAssetsを無視するようなコードを書けるほうがより汎用的で影響範囲も狭めてかけるので今後このような書き方も取り入れてみたいと感じました。小さい内容かもですが、結構苦しめられたことがあったのでこの部分が個人的には一番感動しました。

Integ Testについてはテストを仕込んでおくと破壊的な変更がある場合に、テストが失敗し先に結果を教えてくれるという仕様もあるようです。これはDBなどステートのあるリソースの削除保護の安心感がさらに増しそうです。

cdk-integtest-faile

またTriggerモジュールとcdk-intrinsic-validatorについても紹介がありました。

Triggerの方は、テンプレートにカスタムリソースを仕込んでカスタムリソースのLambdaが失敗した場合に、Cfn自体を切り戻す機能を持っています。この例だとURLへのアクセスが失敗した場合はCfnがロールバックされます。

cdk-trigger

この処理自体は複雑なものではないのですが、昔仲間内で「この機能なんだろうね」と聞いた時に誰も分からない謎機能だったので、個人的な長年の疑問が解消されました。

Triggerよりもう少し複雑な検査がしたい場合は、以下のcdk-intrinsic-validatorが推奨されていました。

この機能を使うとCodeDeployと似たようにCloudWatch Alarmを何分か監視した後にアラートが出ていた場合や複数の操作をSfnで作ってステートが失敗になった場合、スタックをロールバックすることができます。これを使うことでより安全にCDKのデプロイができそうです。

cdk-add-sfn

最後には、CDKの拡張性の高さやCDK Bookの共著者などについて語られていました。

所感

日本だとあまり見ないような情報も多くあったのでかなり参考になりました。特にSnapshot Testの書き方やTriggerの話は昔からよく考えてた部分だったので、かなり刺激になりました。またcdk-intrinsic-validatorもまだスター数は少ないですが、コンセプトが素晴らしいのでCDKに組み込まれないかなと勝手に期待しています。この記事がどなたかの参考になれば幸いです。

宣伝

弊社でre:Growth(re:Inventの振り返りイベント)やります!募集締め切っちゃてるんですが、現地来る方よろしくお願いします。受付誘導やってます!

12/15(金)にある以下の弊社のイベントでもCDKの話するみたいなので良ければご参加下さい!自分も行ってみます〜