[AWS]CFnで作成したスタックをCDKで書き直し→デプロイ（＆その逆）したらどうなるの

こんにちは、AWSおよびCDKひよこクラブのmasaozi3です:hatching_chick:
本記事はAWS/CDK/CloudFormation初学者:hatching_chick:向けの内容です。

あまりやらないとおもうのでニーズがあるのかわかりませんが、一度CFnで作成したスタック定義を完全にCDKで書き直したい場面にぶちあたり、どういう挙動になるのか気になったので調査してみました。
そしてさらにニーズがあるのかわかりませんが、逆にCDKで作成したスタックをCFnに完全に書き換えるパターンもやってみました。

ちなみにCDKでは[cloudformation-include](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.cloudformation_include.CfnInclude.html) を使うことで、CFnファイルをテンプレートとして取り込みつつ実装することもできるそうですが、今回はCDKで全てを書き直し、CFnのファイルは破棄できる状態にすることが前提です。


スタックIDを一致させれば同一のスタックへの操作と認識してくれて、その中のリソースもIDを一致させれば同じリソースへの操作として認識してくれないかな…というのが期待する結果です。
「前述のライブラリがあるってことは、うまいこといけるのでは？」という甘い考えでやっていきます:relaxed:

# 検証環境
* Mac OS 12
* Typescript 4.8.4
* node 18.10.0
* CDK CLI 2.44.0

# 準備
S3と、SSL通信じゃないアクセスを弾くバケットポリシーを作成するだけのシンプルなスタックを定義します。
スタックID、リソースのIDがCFnとCDKで一致するように明記しています。

## CFn
```yaml:s3stack.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: create s3 bucket from CFn

Resources:
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: 'cfn-cdk-sample-bucket'
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  S3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: 'cfn-cdk-sample-bucket'
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action: 's3:*'
            Effect: Deny
            Principal: '*'
            Resource:
              - !GetAtt S3Bucket.Arn
              - !Sub ${S3Bucket.Arn}/*
            Condition:
              Bool:
                'aws:SecureTransport': 'false'
```

## CDK
スタックの定義はこちら。
```typescript:s3stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import {BlockPublicAccess, Bucket, BucketAccessControl} from "aws-cdk-lib/aws-s3";

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

    new Bucket(this, 'S3Bucket', {
      bucketName: 'cfn-cdk-sample-bucket',
      accessControl: BucketAccessControl.PRIVATE,
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
      enforceSSL: true
    })
  }
}
```
CDK appの方はこちら。
```typescript:cfncdksample.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { s3Stack } from '../lib/s3-stack';

const app = new cdk.App();

new s3Stack(app, 'cfn-cdk-sample-stack', {
    stackName: 'cfn-cdk-sample-stack',
    description: 'create s3 bucket from CDK yeah!',
});
```

# 調査1. CFn→CDKの書き換え
まずはCFnをデプロイします。
```bash
$ aws cloudformation create-stack \
--stack-name cfn-cdk-sample-stack \
--template-body file://path/to/s3stack.yaml \
--profile my-profile
```

AWSコンソール上で確認すると無事出来上がっています。
![cfn-cdk-step1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/877395/93e9707b-2fea-1aab-b30f-f37ce8115030.png)

次はいざ、このスタックに対してCDKでdiffを出してみます。
```bash
$ npm run cdk diff cfn-cdk-sample-stack -- --profile my-profile
```
いろいろ差分が示されました。
![cfn-cdk-step2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/877395/790e1fc1-8820-7b8a-b223-74f3fa9257c3.png)
ふむふむどうやら無事に、CFnで作成したスタックの**更新**と認識されているようです。
わざと変えておいたDescriptionや、実装ミスで完全に同じにできていない箇所、CFnとCDKでの書き方の違いによるものが変更内容として出ていますね。
ここまでは良さそうです。

スタック内のリソースはどうでしょうか。
![cfn-cdk-step2-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/877395/f54ca919-dbba-9031-5843-eb1b530bc320.png)
残念ながら、完全に別のリソースとして作り直されてしまうようです。
赤字の*destroy*が悲しい…:weary:

特にS3はIDもバケット名も揃えて書きましたが、CDKによって指定したIDをもとに内部でさらにユニークなIDを振り直されてしまってだめなようです。
`XXX.fromLookup()`とか`XXX.fromXXXArn()`など既存のリソースを参照するための実装があることを思うと、納得の結果ではあります。

一応デプロイもしてみましょう。
```bash
$ npm run cdk deploy cfn-cdk-sample-stack -- --profile my-profile
```
更新失敗しました。
![cdk-cfn-step3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/877395/919cf8c8-4e06-123a-f0b1-442cfe620b33.png)

![cdk-cfn-step3-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/877395/e0208794-5a46-1bb5-1750-d7970e37df30.png)
diffの段階で判明したとおり、**別のリソースとして同じ名前のバケットを作成する**という操作をしようとしたため
「同名のバケットもうこのスタック内にありますね」と怒られてしまったようです:skull:

## CFn→CDKのまとめ
* スタックはIDを揃えれば同一のスタックとして認識してくれる:relaxed:
* スタック内のリソースは別物（作り直し）になる:frowning2:
  * `new`してる限りCDKでIDが振り直される
  * `XXX.fromLookup()`とか`XXX.fromXXXArn()`など既存のリソースを参照するための実装がある


# 調査2. CDK→CFnの書き換え
先程のCFn→CDKで作成したスタックやリソースは一通り削除したうえで、まずはCDKをデプロイします。
```bash
$ npm run cdk deploy cfn-cdk-sample-stack --profile my-profile
```

ここで設定されたリソースのIDをもとに少しだけCFnを修正します。
```yaml:s3stack.yaml
Resources:
  S3Bucket07682993:  # ここをCDKで設定されたIDに修正
    Type: AWS::S3::Bucket
    Properties:
      BucketName: 'cfn-cdk-sample-bucket'

〜中略〜

  S3BucketPolicyF560589A:  # ここをCDKで設定されたIDに修正
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: 'cfn-cdk-sample-bucket'
    …
```

CFnではdiffを出す方法がないので、今回は変更セットを作成してみます。
```bash
$ aws cloudformation create-change-set \
--stack-name {stackArn}\
--change-set-name DemoChangeSet \
--template-body file://path/to/s3stack.yaml \
--profile my-profile
```

AWSコンソール上から、作成した変更セットを確認してみると、無事に変更セットが作成されていました。
![cdk-cfn-step4-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/877395/87465109-4fe6-669a-406c-799ee8fe3270.png)
CDKで作成したスタックの**更新**と認識されているようです。

アクションの欄を見ると、S3もバケットポリシーも `Modify` とあるので、CDKで作成したリソースに対する**更新**だと認識してくれているようです :tada: 
CDKMetadataは `Remove` とあるのでもう不要だと判断して削除されるようですね。

そして置換の欄を見ると、S3は`False`なので再作成されないようですね。CFn→CDKのときの挙動を思うと納得です。
バケットポリシーは`True`なので、同じIDで作成され直すようです。
このあたりについては[どのリソースタイプの何のプロパティを変更するかによって決定される](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html)ので今回の調査としては問題ないです。

変更セットを適用してみると、無事にバケットを作り直すことなくスタックの更新が完了しました。
![cd-cfn-step5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/877395/5a2e54b0-19a3-738c-0d46-7236459e3d4e.png)


## CDK→CFnのまとめ
* スタックはIDを揃えれば同一のスタックとして認識してくれる:relaxed:
* スタック内のリソースはもIDを揃えれば同一のスタックとして認識してくれる:relaxed:

# まとめ
ということで、今回はレアケースだと思いますがCFnで作成したスタックとリソースをCDKでもう一度書き直してみるとどうなるのか試してみました。

CFn→CDKのケースではIDを揃えればスタックは同じものを操作できますが、スタック内のリソースはCDKによってIDを振り直されるので別のものと認識されてしまう事がわかりました。
逆にCDK→CFnのケースでは、IDを揃えればスタックもリソースも同じものを操作できるということがわかりました。

CFn→CDKのケースで、リソースを作り直されたくなければちゃんとLookupせよ、ということで
ニーズのよくわからない調査はこれにて終了です！お疲れさまでした:xmas-wreath2: