AWS CDK入門としてネットワークリソースを作ってみた

2019.11.29

AWS Cloud Development Kit (AWS CDK) 、乗り遅れた感がありますが私も始めました。

入門としてVPCなどのネットワークリソースを作成してみます。

目次

  1. 環境
  2. セットアップ
  3. Vpc() を使ってリソースを作成 (2行追記)
  4. CfnXXX() を使ってCloudFormationらしく作成 (184行追記)
  5. まとめ

環境

今回は Pythonを利用します。

インストールとセットアップ詳細は割愛します (以下の AWS Workshop, 弊社ブログを参照ください)

環境は以下のとおり

  • cdk: 1.18.0
  • Python: 3.7.3

セットアップ

初期セットアップしてきます。

  • practice-nw ディレクトリ配下にプロジェクトを作成
  • cdk init で空のCDKプロジェクトを作成
  • Python の仮想環境を有効化
mkdir practice-nw
cd practice-nw
cdk init app --language=python
source .env/bin/activate

今回は VPCやサブネットなどを作成するために awscdk.awsec2 を利用します。 setup.py にその旨を書きます(以下抜粋)。

./setup.py

install_requires=[
        "aws-cdk.core",
        "aws-cdk.aws-ec2"
    ],

pip install で必要なパッケージをインストールします。

pip install -r requirements.txt

(必要な方のみ) 該当リージョン上で初めてCDKを実行する場合は cdk bootstrap を一度実行する必要があります。

確認

cdk init で出来たものを確認していきます。

cdk list (もしくは cdk ls)でこのプロジェクトで作成されるスタック一覧を表示します。

$ cdk ls
practice-nw

"practice-nw" というスタックは次の app.py から生成されています。

./app.py

#!/usr/bin/env python3
from aws_cdk import core
from practice_nw.practice_nw_stack import PracticeNwStack

app = core.App()
PracticeNwStack(app, "practice-nw")
app.synth()

7行目ハイライト部分 PracticeNwStack のコンストラクタで スタックが生成されます。

さらに、この "PracticeNwStack" クラス は 3行目ハイライト部分の from practice_nw.practice_nw_stack ... によってインポートされています。

./practice_nw/practice_nw_stack.py

from aws_cdk import core

class PracticeNwStack(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

それでは、この practice_nw_stack.py にスタックのリソースを書いていきましょう。

Vpc() を使ってリソースを作成 (2行追記)

practice_nw_stack.py に (わずか) 2行追記しました。

./practice_nw/practice_nw_stack.py

from aws_cdk import core
from aws_cdk import aws_ec2 as ec2

class PracticeNwStack(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
        ec2.Vpc(self, "vpc")

デプロイする前に cdk diff で作られる予定のリソースを確認してみましょう。

cdk deploy でデプロイします。

↓のように作成中/作成したリソースを分かりやすく表示してくれます。

マネジメントコンソールで

  • CloudFormation スタックが作成されていること
  • 各種リソースが作成されていること

を確認しましょう。

Vpc() について

作成したリソースを図で表すと以下になります。

ec2.Vpc(self, "vpc") でこれだけのリソースをよしなに作成してくれるのは、 コンストラクタのパラメータにそれぞれデフォルトの値が定義されているからです。

Vpc() コンストラクタのパラメータを列挙してみます。

  • cidr
  • defaultInstanceTenancy
  • enableDnsHostnames
  • enableDnsSupport
  • gatewayEndpoints
  • maxAzs
  • natGatewayProvider
  • natGatewaySubnets
  • natGateways
  • subnetConfiguration
  • vpnConnections
  • vpnGateway
  • vpnGatewayAsn
  • vpnRoutePropagationcidr
  • defaultInstanceTenancy
  • enableDnsHostnames
  • enableDnsSupport
  • gatewayEndpoints

これらパラメータを指定することで、要件に合わせた VPCネットワークを作成することが出来ます。 ※それぞれの詳細は class Vpc (Python版) を参照ください

CfnXXX() を使ってCloudFormationらしく作成 (184行追記)

CfnVPC() のように CloudFormationのリソースと 1対1 で対応 するクラスがあります。 今回は Vpc() で出来る内容を (わざわざ) CfnXXX() でも作成してみます。

まず cdk destroy で前に作成したスタックを削除します。

practice_nw_cfn_stack.py を作成して、 そこにゴリゴリ書いていきます。

./practice_nw/practice_nw_cfn_stack.py

from aws_cdk import core
from aws_cdk import aws_ec2 as ec2


class PracticeNwCfnStack(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
        prefix = "PracticeNw"
        def name(s): return "{0}/{1}".format(prefix, s)

        # VPC
        vpc = ec2.CfnVPC(
            self, "vpc",
            cidr_block="192.168.0.0/16",
            enable_dns_hostnames=True,
            enable_dns_support=True,
            tags=[
                core.CfnTag(key="Name", value=name("vpc"))
            ]
        )

        # InternetGateway
        igw = ec2.CfnInternetGateway(
            self, "igw",
            tags=[
                core.CfnTag(key="Name", value=name("igw"))
            ]
        )
        igw_attachment = ec2.CfnVPCGatewayAttachment(
            self, "igw_attachment",
            vpc_id=vpc.ref,
            internet_gateway_id=igw.ref
        )

        # PrivateSubnetA
        private_subnet_a = ec2.CfnSubnet(
            self, "private_a",
            vpc_id=vpc.ref,
            cidr_block="192.168.0.0/24",
            availability_zone="ap-northeast-1a",
            tags=[
                core.CfnTag(key="Name", value=name("private_a"))
            ]
        )
        # PrivateSubnetC
        private_subnet_c = ec2.CfnSubnet(
            self, "private_c",
            vpc_id=vpc.ref,
            cidr_block="192.168.1.0/24",
            availability_zone="ap-northeast-1c",
            tags=[
                core.CfnTag(key="Name", value=name("private_c"))
            ]
        )

        # PublicSubnetA
        public_subnet_a = ec2.CfnSubnet(
            self, "public_a",
            vpc_id=vpc.ref,
            cidr_block="192.168.10.0/24",
            availability_zone="ap-northeast-1a",
            tags=[
                core.CfnTag(key="Name", value=name("public_a"))
            ]
        )
        # PublicSubnetC
        public_subnet_c = ec2.CfnSubnet(
            self, "public_c",
            vpc_id=vpc.ref,
            cidr_block="192.168.11.0/24",
            availability_zone="ap-northeast-1c",
            tags=[
                core.CfnTag(key="Name", value=name("public_c"))
            ]
        )

        # EIP1 (for NATGW)
        eip1 = ec2.CfnEIP(
            self, "eip1",
            domain="vpc",
        )
        eip1.add_depends_on(igw_attachment)

        # EIP2 (for NATGW)
        eip2 = ec2.CfnEIP(
            self, "eip2",
            domain="vpc",
        )
        eip2.add_depends_on(igw_attachment)

        # NatGatewayA
        natgw_a = ec2.CfnNatGateway(
            self, "natgw_a",
            allocation_id=eip1.attr_allocation_id,
            subnet_id=public_subnet_a.ref,
            tags=[
                core.CfnTag(key="Name", value=name("natgw_a"))
            ]
        )
        # NatGatewayC
        natgw_c = ec2.CfnNatGateway(
            self, "natgw_c",
            allocation_id=eip2.attr_allocation_id,
            subnet_id=public_subnet_c.ref,
            tags=[
                core.CfnTag(key="Name", value=name("natgw_c"))
            ]
        )

        # RouteTable of PrivateSubnetA
        rtb_private_a = ec2.CfnRouteTable(
            self, "rtb_private_a",
            vpc_id=vpc.ref,
            tags=[
                core.CfnTag(key="Name", value=name("rtb_private_a"))
            ]
        )
        ec2.CfnSubnetRouteTableAssociation(
            self, "rtb_private_a_association",
            route_table_id=rtb_private_a.ref,
            subnet_id=private_subnet_a.ref
        )
        ec2.CfnRoute(
            self, "route_private_a",
            route_table_id=rtb_private_a.ref,
            destination_cidr_block="0.0.0.0/0",
            nat_gateway_id=natgw_a.ref
        )

        # RouteTable of PrivateSubnetC
        rtb_private_c = ec2.CfnRouteTable(
            self, "rtb_private_c",
            vpc_id=vpc.ref,
            tags=[
                core.CfnTag(key="Name", value=name("rtb_private_c"))
            ]
        )
        ec2.CfnSubnetRouteTableAssociation(
            self, "rtb_private_c_association",
            route_table_id=rtb_private_c.ref,
            subnet_id=private_subnet_c.ref
        )
        ec2.CfnRoute(
            self, "route_private_c",
            route_table_id=rtb_private_c.ref,
            destination_cidr_block="0.0.0.0/0",
            nat_gateway_id=natgw_c.ref
        )

        # RouteTable of PublicSubnetA
        rtb_public_a = ec2.CfnRouteTable(
            self, "rtb_public_a",
            vpc_id=vpc.ref,
            tags=[
                core.CfnTag(key="Name", value=name("rtb_public_a"))
            ]
        )
        ec2.CfnSubnetRouteTableAssociation(
            self, "rtb_public_a_association",
            route_table_id=rtb_public_a.ref,
            subnet_id=public_subnet_a.ref
        )
        ec2.CfnRoute(
            self, "route_public_a",
            route_table_id=rtb_public_a.ref,
            destination_cidr_block="0.0.0.0/0",
            gateway_id=igw.ref
        )

        # RouteTable of PublicSubnetC
        rtb_public_c = ec2.CfnRouteTable(
            self, "rtb_public_c",
            vpc_id=vpc.ref,
            tags=[
                core.CfnTag(key="Name", value=name("rtb_public_c"))
            ]
        )
        ec2.CfnSubnetRouteTableAssociation(
            self, "rtb_public_c_association",
            route_table_id=rtb_public_c.ref,
            subnet_id=public_subnet_c.ref
        )
        ec2.CfnRoute(
            self, "route_public_c",
            route_table_id=rtb_public_c.ref,
            destination_cidr_block="0.0.0.0/0",
            gateway_id=igw.ref
        )

app.py を書き換えます。

./app.py

#!/usr/bin/env python3
from aws_cdk import core

# from practice_nw.practice_nw_stack import PracticeNwStack
from practice_nw.practice_nw_cfn_stack import PracticeNwCfnStack

app = core.App()
# PracticeNwStack(app, "practice-nw")
PracticeNwCfnStack(app, "practice-nw-cfn")
app.synth()

cdk deploy でデプロイします。

184行書いてできたものは 残念ながら 細かいパラメータに違いはあれど 2行追記した Vpc() と同じです。

まとめ

今回は 2パターンでネットワークリソースを作成してみました。 AWS CDK 便利すぎますね

AWS CDKの第1印象としては ネットワークリソースをよしなに作成してくるけど、細かい制御はできないのか〜 と、誤った印象を持っていました。

しかし Vpc()パラメータ を設定したり、 そもそも CloudFormationと 1対1で対応しているクラス を使用したりすることで、 CloudFormationテンプレートと同じレベルで制御ができることが分かりました。

(私はVSCodeで) 書いていて思ったことですが、プログラムで書いていることもあり、補完機能が素晴らしいです。

CloudFormationらしく 184行も追記しましたが、ほとんどストレスなく書けました。 「リソースのパラメータに何を書けば良いのか」 といった部分はポップアップ画面に出してくれるため、わざわざドキュメントを見に行く手間が省けます。

はじめのうちは、CDKになれるためにも CloudFormationらしく書いても良いかもしれません。

ただ、CDKらしく書くには、もちろん Vpc() のような より上位のコンストラクタ を使った方が便利・楽です。

その際は ドキュメントを見て

  • コンストラクタのパラメータが何なのか
  • それぞれのデフォルト値が何なのか

といったところをよく見る必要があるかと思います。