LoginSignup
6
5

More than 3 years have passed since last update.

AWS CDKをやってみた!

Last updated at Posted at 2020-07-19

やや長い前置き

この記事について

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

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

CDKとは?

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

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

知ったきっかけ


社内の技術習得活動で、チームメンバーが流してくれた情報に、さらっとAWS CDKのことが書いてありました。

最初は、存在を知らなくて、AWS SDKの誤植かなぁ…などと呑気なことを考えていました。

本題からそれるので深追いしませんが、流れてきた情報はとても有用です!ぜひご一読をオススメします。
https://tomomano.gitlab.io/intro-aws/


この記事で触れること

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を最低限の変更で一旦流し込んでみる!(1周目)

大雑把な手順としては、前述の、exampleをcloneしてきて、Pythonのnew-vpc-alb-asg-mysql
exampleをちょっとだけ変えてまずは流し込んで見ます。(これが1周目です。あとで、2周目があります。追記:3周目もあります。)

clone

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

$ 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となっています。
  • これで良ければ、このまま実行で構いませんが、私の場合は変更する必要があったので、変えます。
  • また、インラインで書かれると使い勝手が悪いので、変数に切り出すことにしました。
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を変更します。)

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)

流し込み!

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

# 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じゃないのかー)

$ cdk bootstrap

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

CloudFormationのStackが出来あがり、やがて、目当てのリソースが出来上がります。
全てを列挙出来ないので、Subnetだけ、マネジメントコンソールからの見え方を添付します。
cdk_subnet.png

この時点での問題点

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

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

リソースの削除

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

ひとやすみ(といいつつ案外重要なことを書く)

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

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

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

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)

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

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

2周目

2周目の変更点

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

リソース名を、akira-test-cdk-始まりに変える

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ポート利用

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

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の変更

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上で稼働させる。
(この辺、力尽きて説明が雑ですいません…まぁ、本題じゃないので。)

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した結果のエンドポイントを叩く。

3周目…

2周目までは、
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で、アカウントやリージョンを明示する

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を指定する。

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****************"})

これ以降は省略します。

その他の修正

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
pom.xml(抜粋です。実行形式のjarを生成します。)
  <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
      <executable>true</executable>
    </configuration>
  </plugin>

/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

サービスの有効化と実行

$ 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

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5