これは 😺TECHSCORE Advent Calendar 2019😺の18日目の記事です。
今までCloudFormationテンプレートを直接YAMLで書いていましたが、作成するリソース数が多くなるにつれて記述量の多さに辛さを感じるようになってきました。
そんな折AWS CDKのことを知り、試しに簡単な構成をAWS CDKで作成してみることにしました。
利用するAWS CDKのAPIについて
Developer GuideやAPI Referenceを見ると、AWS CDKのAPIにはCloudFormationの各リソースタイプ(VPC等)と1対1で対応している低レベルなものと、より高レベルなもの(例えば、VPCやサブネットを作成する際にインターネットゲートウェイやルートテーブル、ルートといった関連リソースを自動的に作成・関連付けしてくれる)があるようですが、利用するAZやサブネットの個数を柔軟にコントロールしたかったので、今回は低レベルAPIの方を試してみたいと思います。
必要なソフトウェアをインストール
Getting Started With the AWS CDKに従って、AWS CDKを実行する環境を準備します。
AWS CDKの言語には、Pythonを選択します。
1 2 3 4 |
curl -sL https://rpm.nodesource.com/setup_10.x | sudo bash - sudo yum install python3 sudo yum install nodejs sudo npm install -g aws-cdk |
今回確認に利用した環境は、以下の通りです。
OS: Amazon Linux release 2 (Karoo)
node: v10.17.0
npm: 6.11.3
Python: 3.7.4
pip: 19.0.3
cdk: 1.18.0
AWSのクレデンシャルとアクセス権限を設定
AWSのクレデンシャルを環境変数に設定しておきます。
1 2 3 |
export AWS_ACCESS_KEY_ID=Specifies your access key. export AWS_SECRET_ACCESS_KEY=Specifies your secret access key. export AWS_DEFAULT_REGION=ap-northeast-1 |
IAMのアクセス権限には、以下のAWS管理ポリシーをアタッチしておきます。
1 2 3 |
AWSCloudFormationFullAccess AmazonEC2FullAccess AmazonS3FullAccess |
作成するリソースの構成
パブリックサブネット(/20)とプライベートサブネット(/20)をAZごとに1つずつ持つVPC(10.0.0.0/16)を作成します。
パブリックサブネットにはNAT Gatewayを配置します。
AWS CDKアプリの雛形を作成
Getting Started With the AWS CDKにあるようにアプリの雛形を作成し、AWS CDKのモジュールをインストールします。
今回はVPCとサブネットを作成しますので、コアモジュールとEC2モジュールをインストールします。
その他のモジュールについてはAWS CDK Python Referenceを参照してください。
1 2 3 4 5 6 7 |
mkdir my-network cd my-network cdk init --language python source .env/bin/activate pip install -r requirements.txt pip install --upgrade aws-cdk.core pip install --upgrade aws-cdk.aws_ec2 |
作成するAWSリソースのコードを追加
雛形を作成した時点では、以下のような構成となっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
└── my-network ├── README.md ├── app.py ├── cdk.json ├── my_network │ ├── __init__.py │ ├── my_network.egg-info │ │ ├── PKG-INFO │ │ ├── SOURCES.txt │ │ ├── dependency_links.txt │ │ ├── requires.txt │ │ └── top_level.txt │ └── my_network_stack.py ├── requirements.txt └── setup.py |
my_network_stack.py
を開くと、以下のようなコードが記述されています。
1 2 3 4 5 6 7 8 9 |
from aws_cdk import core class MyNetworkStack(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 |
コメントにあるように、この中に作成するリソースのコードを記述していきます。
作成するリソースの数が多いので、my_resourcesディレクトリを作成してそこに追加したモジュールを呼び出すようにします。
モジュール追加後の構成は以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
└── my-network ├── README.md ├── app.py ├── cdk.json ├── my_network │ ├── __init__.py │ ├── my_network.egg-info │ │ ├── PKG-INFO │ │ ├── SOURCES.txt │ │ ├── dependency_links.txt │ │ ├── requires.txt │ │ └── top_level.txt │ └── my_network_stack.py │ └── my_resources │ ├── __init__.py │ ├── availability_zone.py │ ├── nat_gateway.py │ ├── route.py │ ├── subnet.py │ └── vpc.py ├── requirements.txt └── setup.py |
追加したモジュールの内容は以下の通りです。
availability_zone.py(アベイラビリティーゾーンの定義)※東京リージョンに限定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class AvailabilityZone: def __init__(self, region='ap-northeast-1') -> None: self.__region = region if self.__region == 'ap-northeast-1': self.__names = ['ap-northeast-1a', 'ap-northeast-1c', 'ap-northeast-1d'] else: self.__names = [] @property def names(self) -> list: return self.__names def name(self, az_number) -> str: return self.__names[az_number] |
nat_gateway.py(NAT Gateway)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import hashlib from aws_cdk import ( core, aws_ec2, ) def create_nat_gateway(scope: core.Construct, vpc: aws_ec2.CfnVPC, subnet: aws_ec2.CfnSubnet) -> aws_ec2.CfnNatGateway: vpc_id = [tag['value'] for tag in vpc.tags.render_tags() if tag['key'] == 'Name'].pop() subnet_id = [tag['value'] for tag in subnet.tags.render_tags() if tag['key'] == 'Name'].pop() id = hashlib.md5(subnet_id.encode()).hexdigest() eip = aws_ec2.CfnEIP(scope, f'{vpc_id}/EIP-{id}') nat_gateway = aws_ec2.CfnNatGateway(scope, f'{vpc_id}/NatGateway-{id}', allocation_id=eip.attr_allocation_id, subnet_id=subnet.ref, tags=[core.CfnTag( key='Name', value=f'{vpc_id}/NatGateway-{id}', )], ) return nat_gateway |
route.py(ルートテーブル、ルート)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
import hashlib from aws_cdk import ( core, aws_ec2, ) def create_privagte_route_table(scope: core.Construct, vpc: aws_ec2.CfnVPC, nat_gateway: aws_ec2.CfnNatGateway) -> aws_ec2.CfnRouteTable: vpc_id = [tag['value'] for tag in vpc.tags.render_tags() if tag['key'] == 'Name'].pop() ngw_id = [tag['value'] for tag in nat_gateway.tags.render_tags() if tag['key'] == 'Name'].pop() id = hashlib.md5(ngw_id.encode()).hexdigest() route_table = aws_ec2.CfnRouteTable(scope, f'{vpc_id}/RouteTable-{id}', vpc_id=vpc.ref, tags=[core.CfnTag( key='Name', value=f'{vpc_id}/RouteTable-{id}', )], ) aws_ec2.CfnRoute(scope, f'{vpc_id}/Route-{id}', route_table_id=route_table.ref, destination_cidr_block='0.0.0.0/0', nat_gateway_id=nat_gateway.ref, ) return route_table def create_public_route_table(scope: core.Construct, vpc: aws_ec2.CfnVPC, internet_gateway: aws_ec2.CfnInternetGateway) -> aws_ec2.CfnRouteTable: vpc_id = [tag['value'] for tag in vpc.tags.render_tags() if tag['key'] == 'Name'].pop() igw_id = [tag['value'] for tag in internet_gateway.tags.render_tags() if tag['key'] == 'Name'].pop() id = hashlib.md5(igw_id.encode()).hexdigest() route_table = aws_ec2.CfnRouteTable(scope, f'{vpc_id}/RouteTable-{id}', vpc_id=vpc.ref, tags=[core.CfnTag( key='Name', value=f'{vpc_id}/RouteTable-{id}', )], ) aws_ec2.CfnRoute(scope, f'{id}/Route-{id}', route_table_id=route_table.ref, destination_cidr_block='0.0.0.0/0', gateway_id=internet_gateway.ref, ) return route_table def create_route_table_association(scope: core.Construct, vpc: aws_ec2.CfnVPC, subnet: aws_ec2.CfnSubnet, route_table: aws_ec2.CfnRouteTable) -> aws_ec2.CfnSubnetRouteTableAssociation: vpc_id = [tag['value'] for tag in vpc.tags.render_tags() if tag['key'] == 'Name'].pop() subnet_id = [tag['value'] for tag in subnet.tags.render_tags() if tag['key'] == 'Name'].pop() id = hashlib.md5(subnet_id.encode()).hexdigest() association = aws_ec2.CfnSubnetRouteTableAssociation(scope, f'{vpc_id}/SubnetRouteTableAssociation-{id}', route_table_id=route_table.ref, subnet_id=subnet.ref, ) return association |
subnet.py(サブネット)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
import hashlib import ipaddress import uuid from aws_cdk import ( core, aws_ec2, ) from my_resources import ( availability_zone, vpc, ) class SubnetGroup: def __init__(self, scope: core.Construct, vpc: aws_ec2.CfnVPC, *, desired_layers: int=2, desired_azs: int=2, region: str='ap-northeast-1', private_enabled: bool=True, cidr_mask: int=20) -> None: self._cidr_mask = cidr_mask self._desired_azs = desired_azs self._desired_layers = desired_layers self._private_enabled = private_enabled self._region = region self._reserved_azs = 5 self._reserved_layers = 3 self._scope = scope self._vpc = vpc self._desired_subnet_points = [] for layer_number in range(self._desired_layers): for az_number in range(self._desired_azs): self._desired_subnet_points.append([layer_number, az_number]) self._public_subnets = [] self._private_subnets = [] @property def cidr_mask(self) -> int: return self._cidr_mask @property def desired_azs(self) -> int: return self._desired_azs @property def desired_layers(self) -> int: return self._desired_layers @property def desired_subnet_points(self) -> list: return self._desired_subnet_points @property def private_enabled(self) -> bool: return self._private_enabled @property def private_subnets(self) -> list: return self._private_subnets @property def public_subnets(self) -> list: return self._public_subnets @property def region(self) -> str: return self._region @property def reserved_azs(self) -> int: return self._reserved_azs @property def reserved_layers(self) -> int: return self._reserved_layers @property def scope(self) -> core.Construct: return self._scope @property def vpc(self) -> aws_ec2.CfnVPC: return self._vpc def create_subnets(self) -> None: nw = ipaddress.ip_network(self.vpc.cidr_block) cidrs = list(nw.subnets(new_prefix=self.cidr_mask)) cidrs.reverse() az = availability_zone.AvailabilityZone() vpc_id = [tag['value'] for tag in self.vpc.tags.render_tags() if tag['key'] == 'Name'].pop() for layer in range(self.reserved_layers): for az_number in range(self.reserved_azs): current = [layer, az_number] cidr = str(cidrs.pop()) if current in self.desired_subnet_points: id = hashlib.md5(f'{layer}-{az_number}'.encode()).hexdigest() subnet = aws_ec2.CfnSubnet(self.scope, f'{vpc_id}/Subnet-{id}', cidr_block=cidr, vpc_id=self.vpc.ref, availability_zone=az.name(az_number), tags=[ core.CfnTag( key='Name', value=f'{vpc_id}/Subnet-{id}', ), core.CfnTag( key='Layer', value=f'{layer}', ), core.CfnTag( key='AZNumber', value=f'{az_number}', ), ], ) if self.private_enabled and layer > 0: self._private_subnets.append(subnet) else: self._public_subnets.append(subnet) |
vpc.py(VPC、インターネットゲートウェイ)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
from aws_cdk import ( core, aws_ec2, ) def create_vpc(scope: core.Construct, id: str, *, cidr='10.0.0.0/16', enable_dns_hostnames=True, enable_dns_support=True) -> aws_ec2.CfnVPC: vpc = aws_ec2.CfnVPC(scope, id, cidr_block=cidr, enable_dns_hostnames=enable_dns_hostnames, enable_dns_support=enable_dns_support, tags=[core.CfnTag( key='Name', value=id, )] ) return vpc def create_internet_gateway(scope: core.Construct, vpc: aws_ec2.CfnVPC) -> aws_ec2.CfnInternetGateway: vpc_id = [tag['value'] for tag in vpc.tags.render_tags() if tag['key'] == 'Name'].pop() internet_gateway = aws_ec2.CfnInternetGateway(scope, f'{vpc_id}/InternetGateway', tags=[core.CfnTag( key='Name', value=f'{vpc_id}/InternetGateway', )] ) aws_ec2.CfnVPCGatewayAttachment(scope, f'{vpc_id}/VPCGatewayAttachment', vpc_id=vpc.ref, internet_gateway_id=internet_gateway.ref, ) return internet_gateway |
network_stack.py
を以下のように編集して、追加したモジュールを呼び出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
from aws_cdk import core from my_resources import ( vpc, subnet, nat_gateway, route, ) class MyNetworkStack(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 vpc_id = 'MyVPC' subnet_desired_layers = 2 subnet_desired_azs = 2 private_subnet_enabled=True v = vpc.create_vpc(self, vpc_id) igw = vpc.create_internet_gateway(self, v) subnet_group = subnet.SubnetGroup(self, v, desired_layers=subnet_desired_layers, desired_azs=subnet_desired_azs, private_enabled=private_subnet_enabled) subnet_group.create_subnets() if subnet_group.public_subnets: public_route_table = route.create_public_route_table(self, v, igw) for public_subnet in subnet_group.public_subnets: route.create_route_table_association(self, v, public_subnet, public_route_table) if subnet_group.private_subnets: private_route_tables = [] for public_subnet in subnet_group.public_subnets: ngw = nat_gateway.create_nat_gateway(self, v, public_subnet) private_route_table = route.create_privagte_route_table(self, v, ngw) private_route_tables.append(private_route_table) for private_subnet in subnet_group.private_subnets: az_number = [tag['value'] for tag in private_subnet.tags.render_tags() if tag['key'] == 'AZNumber'].pop() route.create_route_table_association(self, v, private_subnet, private_route_tables[int(az_number)]) |
AWS CDKの実行
準備ができましたので、アプリのルートディレクトリ(my-network)でcdkを実行してみます。
まずは、
cdk diff
を実行して作成・変更されるリソースの差分を確認してみます。
1 |
cdk diff |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
Stack my-network Conditions [+] Condition CDKMetadataAvailable: {"Fn::Or":[{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ca-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-northwest-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-central-1"]}]},{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-3"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"me-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"sa-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-2"]}]}]} Resources [+] AWS::EC2::VPC MyVPC MyVPC [+] AWS::EC2::InternetGateway MyVPC--InternetGateway MyVPCInternetGateway [+] AWS::EC2::VPCGatewayAttachment MyVPC--VPCGatewayAttachment MyVPCVPCGatewayAttachment [+] AWS::EC2::Subnet MyVPC--Subnet-c7763203e20a64b270352752d6a1e7c6 MyVPCSubnetc7763203e20a64b270352752d6a1e7c6 [+] AWS::EC2::Subnet MyVPC--Subnet-c2eb282156233b5d827219971c8b04c2 MyVPCSubnetc2eb282156233b5d827219971c8b04c2 [+] AWS::EC2::Subnet MyVPC--Subnet-eca26941bc5187d1e2983961edb6dbb6 MyVPCSubneteca26941bc5187d1e2983961edb6dbb6 [+] AWS::EC2::Subnet MyVPC--Subnet-ea66c06c1e1c05fa9f1aa39d98dc5bc1 MyVPCSubnetea66c06c1e1c05fa9f1aa39d98dc5bc1 [+] AWS::EC2::RouteTable MyVPC--RouteTable-e7636240538bdd71bd55872aed605e26 MyVPCRouteTablee7636240538bdd71bd55872aed605e26 [+] AWS::EC2::Route e7636240538bdd71bd55872aed605e26--Route-e7636240538bdd71bd55872aed605e26 e7636240538bdd71bd55872aed605e26Routee7636240538bdd71bd55872aed605e26 [+] AWS::EC2::SubnetRouteTableAssociation MyVPC--SubnetRouteTableAssociation-8a80275c4aeaac9a8e6f6f36e18f5f5b MyVPCSubnetRouteTableAssociation8a80275c4aeaac9a8e6f6f36e18f5f5b [+] AWS::EC2::SubnetRouteTableAssociation MyVPC--SubnetRouteTableAssociation-7c555b7a2a9e217d5de327ca36a79a54 MyVPCSubnetRouteTableAssociation7c555b7a2a9e217d5de327ca36a79a54 [+] AWS::EC2::EIP MyVPC--EIP-8a80275c4aeaac9a8e6f6f36e18f5f5b MyVPCEIP8a80275c4aeaac9a8e6f6f36e18f5f5b [+] AWS::EC2::NatGateway MyVPC--NatGateway-8a80275c4aeaac9a8e6f6f36e18f5f5b MyVPCNatGateway8a80275c4aeaac9a8e6f6f36e18f5f5b [+] AWS::EC2::RouteTable MyVPC--RouteTable-f178a135091a35c9541fbf72fb1c8dc1 MyVPCRouteTablef178a135091a35c9541fbf72fb1c8dc1 [+] AWS::EC2::Route MyVPC--Route-f178a135091a35c9541fbf72fb1c8dc1 MyVPCRoutef178a135091a35c9541fbf72fb1c8dc1 [+] AWS::EC2::EIP MyVPC--EIP-7c555b7a2a9e217d5de327ca36a79a54 MyVPCEIP7c555b7a2a9e217d5de327ca36a79a54 [+] AWS::EC2::NatGateway MyVPC--NatGateway-7c555b7a2a9e217d5de327ca36a79a54 MyVPCNatGateway7c555b7a2a9e217d5de327ca36a79a54 [+] AWS::EC2::RouteTable MyVPC--RouteTable-765dc824da4c4f28ce886a61c6b54742 MyVPCRouteTable765dc824da4c4f28ce886a61c6b54742 [+] AWS::EC2::Route MyVPC--Route-765dc824da4c4f28ce886a61c6b54742 MyVPCRoute765dc824da4c4f28ce886a61c6b54742 [+] AWS::EC2::SubnetRouteTableAssociation MyVPC--SubnetRouteTableAssociation-ad32a7208c1d822e4f35b34060485714 MyVPCSubnetRouteTableAssociationad32a7208c1d822e4f35b34060485714 [+] AWS::EC2::SubnetRouteTableAssociation MyVPC--SubnetRouteTableAssociation-c73221f09ade1614a962f2c9c60cd682 MyVPCSubnetRouteTableAssociationc73221f09ade1614a962f2c9c60cd682 |
定義したリソースが追加の対象となっていることが確認できましたので、
cdk deploy
を実行して実際にリソースを作成します。
1 |
cdk deploy |
1 2 3 4 |
my-network: deploying... my-network: creating CloudFormation changeset... ... ✅ my-network |
cdk deploy
の実行完了後、AWSマネジメントコンソールでCloudFormationスタックを見ると、my-networkスタックでリソースが作成されていることが確認できました。
最後に
今回はAWS CDKの低レベルAPIを試してみましたが、プログラミング言語のループ構造を利用して同じリソースを複数作成できることだけでも、YAMLで直接記述するのと比べてコード量を大幅に減らすことができ、非常に魅力的なものだと感じました。
CloudFormationでリソースを作成したことがあればAPIは直感的に利用できるものとなっており、導入までの敷居は比較的低いのではないかと思います。
今後は本番環境での利用も想定して使っていきたいと思います。