CDK とCloudFormation を繋ぐ話

これは CDK Advent Calendar 2021 の 1日目の記事です。

みなさんこんにちは。大村(@yktko) です。

AWS CDK の Advent Calendar の初日なのですが、ここでは、あえて CDK と AWS CloudFormation (CFn) の間を繋ぐ話をします。このエントリで CDK コードを書く人は CFn を、 CFn テンプレートを書く人は CDK を、相互の理解を深めていただくきっかけになればと思います。

# CDK と CloudFormation の関係

ご存知のように、[CDK](https://aws.amazon.com/jp/cdk/) は TypeScript や Python など一般の言語で AWS の環境を定義できるツールセットです。
そして、その実体は CloudFormation のテンプレートを生成するテンプレートエンジンです。デプロイメントはあくまで CloudFormation が行う。そして CDK はあくまで CFn テンプレートを書きやすくするためのツールであると割り切ってしまうと、悩むことが少なくなります。

CFn を使わずに CDK から入られた方は、「オブジェクト指向のコードでインフラ構成が書ける！」「少ないコードで AWS の環境の定義ができる！」というメリットを感じられていると思います。一方で、一般のアプリケーションコードのようにオブジェクト指向的にコードを書き、クラスを抽象化し、重複がないようコードを書いていくと、課題に突き当たることが多いのではないかと思います。たとえば、ハイレベルコンストラクト(L2 Construct)が使えないので CDK では管理できないリソースが出てきたとか、あるいは指定したいプロパティがハイレベルコンストラクトで指定できないとか、そういう状況です。

CDK は便利なツールキットなのですが、その下に存在する CloudFormation テンプレートの存在をうっすら意識することで、コードを書くときのトレードオフを判断しやすくなります。この記事ではそのための Tips をいくつか紹介したいと思います。

なお、CDK のコードの中で、SDK による API 呼び出しを直接行うコードを書くことはできますが、これは注意が必要です。本来 CDK は（CFn テンプレートという）状態を記述するためのコードですが、そのコードの中に手続きを記述することになります。この場合、リソースの変更や削除といった処理の際にも適切な手続きを記載しないと構成とコードの整合が難しくなります。CDK の Construct の中にもカスタムリソースとして Lambda ファンクションを利用しているものがありますが、これはあくまで例外として捉えるべきでしょう。

# CDK が「よしなに」やってくれる部分の確認方法

CFn に慣れている人が CDK でコードを書こうとして一番心配になるのは、CDK が「よしなに」設定を生成してくれる部分ではないでしょうか。

良くも悪くもすべての必須パラメータを細かく明示的に指定しなければいけない CloudFormation に対して、CDK は Construct Library によって、最小限のコードで AWS のベストプラクティスに沿った設定を自動的に生成してくれます。
たとえばマルチ AZ かつパブリックとプライベートでセグメントを分ける VPC を生成する場合。
CDK ではその定義は1行ですが、

```TypeScript:cdk-vpc-stack.ts
    const vpc = new ec2.Vpc(this, "MyVPC");
```

CloudFormation では694行のテンプレートになります。

```JSON:CdkVpcStack.template.json
{
  "Resources": {
    "MyVPCAFB07A31": {
      "Type": "AWS::EC2::VPC",
      "Properties": {
        "CidrBlock": "10.0.0.0/16",
        "EnableDnsHostnames": true,
        "EnableDnsSupport": true,
        "InstanceTenancy": "default",
（略）
```

コードが少なくなるのは、メンテナンス上大きなアドバンテージですが、一方で自分の意図しない設定が入ってしまうのではないかという点が気になると思います。
そういう場合は `cdk synth` を実行した後に作られる `cdk.out` ディレクトリを確認してください。
スタックごとに CFn テンプレートが生成されており、実際にデプロイされる設定がどのようになっているのかを確認できます。
JSON 形式で見にくいという方は [cfn-flip](https://github.com/awslabs/aws-cfn-template-flip) を使って YAML 形式に変換するのも良いでしょう。

![cdk.out.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221438/896c22ff-23ca-666b-a489-ab4497283b1e.png)


この CFn テンプレートはデプロイ時にエラーになる場合の原因調査にも役立ちます。`cdk deploy` でエラーになったとき、コマンド結果の出力や CloudFormation のマネジメントコンソールにエラーの詳細が表示されます。
デプロイ対象となるリソースの論理名は cdk synth したときに自動的に生成されますが、CDK コードから直感的にわからない場合もあります。
CFn のエラーメッセージから原因を探るにはデプロイしようとしている論理名が書かれた CFn テンプレートを見た方が、より早く原因を突き止められるでしょう。

# CDK から使う CloudFormation の機能

CDK を使うときは `cdk deploy` でデプロイまで行えますが、そのバックエンドで動くのは基本的に CloudFormation のスタック作成処理です。
これを理解していればデプロイにまつわる処理がどのように実装されているか、どのように使うべきかを理解することができます。
最近 CDK に追加された disable rollback 機能と hotswap 機能を例にとって詳細を見てみます。

## disable rollback
CFn で開発を行なっていると、デプロイして発生したエラーメッセージを見てテンプレートを修正する、という試行錯誤を行うことが多くなります。CDK では type safe な言語やエディタのサジェストを使うことで、コードを書くする段階でエラーが混入する可能性を減らすことができますが、それでも実環境に依存するエラーもあり、デプロイしないとわからないことが多くあります。CDK よるデプロイでは、スタックのデプロイに失敗したとき、デフォルトではロールバックを実行してそのスタックのリソース全てを削除して綺麗な状態にしてくれます。これによって常にクリーンな状態でリソースを作ることができます。

しかし試行錯誤を行っているときは、このロールバックが仇になります。スタック作成の最後の段階にエラーがあるとどうでしょう
ロールバックで全てのリソースが削除されるまで、ミスを混入した自分を責めながら虚無の時間を過ごすことになります。特に、作成に時間がかかる RDS などのリソースの後に、しょうもないミスが混入していると、RDS インスタンスの作成と削除の間、ずっと待ち続けることになってしまいます。辛いですね。

2021 年 10 月に CloudFormation にアップデートがあり、 [スタック操作でエラーが発生した場合、正常にデプロイされたリソースのロールバックを行わないようにする機能](https://aws.amazon.com/jp/about-aws/whats-new/2021/08/aws-cloudformation-troubleshoot-errors/) が追加されました。個人的には 2021 年最高のアップデートの一つだと思います。
CDK はその週のうちにこの機能を取り込み、`cdk deploy` に `--no-rollback` オプションが追加されました。これによって CDK で開発している時も、リソースの作成に失敗した場合それまでのリソースは維持され、コードを修正して作成に失敗したリソースからデプロイを再開できるようになりました。本来は 1 回のデプロイコマンドで正常にデプロイが完了する必要があります。またエラーになったときに環境をクリーンに保つ必要があるため、これを常用することはお勧めできませんが、試行錯誤の多い開発においては有用な機能だと言えます。これは、CFn の機能強化が CDK の機能強化にもつながった例と言えます。CDK で開発していたとしても、CFn のアップデートに注目が必要ですね。

Auroraを作成した後にエラーが発生して、Rollbackせずに処理が停止した時の例はこんな感じです。
![rollback-stop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221438/368502f6-7c20-d3c9-f834-c565e839bdb2.png)

## hotswap 
disable rollback と同時期に CDK に [hotswap 機能がリリース](https://aws.amazon.com/jp/about-aws/whats-new/2021/10/aws-cdk-releases-hotswap-rollback-control/) されました。これは Lambda などアプリケーションコードを含むリソースをデプロイする際に、`--hotswap`を指定することで迅速にアプリケーションコードを入れ替える機能です。これは先程の disable rollback と逆に、CDK が独自に追加した機能です。
本来 CDK でアプリケーションコードを入れ替える際、アプリケーションコードを変更した CFn テンプレートを生成し、Changeset を作り、CFnでデプロイします。これは CDK コードに記述した通りの構成を展開するという目的では必要なのですが、アプリケーションコードをわずかに修正するだけでも、数十秒のデプロイ時間を必要としていました。そこで、hotswap ではアプリケーションコードの入れ替えに CFn ではなく Lambda の API を直接使うことで、アプリケーションの修正デプロイを数秒で完了するようにしました。実装をみるとわかるように、 CDK の hotswap 機能は、CDKコードの設定内容とリリースしたアプリケーションコードが一致しないようになるため、厳密な構成管理が必要な本番環境に使えるものではありません。あくまで開発を迅速に行うための機能であるということに注意が必要です。

このように、 disable rollbackも、hotswapも、CDK の背後にあるデプロイメントの仕組みを理解していると、その用途を理解しやすくなります。

# CloudFormation テンプレートを CDK の ローレベル（L1) Construct で作る

本日一番お勧めしたいのがこちらの内容です。
仮に CloudFormation テンプレートを作る場合であっても、CDK を使った方がよい（かもしれない）ということです。

CDK にはオブジェクト指向的にリソースを定義できる[ハイレベルコンストラクト（L2 Construct）](https://docs.aws.amazon.com/ja_jp/cdk/latest/guide/constructs.html#constructs_using)と、[CFn リソースと 1:1 で対応しているローレベルコンストラクト（L1 Construct）](https://docs.aws.amazon.com/ja_jp/cdk/latest/guide/constructs.html#constructs_l1_using)があります。
このうち、L1 Construct をもっと積極的に使ってはどうか、という考え方です。

L1 Construct は CFn のリソースがリリースされると、その定義に従って Construct Library が自動生成され、次の CDK アップデートで取り込まれます。従って、L1 Construct を使えば、CFn で定義可能な全てのリソースが CDK でも利用可能です。L1 Construct を使う際は CFn と同様に必須となる全てのパラメータを明示的に指定する必要があります。
ですが、エディタによるサジェストが強力であるため、どのプロパティが必須の指定なのか、どういった値を設定するべきなのか、といったことがコードを書きながら確認できます。
私の場合 YAML を直接書くときより、[CFn リファレンス(https://docs.aws.amazon.com/en_us/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html)を参照する回数は明らかに減りました。

こちらは VisualStudio Code で、SQS を L1 Construct ([CfnQueue](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-sqs.CfnQueue.html)) で作った時の例です。サジェストにより指定可能なプロパティ名、必須かどうか(`?`の有無)、そしてプロパティの型がわかります。
![suggest.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221438/6dc64291-9101-ad52-227d-7dd9534a9676.png)
![suggest-with-type.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221438/b060e610-7f39-1b2c-f51e-c46501eab69b.png)

AWSの環境を管理する際、CDK ではなく CFn テンプレートが必要な場合があります。たとえば CFn StackSets で同じ設定を展開したい場合などです。私は、こういう場合でも、まず CDK の L2 あるいは L1Construct を使ってベースとなるテンプレートを生成して、そこからパラメータをつけるなどの加工を行っています。

また、CDK の L2 Construct がまだ作られていないリソースがあります。たとえば [AWS WAFv2](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-wafv2-readme.html) などです。
こういったものを定義するときも、躊躇なく L1 Construct を使います。
たとえば AWS Samples で公開している [BLEA (Baseline Environment on AWS)](https://github.com/aws-samples/baseline-environment-on-aws) では[この部分](https://github.com/aws-samples/baseline-environment-on-aws/blob/main/usecases/guest-webapp-sample/lib/blea-waf-stack.ts#L23)で WAF を L1 Construct で定義しています。

```typescript:blea-waf-stack.ts 
    const webAcl = new wafv2.CfnWebACL(this, 'WebAcl', {
      defaultAction: { allow: {} },
      name: 'BLEAWebAcl',
      scope: props.scope,
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        metricName: 'BLEAWebAcl',
        sampledRequestsEnabled: true,
      },
      rules: [
        {
          priority: 1,
```

そして L2 Construct で作った ALB と WAF との紐付けも[このように](https://github.com/aws-samples/baseline-environment-on-aws/blob/main/usecases/guest-webapp-sample/lib/blea-frontend-simple-stack.ts#L71) L1 Construct で行っています。

```typescript:blea-frontend-simple-stack.ts
    new wafv2.CfnWebACLAssociation(this, 'WebAclAssociation', {
      resourceArn: lbForApp.loadBalancerArn,
      webAclArn: props.webAcl.attrArn,
    });
```

このように、あくまで CDK は CFn テンプレートを生成しているという考えのもと、L1 Construct を柔軟に使用して、CDK による管理を行うことを考えてみてはいかがでしょうか。

# 既存の CFn テンプレートを CDK で編集する

最後に、既存の CFn テンプレートが既にあるときに CDK にどのように取り組むのかを考えます。

CDK には [cloudformation-include](https://docs.aws.amazon.com/cdk/api/latest/docs/cloudformation-include-readme.html) モジュールがあり、これを使って既存の CFn テンプレートの資産を活かしつつ、また既存の CFn で作ったリソースも再作成することなく、CDK による管理へ移行することができます。

こちらも[BLEA](https://github.com/aws-samples/baseline-environment-on-aws)での実装例でご紹介します。
[AWS ControlTower で提供している Detective Guardrail](https://docs.aws.amazon.com/ja_jp/controltower/latest/userguide/guardrails-reference.html) は [AWS Config の Conformance Pack でも提供されています](https://aws.amazon.com/jp/blogs/news/aws-control-tower-detective-guardrails-as-an-aws-config-conformance-pack/)。 これは [AWS Config のドキュメント](https://docs.aws.amazon.com/ja_jp/config/latest/developerguide/aws-control-tower-detective-guardrails.html)に記載されているように、[CFnテンプレートが公開](https://github.com/awsdocs/aws-control-tower-guide)されています。
BLEAではこのテンプレートを変更することなく使用して ControlTower と同様のガードレールを展開するよう設定していますが、CDK でこれを再実装するのではなく、[cloudformation-include モジュールを使って CFn テンプレートをインポートする](https://github.com/aws-samples/baseline-environment-on-aws/blob/main/usecases/base-standalone/lib/blea-config-ct-guardrail-stack.ts)ことで実装しています。これによって CFn テンプレートをそのまま使って CDK でデプロイできます。

```typescript:blea-config-ct-guardrail-stack.ts 
import * as cdk from '@aws-cdk/core';
import * as cfn_inc from '@aws-cdk/cloudformation-include';

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

    // https://github.com/awslabs/aws-config-rules/tree/master/aws-config-conformance-packs
    new cfn_inc.CfnInclude(this, 'ConfigCtGr', {
      templateFile: 'cfn/AWS-Control-Tower-Detective-Guardrails.yaml',
    });
  }
}
```

`CfnInclude()`でインポートしたテンプレートからリソースオブジェクトを参照し、L1 または L2 Construct を取得することで、対象リソースのプロパティを変更することも可能です。ただ、プロパティはコンストラクタにしか指定できないものもあり、そういったプロパティを変更する場合はテンプレート自体を変更する必要があります。このようにして、既存の CFn テンプレートを活かしたまま、CDK の管理に移行していくことが可能です。

さらに、既存のテンプレートですでにスタックを作成している場合、CDK コード内で指定するスタック名として、既存のスタック名を指定することで、以後そのスタックを CDK で管理できるようになります。一見不思議な動きをしているように見えるかもしれませんが、CDK はあくまで CFn テンプレートを生成しているだけで、その CFn テンプレートでスタックを生成していると考えると、動きが理解できると思います。
CFn からみると、既存のテンプレートと同様の、CDK によって生成されたテンプレートが新しく提示されているだけです。そのテンプレートは既存のテンプレートを include して CDK によって生成されており、メタデータ以外は変わらない内容である、ということになります。
[cloudformation-includeについてはブログが公開されています](https://aws.amazon.com/jp/blogs/news/migrating-cloudformation-templates-to-the-aws-cloud-development-kit/)。詳しくはそちらもご覧ください。

# おわりに

CDK と CloudFormation の関係について解説しました。いかがでしたでしょうか。
すでにCDK による管理をやっている方は一歩 DiveDeep して CFn の世界をチラ見する。
すでにCFn による管理をやっている方は一歩引いてみて CDK を使ったテンプレート作成の世界を見る。
それぞれの皆さんのお役に立てれば幸いです。

Happy Coding! & Happy Operating!

## 参考
* [AWS Blackbelt - AWS CDK](https://aws.amazon.com/jp/blogs/news/webinar-bb-aws-cloud-development-kit-cdk-2020/)
* [AWS Blackbelt - AWS CloudFormation](https://aws.amazon.com/jp/blogs/news/webinar-bb-aws-cloudformation-2020/)
* [AWS Blackbelt - AWS CloudFormation DeepDive](https://aws.amazon.com/jp/blogs/news/webinar-bb-aws-cloudformation-deep-dive-2020/)
* [AWS環境にセキュアなベースラインを提供するテンプレート「Baseline Environment on AWS」のご紹介](https://aws.amazon.com/jp/blogs/news/announcing-baseline-environment-on-aws/)