やや長い前置き
この記事について
最近は「やってみた」系の記事はあまり書かなくなってきました。
というのも、公式のサイトがどこも充実しているので、結局は公式サイトをなぞるだけになってしまうからです。
今回は、偶然にAWS CDKを知って、激しく感動したので、その気持ちのままに書いてみます。
CDKとは?
さっそく公式サイトからの引用となりますが、CloudFormationとかTeraformを使わずとも、馴染みのプログラミング言語で、AWSのリソースが作れる!!これは、ナウい!ナウすぎます!!
AWS クラウド開発キット (AWS CDK) は、使い慣れたプログラミング言語を使用してクラウドアプリケーションリソースをモデル化およびプロビジョニングするためのオープンソースのソフトウェア開発フレームワークです。
知ったきっかけ
社内の技術習得活動で、チームメンバーが流してくれた情報に、さらっとAWS CDKのことが書いてありました。
本題からそれるので深追いしませんが、流れてきた情報はとても有用です!ぜひご一読をオススメします。
https://tomomano.gitlab.io/intro-aws/
今回は、この中から、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となっています。
- これで良ければ、このまま実行で構いませんが、私の場合は変更する必要があったので、変えます。
- また、インラインで書かれると使い勝手が悪いので、変数に切り出すことにしました。
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を変更します。)
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だけ、マネジメントコンソールからの見え方を添付します。
この時点での問題点
- 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
にて、以下の指定をしているので、それで決まるようです。
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-始まりに変える
# !/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ポート利用
各々、偽名なので、よしなに読み替えてください。
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の変更
# !/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上で稼働させる。
(この辺、力尽きて説明が雑ですいません…まぁ、本題じゃないので。)
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で、アカウントやリージョンを明示する
# !/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を指定する。
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****************"})
これ以降は省略します。
その他の修正
# !/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
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
</plugin>
[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