AWS CDK(Python)でアカウントIDを含むS3バケットを作る

この記事は [ハンズラボ AdventCalendar2021](https://qiita.com/advent-calendar/2021/handslab) 1日目の記事です。


プログラム言語でAWSインフラを定義できるという [AWS CDK](https://aws.amazon.com/jp/cdk/) を試してみました。言語はPython3を利用します。


## インストール

Node、Python3、virtualenvが必須のため事前にセットアップ。
私の環境ではpyenvも導入しています。

```bash
$ node -v
v12.12.0
$ python -V
Python 3.8.6
$ virtualenv --version
virtualenv 20.10.0 from /Users/xxxx/.pyenv/versions/3.8.6/lib/python3.8/site-packages/virtualenv/__init__.py
```


早速CDKをインストールします。

```bash
$ npm install -g aws-cdk
# インストール確認
$ cdk --version
1.134.0 (build dd5e12d)
```

## 初期設定

今回の検証用ディレクトリを作成し、そこで初期設定を行います。

```bash
$ mkdir cdk_practice
$ cd cdk_practice

$ cdk init app --language python
```

`source .venv/bin/activate` → `pip install -r requirements.txt` を実行しなさい、とメッセージが表示されますが私は pipenv を使いたいので小細工をします。
特にこだわりのない方はメッセージに従ってコマンドを実行することをおすすめします。

```bash
# デフォルトで作成された仮想環境を削除
$ rm -rf .venv

# pipenvで再セットアップ
$ export PIPENV_VENV_IN_PROJECT=true
$ pipenv --python 3

# ライブラリインストール
$ pipenv install
$ pipenv install --dev -r ./requirements-dev.txt

# もう使わないので削除
$ rm -rf requirements*.txt

# 仮想環境有効化
$ pipenv shell
```


## コーディング

[ドキュメント](https://docs.aws.amazon.com/cdk/latest/guide/work-with-cdk-python.html)によるとCDKでは構築したいサービスに対応するコアモジュールをインストールする必要があるようです。命名規則は `aws-cdk.SERVICE-NAME` とのことです。今回はS3バケットを作成したいので、以下のコマンドを実行しS3用モジュールをインストールします。

```bash
$ pipenv install aws-cdk.aws-s3
```

次はコードを見ていきます。主に修正を行うのは `app.py` と `cdk_practice/cdk_practice_stack.py` のようです。過去のバージョンでは `setup.py` も存在していたようですが、現在のバージョンでは配置されないようです。


### app.py

初期化直後の `app.py` はこんな感じです。

```python
#!/usr/bin/env python3
import os

from aws_cdk import core as cdk

# For consistency with TypeScript code, `cdk` is the preferred import name for
# the CDK's core module.  The following line also imports it as `core` for use
# with examples from the CDK Developer's Guide, which are in the process of
# being updated to use `cdk`.  You may delete this import if you don't need it.
from aws_cdk import core

from cdk_practice.cdk_practice_stack import CdkPracticeStack


app = core.App()
CdkPracticeStack(app, "CdkPracticeStack",
    # If you don't specify 'env', this stack will be environment-agnostic.
    # Account/Region-dependent features and context lookups will not work,
    # but a single synthesized template can be deployed anywhere.

    # Uncomment the next line to specialize this stack for the AWS Account
    # and Region that are implied by the current CLI configuration.

    #env=core.Environment(account=os.getenv('CDK_DEFAULT_ACCOUNT'), region=os.getenv('CDK_DEFAULT_REGION')),

    # Uncomment the next line if you know exactly what Account and Region you
    # want to deploy the stack to. */

    #env=core.Environment(account='123456789012', region='us-east-1'),

    # For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html
    )

app.synth()
```


これに以下の通り修正を加えました。

- `from aws_cdk import core as cdk` でimportするのが推奨というコメントに従い `from aws_cdk import core` を削除
- CdkPracticeStackにenv追加


```python
#!/usr/bin/env python3
import os
from aws_cdk import core as cdk
from cdk_practice.cdk_practice_stack import CdkPracticeStack

app = cdk.App()
CdkPracticeStack(app, "CdkPracticeStack",env=cdk.Environment(region='ap-northeast-1.'))

app.synth()

```

複数のスタックを管理したい場合は`CdkPracticeStack`と同様の定義を追加していけば良さそうですね。

### cdk_practice_stack.py

次は初期化直後の `cdk_practice/cdk_practice_stack.py` です。


```python
from aws_cdk import (
    core as cdk
    # aws_sqs as sqs,
)

# For consistency with other languages, `cdk` is the preferred import name for
# the CDK's core module.  The following line also imports it as `core` for use
# with examples from the CDK Developer's Guide, which are in the process of
# being updated to use `cdk`.  You may delete this import if you don't need it.
from aws_cdk import core


class CdkPracticeStack(cdk.Stack):

    def __init__(self, scope: cdk.Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # The code that defines your stack goes here

        # example resource
        # queue = sqs.Queue(
        #     self, "CdkPracticeQueue",
        #     visibility_timeout=cdk.Duration.seconds(300),
        # )

```

今回構築したいS3バケットの仕様は以下の通りです。

- バケット名重複防止のためバケット名にAWSアカウントIDを含ませる
- デフォルトキーを利用してサーバーサイドで暗号化する
- ファイルの有効期限を1年とする
- スタックが削除されたら合わせて削除する


コアモジュールの仕様書と睨めっこしながら書き上げたコードがこちらです。

https://docs.aws.amazon.com/cdk/api/latest/python/index.html



```python
from aws_cdk import (
    core as cdk,
    aws_s3 as s3,
)


class CdkPracticeStack(cdk.Stack):

    def __init__(self, scope: cdk.Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # アカウントIDの取得
        accountId = cdk.Aws.ACCOUNT_ID

        # バケットの作成
        bucket = s3.Bucket(self,
            'cdkpracticebucket',
            # サーバーサイド暗号化
            encryption=s3.BucketEncryption.S3_MANAGED,
            # バケット名にアカウントIDを付与
            # f'hogehoge-{accountId}'だとNG
            bucket_name= 'cdkpracticebucket-' + accountId
        )

        # 1年経過で削除するライフサイクルルールを追加
        bucket.add_lifecycle_rule(
            enabled=True,
            expiration=cdk.Duration.days(365)
        )

        # スタック削除時に一緒に削除
        bucket.apply_removal_policy(cdk.RemovalPolicy.DESTROY)

```

## テンプレート生成

以下のコマンドを実行すると記述した定義をもとにCloudFormationテンプレート(yaml形式)が生成され、標準出力に表示されます。定義に誤りがある場合はエラーになります。

```bash
$ cdk synth
```

今回はこのようなテンプレートになりました。また`/cdk.out`にも同様にテンプレートが出力されるようです。

```yaml
Resources:
  cdkpracticebucket59CD39AD:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      BucketName:
        Fn::Join:
          - ""
          - - cdkpracticebucket-
            - Ref: AWS::AccountId
      LifecycleConfiguration:
        Rules:
          - ExpirationInDays: 365
            Status: Enabled
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
    Metadata:
      aws:cdk:path: CdkPracticeStack/cdkpracticebucket/Resource
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Modules: aws-cdk=1.134.0
```



## デプロイ
テンプレートを見た感じ問題なさそうですのでデプロイしていきます。ここからはAWSアカウントに対するcredentialが必要ですので、別途設定してください。

```bash
$ cdk deploy
```


うまくいきました。
![スクリーンショット 2021-11-28 12.09.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501670/9058b3ac-525f-664f-78f6-1e430945b4d8.png)

S3バケットが作成されています。
![スクリーンショット 2021-11-28 13.21.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501670/a848abf4-b582-c840-24c8-5adf9b81696b.png)

暗号化やライフサイクルルールも大丈夫そうです。
![スクリーンショット 2021-11-28 13.21.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501670/7a97120b-d7b6-895c-6fcf-5223cfb587ad.png)

![スクリーンショット 2021-11-28 13.22.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501670/77e961f7-9ddf-b526-560f-bb631ed19337.png)


## 後片付け

一通り検証できたので後片付けをします。削除コマンドは `cdk destroy` です。


```bash
$ cdk destroy
Are you sure you want to delete: CdkPracticeStack (y/n)? y
CdkPracticeStack: destroying...



 ✅  CdkPracticeStack: destroyed
```

削除できました。
![スクリーンショット 2021-11-28 13.24.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501670/164124b1-4430-f7da-40bd-124e2d64e180.png)

`apply_removal_policy` を指定していたのでバケットも消えています。指定しなかった場合、デフォルト設定が`RETAIN`のため削除がスキップされます。

## ハマりポイント

バケット名にアカウント名を含める際、CloudFormationでは擬似パラメーターを活用してこのように書くことが多いです。

`!Sub hogehoge-${AWS::AccountId}`

CDKでアカウントIDを取得したい場合は [aws_cdk.core.Aws](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Aws.html
) を見れば良いようです。

そこでPythonでも同じようにbucket_nameを指定してみたところエラー。


```python
bucket_name= f'cdkpracticebucket-{accountId}'
```

色々と調査した結果、以下の記述だとテンプレートに変換することができました。

```python
bucket_name= 'cdkpracticebucket-' + accountId
```

生成されたテンプレートでは、`Fn::Join`を利用した形式に自動変換されていました。加算演算子を使った場合のみ、CDK内部で何らかの変換処理が行われているものと思われます。

```yaml
      BucketName:
        Fn::Join:
          - ""
          - - cdkpracticebucket-
            - Ref: AWS::AccountId
```

バケット名のような文字列のみ受け付けるパラメータに擬似パラメータを渡したい場合は工夫が必要のようです。

## 感想

上記のハマりなどもあり最初は苦労しました。しかし、一般のプログラム言語を利用していることからIDEの豊富な支援をそのまま受けることができ、補完機能なども多数扱えるためノウハウが溜まればCloudFormationより高速に構築できるようになる可能性を感じました。テストの実施もできるようですので、もう少し深掘りして検証を行ってみても良いかもしれません。