【AWS CDK (Python)】 AWS CDKの始め方

## はじめに
本記事は、私自身の備忘録を兼ねてAWS CDKをこれから始める方の一助になればと思い、AWS CDKの使い方等をまとめたものです。
今回は、Cloud9を使用しAWS CDKの導入～簡単なリソースの作成、削除までの一連の流れを確認しています。
なお、本記事は私自身の経験を基に記載していますが、間違いがあったらすみません。

## AWS CDKとは
AWS CDK（AWS Cloud Development Kit）は、AWSのリソースをPythonやTypeScript等のコードで定義して、AWS CloudFormationでデプロイすることができます。
本記事の記載時点で、JavaScript、TypeScript、Python、Java、C#、Goに対応していて、AWS CDKを使用することで、使い慣れたプログラミング言語でAWSリソースのデプロイができます。
また、YAMLやJsonで記載するCloudFormation Templateと違い、ループ等が使用できるため、効率的にコードを記載することができます。
## 環境
本記事は以下の環境を使用して記載しています。
* AWS Cloud9
* AWS CDK：2.80.0
* Python： 3.10.11
* Node.js： 16.20.0

## AWS CDKの始め方
以下の流れでAWS CDKの環境作成および実行確認をしていきます。

[1. Cloud9の作成](#1-cloud9の作成)
[2. Pythonのバージョンアップ](#2-pythonのバージョンアップ)
[3. AWS CDKのインストール、初期化](#3-aws-cdkのインストール初期化)
[4. コード変更](#4-コード変更)
[5. デプロイ](#5-デプロイ)
[6. リソース変更](#6-リソース変更)
[7. リソース削除](#7-リソース削除)
***

#### 1. Cloud9の作成
まず、AWS CDKを実行するCloud9を作成します。
マネージメントコンソールのサービスメニューから開発者用ツール>Cloud9を選び、Create environmentをクリックしてCloud9の環境作成する画面を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/330f5468-c150-4d60-71b5-4776abbd01b3.png)

作成するCloud9の名前を入力し、Instance typeを選びます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/770b014b-d1f2-af02-dbe5-7843f18237e1.png)

CreateをクリックしてCloud9を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/538cd3ef-4d72-c2a2-4bee-c66b39519c67.png)

作成完了のメッセージが出たら、OpenをクリックしてCloud9を起動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/04d187f2-0b31-96c6-bb73-8d497253d837.png)

Cloud9のIDEが開きました。下部にあるTerminalでAWS CDKの実行を行います。（レイアウトは自由に変更可能です。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/7cc54309-5663-d787-9c0c-42ef5ab56542.png)

これでCloud9の作成完了です。

#### 2. Pythonのバージョンアップ
Cloud9に標準でインストールされているPythonが古いので、バージョンアップします。
ここからは、主にCloud9のTerminalでコマンドを実行していきます。
```console
user_name:~/environment $ python --version
Python 3.7.16
```

まず、Pythonのバージョンアップに必要なpyenvをインストールします。
```console
user_name:~/environment $ git clone https://github.com/pyenv/pyenv.git ~/.pyenv
user_name:~/environment $ ~/.pyenv/bin/pyenv --version
pyenv 2.3.17-10-g920ef145
```

インストールしたpyenvにパスを通します。
```console
user_name:~/environment $ cat << 'EOT' >> ~/.bashrc
> export PATH="$HOME/.pyenv/bin:$PATH"
> eval "$(pyenv init -)"
> EOT
user_name:~/environment $ source ~/.bashrc
```

インストール可能なPythonの最新バージョンが`3.10.11`のようなので、インストールするとOpenSSLが見つからないとERRORが出ました。
```console
user_name:~/environment $ pyenv install --list | grep 3.10
  3.10.0
 (中略)
  3.10.10
  3.10.11
user_name:~/environment $ pyenv install 3.10.11
Downloading Python-3.10.11.tar.xz...
-> https://www.python.org/ftp/python/3.10.11/Python-3.10.11.tar.xz
Installing Python-3.10.11...
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/home/ec2-user/.pyenv/versions/3.10.11/lib/python3.10/ssl.py", line 99, in <module>
    import _ssl             # if we can't import it, let the error propagate
ModuleNotFoundError: No module named '_ssl'
ERROR: The Python ssl extension was not compiled. Missing the OpenSSL lib?

Please consult to the Wiki page to fix the problem.
https://github.com/pyenv/pyenv/wiki/Common-build-problems

BUILD FAILED (Amazon Linux 2 using python-build 2.3.17-10-g920ef145)

Inspect or clean up the working tree at /tmp/python-build.20230523135937.12069
Results logged to /tmp/python-build.20230523135937.12069.log

Last 10 log lines:
        LD_LIBRARY_PATH=/tmp/python-build.20230523135937.12069/Python-3.10.11 ./python -E -m ensurepip \
                $ensurepip --root=/ ; \
fi
Looking in links: /tmp/tmpvfgc3rev
Processing /tmp/tmpvfgc3rev/setuptools-65.5.0-py3-none-any.whl
Processing /tmp/tmpvfgc3rev/pip-23.0.1-py3-none-any.whl
Installing collected packages: setuptools, pip
  WARNING: The scripts pip3 and pip3.10 are installed in '/home/ec2-user/.pyenv/versions/3.10.11/bin' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Successfully installed pip-23.0.1 setuptools-65.5.0
```

openssl11とopenssl11-develをインストールするとopenssl11-develがopenssl-develとコンフリクトしていると言われるので、openssl-develをアンインストールし、再度openssl11-develをインストールします。
```console
user_name:~/environment $ sudo yum install -y openssl11 openssl11-devel
Loaded plugins: extras_suggestions, langpacks, priorities, update-motd
244 packages excluded due to repository priority protections
Package 1:openssl11-1.1.1g-12.amzn2.0.14.x86_64 already installed and latest version
Resolving Dependencies
--> Running transaction check
---> Package openssl11-devel.x86_64 1:1.1.1g-12.amzn2.0.14 will be installed
--> Processing Conflict: 1:openssl11-devel-1.1.1g-12.amzn2.0.14.x86_64 conflicts openssl-devel
--> Finished Dependency Resolution
Error: openssl11-devel conflicts with 1:openssl-devel-1.0.2k-24.amzn2.0.6.x86_64
 You could try using --skip-broken to work around the problem
 You could try running: rpm -Va --nofiles --nodigest
user_name:~/environment $ sudo yum remove -y openssl-devel
user_name:~/environment $ sudo yum install -y openssl11-devel
```

再度Python`3.10.11`をインストールすると、ERRORが取れました。
```console
user_name:~/environment $ pyenv install 3.10.11
Downloading Python-3.10.11.tar.xz...
-> https://www.python.org/ftp/python/3.10.11/Python-3.10.11.tar.xz
Installing Python-3.10.11...
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/home/ec2-user/.pyenv/versions/3.10.11/lib/python3.10/lzma.py", line 27, in <module>
    from _lzma import *
ModuleNotFoundError: No module named '_lzma'
WARNING: The Python lzma extension was not compiled. Missing the lzma lib?
Installed Python-3.10.11 to /home/ec2-user/.pyenv/versions/3.10.11
```

WARNINGが出ているので、xz-develをインストールします。もう一度Python`3.10.11`のインストールを確認するとWARNINGも取れました。
```console
user_name:~/environment $ sudo yum -y update
user_name:~/environment $ sudo yum -y install xz-devel
user_name:~/environment $ pyenv install 3.10.11
pyenv: /home/ec2-user/.pyenv/versions/3.10.11 already exists
continue with installation? (y/N) y
Downloading Python-3.10.11.tar.xz...
-> https://www.python.org/ftp/python/3.10.11/Python-3.10.11.tar.xz
Installing Python-3.10.11...
Installed Python-3.10.11 to /home/ec2-user/.pyenv/versions/3.10.11
```

Pythonのバージョンをインストールした`3.10.11`に切り替えます。
```console
user_name:~/environment $ pyenv global 3.10.11
user_name:~/environment $ python --version
Python 3.10.11
```

これでPythonのバージョンアップができました。

#### 3. AWS CDKのインストール、初期化
次はAWS CDKのインストールですが、Cloud9を使用する場合、Cloud9にはAWS CDKが既にインストールされているため、インストールは不要です。
```console
user_name:~/environment $ cdk --version
2.79.1 (build 2e7f8b7)
```

最新でない場合は、アップデートしましょう。
```console
user_name:~/environment $ npm update -g cdk
user_name:~/environment $ cdk --version
2.80.0 (build bbdb16a)
```

AWS CDKの作業ディレクトリを作成して、AWS CDKプロジェクトを初期設定します。
```console
user_name:~/environment $ mkdir cdk-app
user_name:~/environment $ cd cdk-app
user_name:~/environment/cdk-app $ cdk init app --language python
```

Python仮想環境をアクティブ化し、必要なモジュールをインストールします。
```console
user_name:~/environment/cdk-app (master) $ source .venv/bin/activate
(.venv) user_name:~/environment/cdk-app (master) $ python -m pip install -r requirements.txt
```

#### 4. コード変更
この時点で作業ディレクトリの下は以下のような構造になっています。
app.pyが、このアプリケーションのメインプログラム、cdk_app/cdk_app_stack.pyが、CDKスタックになり、S3などのAWSリソースを作成するコードを書いていくプログラムになります。（初期状態だと何も記載がないので、何も作成されない状態です。）
```console
(.venv) user_name:~/environment/cdk-app (master) $ tree 
.
├── app.py
├── cdk_app
│   ├── cdk_app_stack.py
│   └── __init__.py
├── cdk.json
├── README.md
├── requirements-dev.txt
├── requirements.txt
├── source.bat
└── tests
    ├── __init__.py
    └── unit
        ├── __init__.py
        └── test_cdk_app_stack.py
```

```python:app.py
#!/usr/bin/env python3
import os
import aws_cdk as cdk
from cdk_app.cdk_app_stack import CdkAppStack

app = cdk.App()
CdkAppStack(app, "CdkAppStack",
    # 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=cdk.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=cdk.Environment(account='123456789012', region='us-east-1'),

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

app.synth()
```

```python:cdk_app/cdk_app_stack.py
from aws_cdk import (
    # Duration,
    Stack,
    # aws_sqs as sqs,
)
from constructs import Construct

class CdkAppStack(Stack):

    def __init__(self, scope: 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, "CdkAppQueue",
        #     visibility_timeout=Duration.seconds(300),
        # )
```

サンプルとして記載されているコードを試しにS3バケットを作成するように変更します。
```python:cdk_app/cdk_app_stack.py
from aws_cdk import (
    # Duration,
    Stack,
    # ↓↓ コード変更 ↓↓
    aws_s3 as s3,
    RemovalPolicy
    # ↑↑ コード変更 ↑↑
)
from constructs import Construct

class CdkAppStack(Stack):

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

        # ↓↓ コード変更 ↓↓
        s3bucket = s3.Bucket(
            self,
            "MyS3Bucket",
            bucket_name = "cdk-test-bucket-2023aabb",
            removal_policy=RemovalPolicy.DESTROY
        )
        # ↑↑ コード変更 ↑↑
```

`cdk diff`を実行すると作成されるリソースの確認ができます。（今回だと、ResourcesにS3バケットが作成されることが確認できます。）
```console
(.venv) user_name:~/environment/cdk-app (master) $ cdk diff
Stack CdkAppStack
Parameters
[+] Parameter BootstrapVersion BootstrapVersion: {"Type":"AWS::SSM::Parameter::Value<String>","Default":"/cdk-bootstrap/xxxxxxxxx/version","Description":"Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"}

Conditions
[+] Condition CDKMetadata/Condition CDKMetadataAvailable: {"Fn::Or":[{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"af-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ca-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-northwest-1"]}]},{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-3"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"me-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"sa-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-2"]}]},{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-2"]}]}]}

Resources
[+] AWS::S3::Bucket MyS3Bucket MyS3BucketXXXXXXXX 

Other Changes
[+] Unknown Rules: {"CheckBootstrapVersion":{"Assertions":[{"Assert":{"Fn::Not":[{"Fn::Contains":[["1","2","3","4","5"],{"Ref":"BootstrapVersion"}]}]},"AssertDescription":"CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."}]}}
```

#### 5. デプロイ
では、実際にS3バケットを作成（デプロイ）していきますが、今回は初回なので、`cdk bootstrap`を実行します。
`cdk bootstrap`は、AWS CDKでデプロイする際に初回だけ必要なコマンドで、実行することで、デプロイに必要なリソース（S3バケットやCloudFormationの実行ロールなど）が作成されます。
なお、`cdk bootstrap`は"初回だけ"必要と書きましたが、リージョンの単位で実行する必要があるので、一度`cdk bootstrap`した後に利用するリージョンが増えた場合は再度`cdk bootstrap`を実行する必要があります。
```console
(.venv) user_name:~/environment/cdk-app (master) $ cdk bootstrap
```

ブートストラップが終わったら、いよいよデプロイです。デプロイは、`cdk deploy`を実行します。
```console
(.venv) user_name:~/environment/cdk-app (master) $ cdk deploy

✨  Synthesis time: 17.62s

CdkAppStack:  start: Building
(中略)
CdkAppStack: deploying... [1/1]
CdkAppStack: creating CloudFormation changeset...

 ✅  CdkAppStack

✨  Deployment time: 36.93s

Stack ARN:
(中略)
✨  Total time: 54.55s
```

これで、デプロイが完了しました。実際にマネージメントコンソールでS3バケットを確認してみます。ちゃんと出来てますね！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/a0e0bb0a-ab02-f0a9-db61-a83fcd0f23dc.png)

#### 6. リソース変更
次にデプロイしたリソースを変更してみましょう。先ほど作成したS3バケットのバージョニングを無効から有効に変更してみたいと思います。
まず、現在のS3バケットのバージョニングの設定を確認すると、無効になってます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/a070654f-f493-c33d-f7c4-66adfd1817ec.png)

では、AWS CDKで設定を変更していきます。
バージョニングを有効にするには、先ほどのコードに`versioned=True,`を追加します。
```python:cdk_app/cdk_app_stack.py
from aws_cdk import (
    # Duration,
    Stack,
    # ↓↓ コード変更 ↓↓
    aws_s3 as s3,
    RemovalPolicy
    # ↑↑ コード変更 ↑↑
)
from constructs import Construct

class CdkAppStack(Stack):

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

        # ↓↓ コード変更 ↓↓
        s3bucket = s3.Bucket(
            self,
            "MyS3Bucket",
            bucket_name = "cdk-test-bucket-2023aabb",
            # ↓↓ バージョニング有効 ↓↓
            versioned=True,
            # ↑↑ バージョニング有効 ↑↑
            removal_policy=RemovalPolicy.DESTROY
        )
        # ↑↑ コード変更 ↑↑
```

コードを変更して、`cdk diff`を実行するとVersioningConfigurationの設定が追加されることが確認できます。
```console
(.venv) user_name:~/environment/cdk-app (master) $ cdk diff
Stack CdkAppStack
Resources
[~] AWS::S3::Bucket MyS3Bucket MyS3BucketXXXXXXXX 
 └─ [+] VersioningConfiguration
     └─ {"Status":"Enabled"}
```

`cdk deploy`を実行します。
```console
(.venv) user_name:~/environment/cdk-app (master) $ cdk deploy
```

S3バケットのバージョニングの設定を確認すると、有効に変更されていました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/63d64929-e8ac-e2bc-c6f4-1c7b4f142c84.png)

#### 7. リソース削除
最後に作成したリソースを削除します。削除は、`cdk destroy`を実行します。
```console
(.venv) user_name:~/environment/cdk-app (master) $ cdk destroy
Are you sure you want to delete: CdkAppStack (y/n)? y
CdkAppStack: destroying... [1/1]

 ✅  CdkAppStack: destroyed
```

マネージメントコンソールでS3バケットを確認すると削除されていることが確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/d7795140-6059-ded1-7121-81f8880c6c73.png)

## まとめ
Cloud9を作成し、AWS CDKの導入～リソースのデプロイ、削除までの一連の流れを確認できました。今回は、S3バケット1つだけだったのであまり感じないですが、作成するリソースが沢山だったり何度も作成する状況だとマネージメントコンソールから作成するより楽で間違いもなく作成できそうです。
最後まで読んでいただいてありがとうございます。
少しでも参考になれば幸いです。
#### 参考文献
* [AWS Cloud Development Kit (AWS CDK) v2デベロッパーガイド](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/home.html)
* [AWS CDK Python Reference](https://docs.aws.amazon.com/cdk/api/v2/python/index.html)
* [【AWS】素の Cloud9 に Python 3.9 をインストールする](https://qiita.com/wa_0a/items/cf8fad294d74938c80ec)
* [ERROR: The Python ssl extension was not compiled. Missing the OpenSSL lib? #950](https://github.com/pyenv/pyenv/issues/950)