CDKで既存のS3バケットを管理する

## 概要
最近AWS CDKで環境構築する機会がありました。
その際に既存のS3バケットをCDKの管理下におく方法を調査したので共有させていただきます。
なお、今回は`cdk import`を使用して既存リソースをインポートしていきます。

## 準備
まず、CDKのプロジェクトを作成します。
```bash
mkdir cdk-import-bucket && cd cdk-import-bucket

cdk init sample-app -l typescript
```
`lib/cdk-import-bucket-stack.ts`を確認すると、`SNS`や`SQS`のリソースを作成する記述があるので削除しておきます。
```typescript
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

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

  }
}
```
一度この状態でデプロイしてAWS上にCloudFormationのスタックを作成しておきます。
```bash
cdk deploy
```
次に、CDKでインポートするS3バケットを作成します。
手動で作成してもいいですが、今回はCloudFormationで作成してみます。
```yaml
# sample-bucket.yaml
AWSTemplateFormatVersion: 2010-09-09

Resources:
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: sample-bucket-qwer
      AccessControl: Private
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - ServerSideEncryptionByDefault:
            SSEAlgorithm: AES256
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
```
デプロイ
```bash
aws cloudformation deploy --template-file sample-bucket.yaml --stack-name sample-bucket-stack
```
これでCDKで管理していないS3バケットの作成ができました。

## `cdk import`してみる
では、上記で作成したS3バケットをCDKの管理下にインポートしていきます。
まず、`lib/cdk-import-bucket-stack.ts`を下記のように変更してS3バケットの定義を追加します。
```typescript
import { Stack, StackProps } from 'aws-cdk-lib';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

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

    new Bucket(this, 'ImportBucket', {})
  }
}
```
差分を確認してみます。
```bash
cdk diff

Stack CdkImportBucketStack
Resources
[+] AWS::S3::Bucket ImportBucket ImportBucketBAF3A8E9 

✨  Number of stacks with differences: 1
```
新しくS3バケットが作成されようとしているのがわかります。
ではここで定義したS3バケットに対し、`cdk import`で既存のリソースをインポートしてみます。
```bash
cdk import

CdkImportBucketStack
CdkImportBucketStack/ImportBucket/Resource (AWS::S3::Bucket): enter BucketName [undefined]: 
```
上記のようにインポートするバケット名を聞かれるので入力します。
すると失敗してしまいます。
これは、対象のS3バケットを作成したときにCloudFormationで作成したことが原因です。
すでにCloudFormationで管理されているリソースについては`cdk import`ができないようです。
なのでS3を作成したCloudFormationスタックを削除しましょう。
削除前に`DeletionPolicy`を修正して、スタックを削除してもS3バケットが削除されないようにします。
```yaml
AWSTemplateFormatVersion: 2010-09-09

Resources:
  S3Bucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain  # 追加
    Properties:
      BucketName: sample-bucket-qwer
      AccessControl: Private
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - ServerSideEncryptionByDefault:
            SSEAlgorithm: AES256
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
```
`DeletionPolicy`を更新するためにデプロイ
```bash
aws cloudformation deploy --template-file sample-bucket.yaml --stack-name sample-bucket-stack
```
上記が成功したのちスタックの削除
```bash
aws cloudformation delete-stack --stack-name sample-bucket-stack
```
これでCloudFormationのスタックで管理されていないS3バケットが残りました。
もう一度`cdk import`してみます。
```bash
cdk import
CdkImportBucketStack
CdkImportBucketStack/ImportBucket/Resource (AWS::S3::Bucket): enter BucketName [undefined]: sample-bucket-qwer
CdkImportBucketStack: importing resources into stack...
CdkImportBucketStack: creating CloudFormation changeset...

 ✅  CdkImportBucketStack
Import operation complete. We recommend you run a drift detection operation to confirm your CDK app resource definitions are up-to-date. Read more here: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/detect-drift-stack.html
```
成功しました！
マネージメントコンソールでスタックのステータスを確認すると`IMPORT_COMPLETE`になっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3418566/ef51b304-8138-c4a8-2310-f0d1f69b501d.png)

うまくいったようですが、`cdk import`を行った際の出力に気になるメッセージがありました。
```bash
We recommend you run a drift detection operation to confirm your CDK app resource definitions are up-to-date. Read more here: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/detect-drift-stack.html
```
「ドリフトの検出するのをお勧めするよ」と言われてます。
ドリフトとは[こちら](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/using-cfn-stack-drift.html#what-is-drift)に書かれている通りなのですが、要はCloudFormationのスタックに定義されている構成と、実際のリソースの構成に差異がないかを検出するために使用されるものです。
なのでCloudFormationで管理しているリソースのプロパティを手動で変更した場合などの検出に使用できます。

今回で言うとCDKにインポートした定義と既存リソースの構成に差異がないかチェックしてねということのようです。

メッセージの[URL](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/detect-drift-stack.html)にドリフト検出の方法が載っているのでそれ通りに進めたところ、ドリフトステータスが`IN_SYNC`（差分なし）となることを確認しました。
なので正常にインポートが完了したことになります。

## プロパティの更新を試す
リソースのインポートができたので、試しにS3バケットのプロパティの更新も試してみます。
ライフサイクルルールを追加してみましょう。
```typescript
import { Duration, Stack, StackProps } from 'aws-cdk-lib';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

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

    new Bucket(this, 'ImportBucket', {
      lifecycleRules: [
        {
          id: "DeleteExpiredData",
          enabled: true,
          expiration: Duration.days(30),
        }
      ]
    })
  }
}
```
差分の確認
```bash
cdk diff
Stack CdkImportBucketStack
Resources
[~] AWS::S3::Bucket ImportBucket ImportBucketBAF3A8E9 
 └─ [+] LifecycleConfiguration
     └─ {"Rules":[{"ExpirationInDays":30,"Id":"DeleteExpiredData","Status":"Enabled"}]}

✨  Number of stacks with differences: 1
```
想定した差分が確認できたのでデプロイ
```bash
cdk deploy
```
ライフサイクルルールが設定されたか確認してみます
```bash
aws s3api get-bucket-lifecycle-configuration --bucket sample-bucket-qwer

{
    "Rules": [
        {
            "Expiration": {
                "Days": 30
            },
            "ID": "DeleteExpiredData",
            "Filter": {
                "Prefix": ""
            },
            "Status": "Enabled"
        }
    ]
}
```
うまくいきました！

出力されたCloudFormationテンプレートでもプロパティが追加されていることを確認できます。
`cdk.out > CdkImportBucketStack.template.json`
```json
{
 "Resources": {
  "ImportBucketBAF3A8E9": {
   "Type": "AWS::S3::Bucket",
   "Properties": {
    "LifecycleConfiguration": {
     "Rules": [
      {
       "ExpirationInDays": 30,
       "Id": "DeleteExpiredData",
       "Status": "Enabled"
      }
     ]
    }
   },

}

```

## まとめ
CDKで既存のS3バケットをインポートする方法を紹介しました。
今回インポートの対象としたのはS3バケットでしたが、他のリソースでも同じような手順で実行できるのではないかと思います。
どなたかの参考になれば幸いです。

### 参考
- [How to import existing resources into AWS CDK Stacks](https://aws.amazon.com/jp/blogs/devops/how-to-import-existing-resources-into-aws-cdk-stacks/)
- [CloudFormation スタック全体のドリフトを検出する](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/detect-drift-stack.html)
- [スタックとリソースに対するアンマネージド型構成変更の検出](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/using-cfn-stack-drift.html)