PythonでAWS CDKを始めてみよう

## AWS CDKとは

AWS CDKとは、VPCやEC2といったAWSリソースを、PythonやTypeScriptなどのプログラミング言語で定義することができるサービスです。

AWSではCloudFormationというサービスがあり、JSONやYAMLで書いたテンプレートを使ってAWSリソースを定義することができますが、冗長な表現で書かざるをえない部分があります。
AWS CDKでは使い慣れたプログラミングを使い、繰り返し処理などを使用することができるので、CloudFormationよりもとっつきやすくスマートな書き方ができる可能性を持ったサービスです。

## 導入準備

まず以下を導入&準備します。

- AWS CLI
- AWS Account and User
- Node.js
- IDE for your programming language
- AWS CDK Toolkit
- Python


### AWS CLI
必要なのでまだの方はインストールしましょう。

### AWS Account and User
AWSアカウントを作成しましょう。作成したアカウント情報をAWS CLIに設定しましょう。

### Node.js
必要なのでインストールしましょう。バージョンは`10.3.0`以上が必要です。
自分は`nodebrew`を使っています。

### IDE for your programming language
VScodeで良いと思います。

### AWS CDK Toolkit
npmコマンドでインストールします。

### Python
Python3を使います。


続いて、プロジェクトを開始します。

## プロジェクトの開始

プロジェクトのディレクトリを作成します。

```sh
mkdir qiita1204 && cd qiita1204
```

Pythonの仮想環境を作成します。プロジェクトごとに使用するパッケージを管理するためです。

```sh
$ python3 -m venv .venv
$ source .venv/bin/activate
```

仮想環境に入ると以下のようにターミナルに`(.venv)`の表示が出ます。

```sh
(.venv) dfukui@dfukui qiita_1204 %
```

環境を抜けるときはこちら。

```sh
$ deactivate
```

ではプロジェクトを作成します。

```sh
$ cdk init app --language python
```

`app`を`sample-app`に置き換えるとAmazonSQSキューとAmazonSNSトピックで構成したチュートリアル用のサンプルアプリを作成できます。

これからCDKコマンドを何回か使いますがコマンドの説明は以下で確認できます。

[AWS CDK Toolkit (cdk command)](https://docs.aws.amazon.com/ja_jp/cdk/latest/guide/cli.html)


AWS CDKで作成するリソースのパッケージを導入します。
インストールするパッケージは requirements.txt で指定します。
デフォルトで何が書いてある見てみましょう。

```txt:requirements.txt
-e .

```

パッケージ名がありませんが、`-e`オプションを指定する内容になっています。
`-e`オプションは setup.py の内容に従って必要な依存関係をインストールするオプションです。
このままで大丈夫です。

setup.py の`install_requires`にEC2のパッケージ情報を追加します。

```python:setup.py
    install_requires=[
        "aws-cdk.core==1.73.0",
        "aws-cdk.aws_ec2==1.73.0" # 追加
    ],
```

ではパッケージをインストールしましょう。


```sh
$ pip install -r requirements.txt
```

これでAWS CDKを使う準備が整いました。


## AWS CDKを書く

VPCを作り、その中にEC2を作ろうと思います。

```Python
from aws_cdk import (
    aws_ec2 as ec2,
    core
)


class Qiita1204Stack(core.Stack):

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

        # VPCを作成
        my_vpc = ec2.Vpc(
            self,
            id="my-vpc",
            cidr="192.168.0.0/16",
            nat_gateways=0, # NatGatewayを作成しない指定
            subnet_configuration=[
                ec2.SubnetConfiguration(
                    name="my-public-subnet",
                    subnet_type=ec2.SubnetType.PUBLIC,
                    cidr_mask=24
                )
            ]
        )

        # セキュリティグループを作成
        my_ec2_security_group = ec2.SecurityGroup(
            self,
            id="my-ec2-sg",
            vpc=my_vpc,
            allow_all_outbound=True,
            security_group_name="my-ec2-sg"
        )

        # 作成したセキュリティグループにインバウンド許可設定を追加
        my_ec2_security_group.add_ingress_rule(
            peer=ec2.Peer.ipv4("0.0.0.0/0"),
            connection=ec2.Port.tcp(22),
            description="allow ssh access"
        )

        #  AMIを指定
        amzn_linux = ec2.MachineImage.latest_amazon_linux(
            generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX,
            edition=ec2.AmazonLinuxEdition.STANDARD,
            virtualization=ec2.AmazonLinuxVirt.HVM,
            storage=ec2.AmazonLinuxStorage.GENERAL_PURPOSE,
            cpu_type=ec2.AmazonLinuxCpuType.X86_64
        )

        # EC2インスタンスを作成
        my_ec2_instance = ec2.Instance(
            self,
            id="my-ec2-instance",
            instance_type=ec2.InstanceType.of(
                ec2.InstanceClass.BURSTABLE2,
                ec2.InstanceSize.MICRO
            ),
            machine_image=amzn_linux,
            vpc=my_vpc,
            vpc_subnets=ec2.SubnetSelection(
                subnet_type=ec2.SubnetType.PUBLIC
            ),
            instance_name="my-ec2-instance",
            security_group=my_ec2_security_group
        )

```

リソースのオブジェクトを作成し、作成したリソースに設定をAddする、というような感覚ですね。
リソースと設定を抽象的に扱えるのでイメージしやすいです。


stackの作成が完了したら、`cdk diff`で作成/変更内容を確認しましょう。

```sh
(.venv) dfukui@dfukui qiita_1204 % cdk diff
Stack qiita-1204
IAM Statement Changes
┌───┬─────────────────────────────────────┬────────┬────────────────┬───────────────────────────┬───────────┐
│   │ Resource                            │ Effect │ Action         │ Principal                 │ Condition │
├───┼─────────────────────────────────────┼────────┼────────────────┼───────────────────────────┼───────────┤
│ + │ ${my-ec2-instance/InstanceRole.Arn} │ Allow  │ sts:AssumeRole │ Service:ec2.amazonaws.com │           │
└───┴─────────────────────────────────────┴────────┴────────────────┴───────────────────────────┴───────────┘
Security Group Changes
┌───┬──────────────────────┬─────┬────────────┬─────────────────┐
│   │ Group                │ Dir │ Protocol   │ Peer            │
├───┼──────────────────────┼─────┼────────────┼─────────────────┤
│ + │ ${my-ec2-sg.GroupId} │ In  │ TCP 22     │ Everyone (IPv4) │
│ + │ ${my-ec2-sg.GroupId} │ Out │ Everything │ Everyone (IPv4) │
└───┴──────────────────────┴─────┴────────────┴─────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Parameters
[+] Parameter SsmParameterValue:--aws--service--ami-amazon-linux-latest--amzn-ami-hvm-x86_64-gp2:C96584B6-F00A-464E-AD19-53AFF4B05118.Parameter SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter: {"Type":"AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>","Default":"/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2"}

Resources
[+] AWS::EC2::VPC my-vpc myvpc445F9E24
[+] AWS::EC2::Subnet my-vpc/my-public-subnetSubnet1/Subnet myvpcmypublicsubnetSubnet1SubnetF350DCAB
[+] AWS::EC2::RouteTable my-vpc/my-public-subnetSubnet1/RouteTable myvpcmypublicsubnetSubnet1RouteTableF483969B
[+] AWS::EC2::SubnetRouteTableAssociation my-vpc/my-public-subnetSubnet1/RouteTableAssociation myvpcmypublicsubnetSubnet1RouteTableAssociation6585BF60
[+] AWS::EC2::Route my-vpc/my-public-subnetSubnet1/DefaultRoute myvpcmypublicsubnetSubnet1DefaultRoute3EDDB535
[+] AWS::EC2::Subnet my-vpc/my-public-subnetSubnet2/Subnet myvpcmypublicsubnetSubnet2Subnet4FC98CCA
[+] AWS::EC2::RouteTable my-vpc/my-public-subnetSubnet2/RouteTable myvpcmypublicsubnetSubnet2RouteTable41719F68
[+] AWS::EC2::SubnetRouteTableAssociation my-vpc/my-public-subnetSubnet2/RouteTableAssociation myvpcmypublicsubnetSubnet2RouteTableAssociation9C9C9464
[+] AWS::EC2::Route my-vpc/my-public-subnetSubnet2/DefaultRoute myvpcmypublicsubnetSubnet2DefaultRoute82B77C27
[+] AWS::EC2::InternetGateway my-vpc/IGW myvpcIGW4A95849E
[+] AWS::EC2::VPCGatewayAttachment my-vpc/VPCGW myvpcVPCGW0343AEB8
[+] AWS::EC2::SecurityGroup my-ec2-sg myec2sg107DEA6F
[+] AWS::IAM::Role my-ec2-instance/InstanceRole myec2instanceInstanceRole0572A187
[+] AWS::IAM::InstanceProfile my-ec2-instance/InstanceProfile myec2instanceInstanceProfile49635338
[+] AWS::EC2::Instance my-ec2-instance myec2instance87A1F156

```

"Resources"は左からリソースの種類、名前（Nameタグ）、論理IDとなります。

作成したstackでは指定していない IAM や Route などが自動で作成されますね。
また、サブネットが自動で冗長化されています。サブネットの名前の末尾には"subnet"をつけてくれていますね。
その影響でサブネット名が"subnetSubnet"になってしまったので、この辺りは作りながら調整していきたいです。


環境に初めてデプロイする場合は先に`cdk bootstrap`を実行します。

では、デプロイします。

```sh
$ cdk deploy
```

管理コンソールからみるとこんな感じでリソースが出来上がってました。
リソースの名前はCDKが`プロジェクト名/リソース名`という形で設定してくれていますね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235642/cbddb888-29d1-4868-8410-359c13f666ce.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235642/1e68f328-d047-765f-b4d1-18af8a2339a0.png)


## AWS CDKを書く(Cfn編)
先ほど作成した環境はCDKが名前や細かなリソースの設定をよしなに対応してくれました。
しかしNameタグを指定したいなど細かな要望には対応できません。

CDKに任せず自分で細かく設定したい場合はCfnから始まるConstruct（CfnVPC, SecurityGroupなど）を使ってstackを作成します。

CDKのConstructについては以下の記事で分かりやすく説明されています。

[【AWS CDK】CDK標準の3種類のConstructを使って、AWSリソースをデプロイしてみた](https://dev.classmethod.jp/articles/aws-cdk-construct-explanation/)

先ほど作った環境をCfnから始まるConstructを使って作成してみましょう。

setup.py に`aws_iam`を追加して、


```python
    install_requires=[
        "aws-cdk.core==1.73.0",
        "aws-cdk.aws_ec2==1.73.0"
        "aws-cdk.aws_iam==1.73.0" # 追加
    ],
```

stackは以下のようになりました。

```python
from aws_cdk import (
    aws_ec2 as ec2,
    aws_iam as iam,
    core
)


class Qiita1204Stack(core.Stack):

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

        # IAMロールを作成
        my_role_ec2 = iam.CfnRole(
            self,
            id="my-role-ec2",
            assume_role_policy_document={
                "Version": "2012-10-17",
                "Statement": [{
                        "Action": "sts:AssumeRole",
                        "Effect": "Allow",
                        "Principal": {"Service": "ec2.amazonaws.com"}
                }]
            },
            description="the ec2 role",
            managed_policy_arns=[
                # "arn:aws:iam::aws:policy/AmazonS3FullAccess" # 付与したいアクセス権をリストする
            ],
            role_name="my-role-ec2",
            tags=[{
                    "key": "Name",
                    "value": "my-role-ec2"
            }]
        )
        # Instance Profileを作成
        my_instance_profile = iam.CfnInstanceProfile(
            self,
            id="my-instance-profile",
            roles=[my_role_ec2.ref]
        )

        # VPCを作成
        my_vpc = ec2.CfnVPC(
            self,
            id="my-vpc",
            cidr_block="192.168.0.0/16",
            enable_dns_hostnames=True,
            tags=[{
                    "key": "Name",
                    "value": "my-vpc"
            }]
        )

        # Subnetを作成
        my_subnet_1 = ec2.CfnSubnet(
            self,
            id="my-subnet",
            cidr_block="192.168.0.0/24",
            vpc_id=my_vpc.ref,
            availability_zone=core.Fn.select(0, core.Fn.get_azs("")),
            tags=[{
                    "key": "Name",
                    "value": "my-subnet-1"
            }]
        )

        # Internet Gatewayを作成
        my_igw = ec2.CfnInternetGateway(
            self,
            id="my-igw",
            tags=[{
                    "key": "Name",
                    "value": "my-igw"
            }]
        )
        # Internet Gatewayをアタッチ
        ec2.CfnVPCGatewayAttachment(
            self,
            id="my-igw-attachment",
            vpc_id=my_vpc.ref,
            internet_gateway_id=my_igw.ref
        )

        # Routetableを作成
        my_rtb = ec2.CfnRouteTable(
            self,
            id="my-rtb",
            vpc_id=my_vpc.ref,
            tags=[{
                    "key": "Name",
                    "value": "my-rtb"
            }]
        )
        # Routetableとサブネットの関連付け
        ec2.CfnSubnetRouteTableAssociation(
            self,
            id="my-rtb-association",
            route_table_id=my_rtb.ref,
            subnet_id=my_subnet_1.ref
        )

        # Routeの設定
        my_rt = ec2.CfnRoute(
            self,
            id="my-rt",
            route_table_id=my_rtb.ref,
            destination_cidr_block="0.0.0.0/0",
            gateway_id=my_igw.ref
        )

        # Security Groupの作成
        my_sg_ec2 = ec2.CfnSecurityGroup(
            self,
            id="my-sg-ec2",
            vpc_id=my_vpc.ref,
            group_description="my-sg-ec2",
            group_name="my-sg-ec2",
            security_group_ingress=[
                ec2.CfnSecurityGroup.IngressProperty(
                    ip_protocol="tcp",
                    cidr_ip="0.0.0.0/0",
                    from_port=22,
                    to_port=22
                )
            ],
            tags=[{
                    "key": "Name",
                    "value": "my-sg-ec2"
            }]
        )

        #  AMIを指定してimage_idを取得
        amzn_linux = ec2.MachineImage.latest_amazon_linux(
            generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX,
            edition=ec2.AmazonLinuxEdition.STANDARD,
            virtualization=ec2.AmazonLinuxVirt.HVM,
            storage=ec2.AmazonLinuxStorage.GENERAL_PURPOSE,
            cpu_type=ec2.AmazonLinuxCpuType.X86_64
        ).get_image(self).image_id

        # EC2を作成
        my_ec2 = ec2.CfnInstance(
            self,
            id="my-ec2",
            availability_zone=core.Fn.select(0, core.Fn.get_azs("")),
            block_device_mappings=[
                ec2.CfnInstance.BlockDeviceMappingProperty(
                    device_name="/dev/sda1",
                    ebs=ec2.CfnInstance.EbsProperty(
                        delete_on_termination=True,
                        encrypted=False,
                        volume_size=10,
                        volume_type="gp2"
                    )
                )
            ],
            credit_specification=ec2.CfnInstance.CreditSpecificationProperty(
                cpu_credits="standard"
            ),
            iam_instance_profile=my_instance_profile.ref,
            image_id=amzn_linux,
            instance_type="t2.micro",
            security_group_ids=[my_sg_ec2.ref],
            subnet_id=my_subnet_1.ref,
            tags=[{
                    "key": "Name",
                    "value": "my-ec2"
            }]
        )
```

コード量がプラス100行になりました。パラメータをひとつひとつ指定するためです。
細かく設定するのでリソースの関係性の理解が深まるのはメリットかと思いました。


`cdk diff`の結果はこのようになりました。

```sh
(.venv) dfukui@dfukui qiita_1204 % cdk diff
Stack qiita-1204
IAM Statement Changes
┌───┬────────────────────┬────────┬────────────────┬───────────────────────────┬───────────┐
│   │ Resource           │ Effect │ Action         │ Principal                 │ Condition │
├───┼────────────────────┼────────┼────────────────┼───────────────────────────┼───────────┤
│ + │ ${my-role-ec2.Arn} │ Allow  │ sts:AssumeRole │ Service:ec2.amazonaws.com │           │
└───┴────────────────────┴────────┴────────────────┴───────────────────────────┴───────────┘
Security Group Changes
┌───┬──────────────────────┬─────┬──────────┬─────────────────┐
│   │ Group                │ Dir │ Protocol │ Peer            │
├───┼──────────────────────┼─────┼──────────┼─────────────────┤
│ + │ ${my-sg-ec2.GroupId} │ In  │ TCP 22   │ Everyone (IPv4) │
└───┴──────────────────────┴─────┴──────────┴─────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Parameters
[+] Parameter SsmParameterValue:--aws--service--ami-amazon-linux-latest--amzn-ami-hvm-x86_64-gp2:C96584B6-F00A-464E-AD19-53AFF4B05118.Parameter SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter: {"Type":"AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>","Default":"/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2"}

Resources
[+] AWS::IAM::Role my-role-ec2 myroleec2
[+] AWS::IAM::InstanceProfile my-instance-profile myinstanceprofile
[+] AWS::EC2::VPC my-vpc myvpc
[+] AWS::EC2::Subnet my-subnet mysubnet
[+] AWS::EC2::InternetGateway my-igw myigw
[+] AWS::EC2::VPCGatewayAttachment my-igw-attachment myigwattachment
[+] AWS::EC2::RouteTable my-rtb myrtb
[+] AWS::EC2::SubnetRouteTableAssociation my-rtb-association myrtbassociation
[+] AWS::EC2::Route my-rt myrt
[+] AWS::EC2::SecurityGroup my-sg-ec2 mysgec2
[+] AWS::EC2::Instance my-ec2 myec2

(.venv) dfukui@dfukui qiita_1204 %
```

CDKが行ったサブネット周りの冗長化以外は同じリソースを定義できました。

デプロイするとこんな感じでリソースが出来上がっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235642/bc7925f9-42e5-096e-b116-7cfa6714bda4.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235642/602fcd77-38f7-5294-a728-aca638be23b8.png)

Nameタグがしっかり効いてますね。

## まとめ
Pythonを使ってリソースを設定するのは楽しいですね。今後も積極的に使っていきたいです。
同じリソースの定義でも使用するConstructによってコード量が大きく変わるので、要件に合わせてどちらを使うか判断していきたいです。
自分はなるべく楽をしたいのでCDKに任せる書き方をもう少し掘り下げて学習していきたいと思いました。