AWS CDKをやってみた！

# やや長い前置き
## この記事について
最近は「やってみた」系の記事はあまり書かなくなってきました。
というのも、公式のサイトがどこも充実しているので、結局は公式サイトをなぞるだけになってしまうからです。

**今回は、偶然にAWS CDKを知って、激しく感動したので、その気持ちのままに書いてみます。**

## CDKとは？

さっそく公式サイトからの引用となりますが、CloudFormationとかTeraformを使わずとも、馴染みのプログラミング言語で、AWSのリソースが作れる！！**これは、ナウい！ナウすぎます！！**

>AWS クラウド開発キット (AWS CDK) は、使い慣れたプログラミング言語を使用してクラウドアプリケーションリソースをモデル化およびプロビジョニングするためのオープンソースのソフトウェア開発フレームワークです。

https://aws.amazon.com/jp/cdk/

## 知ったきっかけ

<details>
<summary>社内の技術習得活動で、チームメンバーが流してくれた情報に、さらっとAWS CDKのことが書いてありました。</summary>
<div>
最初は、存在を知らなくて、AWS SDKの誤植かなぁ…などと呑気なことを考えていました。

本題からそれるので深追いしませんが、流れてきた情報はとても有用です！ぜひご一読をオススメします。
https://tomomano.gitlab.io/intro-aws/
</div>
</details>
## この記事で触れること
CDKは、充実したexampleがあります。
https://github.com/aws-samples/aws-cdk-examples

今回は、この中から、Pythonを用い、新しいVPCを作り、ALBやEC2をプロビジョニングする処理をなぞってみます。
https://github.com/aws-samples/aws-cdk-examples/tree/master/python/new-vpc-alb-asg-mysql
exampleをそのまま手持ちの環境に流し込めると良いのですが、なかなか、そういう訳にも行かないと思います。（VPCのCIDRが割当済みで衝突してしまうとかありますので）
**したがって、exampleのどの辺りをいじったら実用的か？という観点を中心に説明していきたいと思います。**

# やっと本題
ここでやっと本題に入ります。
## 事前条件
当然ながら、AWSアカウントが必要です。（この辺はさすがに省略します）
そして諸々のインストールが必要です。Workshopをこなすことで環境構築出来るので、Hello,CDK!くらいまでで良いので、実施することを激しくオススメします。
https://cdkworkshop.com/

## まずは、Exampleを最低限の変更で一旦流し込んでみる！（１周目）
大雑把な手順としては、前述の、exampleをcloneしてきて、Pythonのnew-vpc-alb-asg-mysql
exampleをちょっとだけ変えてまずは流し込んで見ます。（これが１周目です。あとで、２周目があります。追記：３周目もあります。）

### clone
書き忘れましたが、私は、Ubuntu18.04を使っています。
勉強ネタは、practiceというディレクトリにテーマごとにサブディレクトリを掘っています。
なので、~/practice/cdk/配下で、cloneをします。（この辺はよしなに変えてください）

```sh
$ git clone https://github.com/aws-samples/aws-cdk-examples.git
```
この結果、Pythonのnew-vpc-alb-asg-mysql配下は、以下のようになります。

```
$ tree
.
├── app.py
├── cdk.json
├── cdk_vpc_ec2
│   ├── cdk_ec2_stack.py
│   ├── cdk_rds_stack.py
│   └── cdk_vpc_stack.py
├── img_demo_cdk_vpc.png
├── README.md
├── requirements.txt
├── setup.py
└── user_data
    └── user_data.sh

2 directories, 10 files

```
### ファイル修正と実行
まずは、最低限の変更を行い、実際に、流し込んでみます。
修正するファイルは、

- cdk_ec2_stack.py
- cdk_vpc_stack.py

です。

- VPCのCIDRが、10.10.0.0/16となっています。
- これで良ければ、このまま実行で構いませんが、私の場合は変更する必要があったので、変えます。
- また、インラインで書かれると使い勝手が悪いので、変数に切り出すことにしました。

```diff:cdk_vpc_stack.py（変更前・後）
from aws_cdk import core
import aws_cdk.aws_ec2 as ec2

- 
+ vpc_cidr = "x.x.x.x/16" # CIDRを変数定義して、任意の値に書き換えます。

class CdkVpcStack(core.Stack):

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

        # The code that defines your stack goes here

        self.vpc = ec2.Vpc(self, "VPC",
                           max_azs=2,
-                           cidr="10.10.0.0/16", # CIDRが決め打ちで書いてある！
+                           cidr=vpc_cidr,                           
                           # configuration will create 3 groups in 2 AZs = 6 subnets.
                           subnet_configuration=[ec2.SubnetConfiguration(
                               subnet_type=ec2.SubnetType.PUBLIC,
                               name="Public",
                               cidr_mask=24
                           ), ec2.SubnetConfiguration(
                               subnet_type=ec2.SubnetType.PRIVATE,
                               name="Private",
                               cidr_mask=24
                           ), ec2.SubnetConfiguration(
                               subnet_type=ec2.SubnetType.ISOLATED,
                               name="DB",
                               cidr_mask=24
                           )
                           ],
                           # nat_gateway_provider=ec2.NatProvider.gateway(),
                           nat_gateways=2,
                           )
        core.CfnOutput(self, "Output",
                       value=self.vpc.vpc_id)

```



次に、EC2です。（要するにキーペアのkey_nameを変更します。）


```diff:cdk_ec2_stack.py（変更前・後）
from aws_cdk import core
import aws_cdk.aws_ec2 as ec2
import aws_cdk.aws_elasticloadbalancingv2 as elb
import aws_cdk.aws_autoscaling as autoscaling

ec2_type = "t2.micro"
- key_name = "id_rsa"  # Setup key_name for EC2 instance login
+ key_name = "hogehoge" # 予め用意したキーペアに変更します。  
linux_ami = ec2.AmazonLinuxImage(generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX,
                                 edition=ec2.AmazonLinuxEdition.STANDARD,
                                 virtualization=ec2.AmazonLinuxVirt.HVM,
                                 storage=ec2.AmazonLinuxStorage.GENERAL_PURPOSE
                                 )  # Indicate your AMI, no need a specific id in the region
with open("./user_data/user_data.sh") as f:
    user_data = f.read()


class CdkEc2Stack(core.Stack):

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

        # Create Bastion
        bastion = ec2.BastionHostLinux(self, "myBastion",
                                       vpc=vpc,
                                       subnet_selection=ec2.SubnetSelection(
                                           subnet_type=ec2.SubnetType.PUBLIC),
                                       instance_name="myBastionHostLinux",
                                       instance_type=ec2.InstanceType(instance_type_identifier="t2.micro"))
        
        # Setup key_name for EC2 instance login if you don't use Session Manager
        # bastion.instance.instance.add_property_override("KeyName", key_name)

        bastion.connections.allow_from_any_ipv4(
            ec2.Port.tcp(22), "Internet access SSH")

        # Create ALB
        alb = elb.ApplicationLoadBalancer(self, "myALB",
                                          vpc=vpc,
                                          internet_facing=True,
                                          load_balancer_name="myALB"
                                          )
        alb.connections.allow_from_any_ipv4(
            ec2.Port.tcp(80), "Internet access ALB 80")
        listener = alb.add_listener("my80",
                                    port=80,
                                    open=True)

        # Create Autoscaling Group with fixed 2*EC2 hosts
        self.asg = autoscaling.AutoScalingGroup(self, "myASG",
                                                vpc=vpc,
                                                vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE),
                                                instance_type=ec2.InstanceType(instance_type_identifier=ec2_type),
                                                machine_image=linux_ami,
                                                key_name=key_name,
                                                user_data=ec2.UserData.custom(user_data),
                                                desired_capacity=2,
                                                min_capacity=2,
                                                max_capacity=2,
                                                )

        self.asg.connections.allow_from(alb, ec2.Port.tcp(80), "ALB access 80 port of EC2 in Autoscaling Group")
        listener.add_targets("addTargetGroup",
                             port=80,
                             targets=[self.asg])

        core.CfnOutput(self, "Output",
                       value=alb.load_balancer_dns_name)

```


### 流し込み！

おまたせしました。とりあえず、リソースをこの状態で作ってみましょう。

```shell-session
# venvの設定をします。
$ cd ~/practice/cdk/aws-cdk-examples/python/new-vpc-alb-asg-mysql
$ python3 -m venv .env
$ source .env/bin/activate

# 依存ライブラリーのインストール
$ pip install -r requirements.txt

# CDKコマンドを投入し、CloudFormationテンプレートを生成します。
$ cdk ls
cdk-vpc
cdk-ec2
cdk-rds

$ cdk synth
Successfully synthesized to /home/****/practice/cdk/aws-cdk-examples/python/new-vpc-alb-asg-mysql/cdk.out
```

cdk.outというディレクトリーが出来、その中に、CloudFormationのテンプレートが出来ます。
（ちなみに、テンプレートはjsonでした…yamlじゃないのかー）


```shell-session
$ cdk bootstrap

$ cdk deploy cdk-vpc #時間がかかります。（以下、同文）
$ cdk deploy cdk-ec2
$ cdk deploy cdk-rds
```

CloudFormationのStackが出来あがり、やがて、目当てのリソースが出来上がります。
全てを列挙出来ないので、Subnetだけ、マネジメントコンソールからの見え方を添付します。
![cdk_subnet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130262/0cc035d2-0534-431c-ef4b-3c1b526f1e04.png)


### この時点での問題点

- myBastionというホストに、キーペアが登録されていないのでssh出来ません。Session Managerを使う前提のようです
- 同ホストのSecurityGroupが**sshフルオープンになっています**

これらは、２周目で解決することにします。
なので、せっかく作りましたが、あっさりと、リソースを削除します！

### リソースの削除

```shell-session
# 作った時と逆順に消してみました。これできれいに消えました。（予想通り、CloudFormationのStackが削除されます）
$ cdk destroy cdk-rds
$ cdk destroy cdk-ec2
$ cdk destroy cdk-vpc
```

## ひとやすみ（といいつつ案外重要なことを書く）

たとえば、プライベートのサブネットは、
「cdk-vpc/VPC/PrivateSubnet1」
と言った名前で出来ます。

リソース名に/（スラッシュ）って使えるんだ！？というのが新鮮でした。
ケバブケース（―つなぎ）と、CamelCaseが混在しているところが、お茶目ですね。

リソース名はどうやって決定されているのか？というと、（詳細は確認要ですが）
最初のcdk-vpcの部分は、app.py
にて、以下の指定をしているので、それで決まるようです。

```python:app.py（一部のみ抜粋）
vpc_stack = CdkVpcStack(app, "cdk-vpc")
ec2_stack = CdkEc2Stack(app, "cdk-ec2",
                        vpc=vpc_stack.vpc)
rds_stack = CdkRdsStack(app, "cdk-rds",
                        vpc=vpc_stack.vpc,
                        asg_security_groups=ec2_stack.asg.connections.security_groups)

```

従って、２周目に向けては、cdk-の部分をもう少し有意な値に変えたいと思います。
今回は（あまり有意でもないですが）akira-test-cdk-としてみます。

では、２周目に行きましょう！！レッツら！

## ２周目

### ２周目の変更点

- リソース名を、akira-test-cdk-始まりに変える
- Bastionにキーペアを割り当てる
- BastionのSecurityGroupのInboundのIPアドレスを絞り込む
- アプリのポート（8080）に行くように設定する
- RDSは作成せずに、とりあえず、DBに依存しないスモークテストアプリを動かしてみる！

#### リソース名を、akira-test-cdk-始まりに変える
```python:app.py（変更後）
#!/usr/bin/env python3

from aws_cdk import core

from cdk_vpc_ec2.cdk_vpc_stack import CdkVpcStack
from cdk_vpc_ec2.cdk_ec2_stack import CdkEc2Stack
from cdk_vpc_ec2.cdk_rds_stack import CdkRdsStack

app = core.App()

vpc_stack = CdkVpcStack(app, "akira-test-cdk-vpc")
ec2_stack = CdkEc2Stack(app, "akira-test-cdk-ec2",
                        vpc=vpc_stack.vpc)
rds_stack = CdkRdsStack(app, "akira-test-cdk-rds",
                        vpc=vpc_stack.vpc,
                        asg_security_groups=ec2_stack.asg.connections.security_groups)

app.synth()
```

#### Bastionにキーペアを割り当てる & SecurityGroupのInboundのIPアドレスを絞り込む & 8080ポート利用

各々、偽名なので、よしなに読み替えてください。

```diff:cdk_ec2_stack.py（変更前・後）
from aws_cdk import core
import aws_cdk.aws_ec2 as ec2
import aws_cdk.aws_elasticloadbalancingv2 as elb
import aws_cdk.aws_autoscaling as autoscaling

ec2_type = "t2.micro"
key_name = "hogehoge" # 予め用意したキーペアに変更します。 
linux_ami = ec2.AmazonLinuxImage(generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, # AmazonLinux2に変更
                                 edition=ec2.AmazonLinuxEdition.STANDARD,
                                 virtualization=ec2.AmazonLinuxVirt.HVM,
                                 storage=ec2.AmazonLinuxStorage.GENERAL_PURPOSE
                                 )  # Indicate your AMI, no need a specific id in the region
with open("./user_data/user_data.sh") as f:
    user_data = f.read()


class CdkEc2Stack(core.Stack):

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

        # Create Bastion
        bastion = ec2.BastionHostLinux(self, "myBastion",
                                       vpc=vpc,
                                       subnet_selection=ec2.SubnetSelection(
                                           subnet_type=ec2.SubnetType.PUBLIC),
                                       instance_name="myBastionHostLinux",
                                       instance_type=ec2.InstanceType(instance_type_identifier="t2.micro"))
        
        # Setup key_name for EC2 instance login if you don't use Session Manager
        # ここをアンコメントして、キーペアを登録する
-        # bastion.instance.instance.add_property_override("KeyName", key_name)
+       bastion.instance.instance.add_property_override("KeyName", key_name)
        # ソースIPを絞り込む
-        bastion.connections.allow_from_any_ipv4(
+        bastion.connections.allow_from(ec2.Peer.ipv4("x.x.x.x/32"), # ソースIP here!
            ec2.Port.tcp(22), "Internet access SSH")

        # Create ALB
        alb = elb.ApplicationLoadBalancer(self, "myALB",
                                          vpc=vpc,
                                          internet_facing=True,
                                          load_balancer_name="myALB"
                                          )
        alb.connections.allow_from_any_ipv4(
            ec2.Port.tcp(80), "Internet access ALB 80")
        listener = alb.add_listener("my80",
                                    port=80,
                                    open=True)

        # Create Autoscaling Group with fixed 2*EC2 hosts
        self.asg = autoscaling.AutoScalingGroup(self, "myASG",
                                                vpc=vpc,
                                                vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE),
                                                instance_type=ec2.InstanceType(instance_type_identifier=ec2_type),
                                                machine_image=linux_ami,
                                                key_name=key_name,
                                                user_data=ec2.UserData.custom(user_data),
                                                desired_capacity=2,
                                                min_capacity=2,
                                                max_capacity=2,
volume_type=autoscaling.EbsDeviceVolumeType.GP2,
                                                )
-        self.asg.connections.allow_from(alb, ec2.Port.tcp(80),
-         "ALB access 80 port of EC2 in Autoscaling Group")
+        self.asg.connections.allow_from(alb, ec2.Port.tcp(8080), 
+        "ALB access 8080 port of EC2 in Autoscaling Group")
        
        listener.add_targets("addTargetGroup",
-                             port=80, # 80 -> 8080へ変更
+                             port=8080, # 80 -> 8080へ変更

                             targets=[self.asg])

        core.CfnOutput(self, "Output",
                       value=alb.load_balancer_dns_name)


```

user_dataの変更

```sh:user_data.sh
#!/bin/bash
sudo yum update -y
sudo yum install -y java-11-amazon-corretto-headless
sudo yum install -y maven
# sudo yum -y install httpd php
# sudo chkconfig httpd on
# sudo service httpd start
```

### RDSは作成せずに、とりあえず、DBに依存しないスモークテストアプリを動かしてみる！

ひとまず、SpringBootのアプリをEC2上で稼働させる。
（この辺、力尽きて説明が雑ですいません…まぁ、本題じゃないので。）

```java:HelloController.java
package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/")
    public String hello() {
        return "Hello, Spring boot with AWS CDK";
    }
}
```

**アプリのEC2に紐づくSecurityGroupがALBからのアクセスしか受け付けていないため、現時点では、BastionからアプリのEC2に対して、ssh出来ません。従って、SecurityGroupを変更し、Bastionからのsshを受け入れるようにManualで変更してください。**


```
# jar をSCPなどでEC2に持っていく

# 起動する
$ java -jar demo-0.0.1-SNAPSHOT.jar

# ec2をDeployした結果のエンドポイントを叩く。
```

## ３周目…

２周目までは、
AutoScalingグループに紐づくec2インスタンスにsshして、jarのデプロイや実行をManualしていました。
この方法だと、インスタンスが停止した場合に、Manualでの操作は失われてしまいます。（先に気づくべきでした…）

従って、アプリがデプロイされた状態で、AMIを生成し、それを使うように変更します。
なお、この作業の過程で、「**Unable to determine AMI from AMI map since stack is region-agnostic**」というエラーにだいぶ苦しみました。app.pyで、明示的にアカウントやリージョンを指定する必要がありました。

変更するファイルは、以下の通りです。

- app.py
- cdk_ec2_stack.py
- その他の修正
  - user_data.sh
  - pom.xml（デプロイ資材）
  - spring-boot.service（アプリ資材なのでAMIに含める）

### app.pyで、アカウントやリージョンを明示する

```diff:appy.py（変更前・後）
#!/usr/bin/env python3

from aws_cdk import core

from cdk_vpc_ec2.cdk_vpc_stack import CdkVpcStack
from cdk_vpc_ec2.cdk_ec2_stack import CdkEc2Stack
from cdk_vpc_ec2.cdk_rds_stack import CdkRdsStack

app = core.App()
- 
+ env_TOKYO = core.Environment(account="123456789012", region="ap-northeast-1")
- vpc_stack = CdkVpcStack(app, "akira-test-cdk-vpc")
+ vpc_stack = CdkVpcStack(app, "akira-test-cdk-vpc", env=env_TOKYO)
- ec2_stack = CdkEc2Stack(app, "akira-test-cdk-ec2", 
+ ec2_stack = CdkEc2Stack(app, "akira-test-cdk-ec2", env=env_TOKYO,
                        vpc=vpc_stack.vpc)
- rds_stack = CdkRdsStack(app, "akira-test-cdk-rds", 
+ rds_stack = CdkRdsStack(app, "akira-test-cdk-rds", env=env_TOKYO,
                        vpc=vpc_stack.vpc,
                        asg_security_groups=ec2_stack.asg.connections.security_groups)

app.synth()
```

### cdk_ec2_stack.pyで、MyAMIを指定する。
```diff:cdk_ec2_stack.py（変更前・後）
from aws_cdk import core
import aws_cdk.aws_ec2 as ec2
import aws_cdk.aws_elasticloadbalancingv2 as elb
import aws_cdk.aws_autoscaling as autoscaling

ec2_type = "t2.micro"
key_name = "abe-dev-dxr-sandbox" #"id_rsa"  # Setup key_name for EC2 instance login 
- linux_ami = ec2.AmazonLinuxImage(generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, # AmazonLinux2に変更
-                                  edition=ec2.AmazonLinuxEdition.STANDARD,
-                                  virtualization=ec2.AmazonLinuxVirt.HVM,
-                                  storage=ec2.AmazonLinuxStorage.GENERAL_PURPOSE
-                                  )  # Indicate your AMI, no need a specific id in the region
+ linux_ami = ec2.MachineImage.generic_linux({"ap-northeast-1": "ami-0****************"})

これ以降は省略します。

```

### その他の修正

```sh:user_data.sh
#!/bin/bash
sudo yum update -y
# 以下は、AMIに含まれるので、コメントアウトします。（つうか、user_dataなしでも良い気がする…）
# sudo yum -y install java-11-amazon-corretto-headless
# sudo yum -y install maven
# sudo yum -y install httpd php
# sudo chkconfig httpd on
# sudo service httpd start
```

```xml:pom.xml（抜粋です。実行形式のjarを生成します。）
  <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
      <executable>true</executable>
    </configuration>
  </plugin>

```

```shell:/ect/systemd/system/spring-boot.service
[Unit]
Description=Spring Boot Demo

[Service]
ExecStart=/home/ec2-user/demo-0.0.1-SNAPSHOT.jar
Restart=always
Type = simple
User=ec2-user
Group=ec2-user
SuccessExitStatus= 143

[Install]
WantedBy=multi-user.target
```

サービスの有効化と実行

```shell-session
$ sudo systemctl enable spring-boot
$ sudo systemctl start spring-boot
```

## 感想
アプリの担当者がAWSの環境をチャチャっと作る際には、CloudFormationはちょっととっつきにくい印象があります。
CDKは、ポピュラーなプログラミング言語で、そこをラップしてくれるので、結果としてインフラコードが読みやすくなる印象があります。今回はPythonを使いましたが、他の言語でも試してみたいと思いました。

- AWS自体の知識は当然ながら必要
- CloudFormationも少しは知っていないと厳しい（cdk synthした結果を読める程度の知識が要る）
- Pythonの知識はさほど必要ではない（初級レベルで大丈夫）
- CDKのAPIの仕様は都度調べる必要がある（というか調べるべき）

API仕様は以下で参照出来る。
https://docs.aws.amazon.com/cdk/api/latest/python/modules.html

追記：
ちなみに、インフラ系のメンバーは、CloudFormationの方が分かりやすいとのこと。（まぁ、そうだろうなぁ）

## 参考
https://qiita.com/yhsmt/items/80916a13b3d7ae8adc7f
https://qiita.com/nomi3/items/0278d3476d690d7e6103