CloudFormation Modulesとは
以下、公式ドキュメントより
Modules are a way for you to package resource configurations for inclusion across stack templates, in a transparent, manageable, and repeatable way.
リソースをパッケージ化(カプセル化)して再利用できるようにしましょう!
というものですね。
ユースケース
パッと思いつくユースケースでは以下のようなものがあげられそうです。
・固定パラメータを各個人にいじらせたくない
・同じような構成を複数作成する際のコード量削減
・専門知識がある方が一度書けば、深い知識がなくても誰でも作成可能になる
本記事で行うこと
今回は、以下のようなパブリックネットワーク構成を、モジュール化してみようと思います。
準備
CloudFormation CLIのインストール
CloudFormation Moduleを使用するにあたって、CloudFormation CLI
をインストールする必要があります。
私の環境では、以下のコマンドを実行しインストールしました。
pip3 install cloudformation-cli-python-plugin
他のインストール方法については下記ドキュメントを参照してください。
https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/what-is-cloudformation-cli.html
初期化
moduleを作成したいフォルダを作成、移動します。
下記コマンドを実行し、必要なファイルを作成します。
cfn init
実行すると入力を求められるので、m
と入力します。
もう一度入力が求められるので、こちらはモジュール名を入力します。
(<Organization>::<Service>::<Name>::MODULE)
私の場合は以下のようにしました。
Mytraining::Network::Public::MODULE
実行完了後、ファイル(フォルダ)が追加されていることを確認します。
それぞれの役割は以下の通りです。
・モジュールに使用するCloudFormationテンプレートを含むfragmentsフォルダ
・モジュールの名前など、モジュールに関する詳細を含むrpdk-configファイル
・cfnコマンドを実行した際のログを格納するrpdk.log
Moduleテンプレートの作成
では早速moduleテンプレートを作成していきます!
なお、私はyaml派なのでyamlを使用して書きたいと思います。
(moduleのyaml形式は2021/4/13にサポート開始されました。
https://aws.amazon.com/jp/about-aws/whats-new/2021/04/aws-cloudformation-modules-provides-yaml-delimiter-support/ )
以下のように書いてみました。
先にモジュール用のテンプレートが正常に動くかを確認しておくといいと思います。
Module側のテンプレート
AWSTemplateFormatVersion: '2010-09-09'
Description: Public Network Module
Parameters:
VpcCidr:
Description: CIDR Block for the VPC
Type: String
MinLength: 9
MaxLength: 18
Default: 10.0.0.0/16
AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
ConstraintDescription: Must be like 10.0.0.0/16
VpcName:
Description: Name of the VPC
Type: String
MinLength: 1
MaxLength: 100
Default: Default-VPC
PubSubnetCidr:
Description: CIDR Block for the public subnet
Type: String
MinLength: 9
MaxLength: 18
Default: 10.0.0.0/24
AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
ConstraintDescription: Must be like 10.0.0.0/24
PubSubnetName:
Description: Name of the Public Subnet
Type: String
MinLength: 1
MaxLength: 100
Default: Default-PubSubnet
PriSubnetCidr:
Description: CIDR Block for the private subnet
Type: String
MinLength: 9
MaxLength: 18
Default: 10.0.1.0/24
AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
ConstraintDescription: Must be like 10.0.0.0/24
PriSubnetName:
Description: Name of the Private Subnet
Type: String
MinLength: 1
MaxLength: 100
Default: Default-PriSubnet
IgwName:
Description: Name of the InternetGateway
Type: String
MinLength: 1
MaxLength: 100
Default: Default-igw
PubRouteTableName:
Description: Name of the Public RouteTable
Type: String
MinLength: 1
MaxLength: 100
Default: Default-PubRtb
PriRouteTableName:
Description: Name of the Private RouteTable
Type: String
MinLength: 1
MaxLength: 100
Default: Default-PriRtb
Resources:
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
InstanceTenancy: default
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Ref VpcName
PubSubnet:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: !Ref PubSubnetCidr
VpcId: !Ref Vpc
AvailabilityZone: !Select [ 0, !GetAZs ]
MapPublicIpOnLaunch: True
Tags:
- Key: Name
Value: !Ref PubSubnetName
PriSubnet:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: !Ref PriSubnetCidr
VpcId: !Ref Vpc
AvailabilityZone: !Select [ 0, !GetAZs ]
MapPublicIpOnLaunch: True
Tags:
- Key: Name
Value: !Ref PriSubnetName
Igw:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Ref IgwName
IgwAttach:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref Igw
VpcId: !Ref Vpc
PubRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Ref PubRouteTableName
PubRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PubRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref Igw
PubSubRtbAssoc:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PubSubnet
RouteTableId: !Ref PubRouteTable
PriRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Ref PriRouteTableName
PriSubRtbAssoc:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PriSubnet
RouteTableId: !Ref PriRouteTable
Outputs:
VpcId:
Description: VPC ID
Value: !Ref Vpc
PubSubnetId:
Description: Public Subnet ID
Value: !Ref PubSubnet
PriSubnetId:
Description: Private Subnet ID
Value: !Ref PriSubnet
IgwId:
Description: InternetGateway ID
Value: !Ref Igw
PubRtbId:
Description: Public RouteTable ID
Value: !Ref PubRouteTable
PriRtbId:
Description: Private RouteTable ID
Value: !Ref PriRouteTable
CloudFormation RegistryにSubmit
ファイルが用意出来たら、CloudFormation Registryに登録します。
以下のコマンドを実行します。
cfn submit
――――――――――――――――――――――――――――――――――――――
以下、公式ドキュメントに載ってない手順です(Trouble Shooting)
――――――――――――――――――――――――――――――――――――――
submitの出力を見ると、どうやら失敗している…
Module fragment is valid.
=== Unhandled exception ===
Please report this issue to the team.
Please include the log file 'rpdk.log'
・・・なんでエラーが…
rpdk.logを見てみると
Traceback (most recent call last):
File "c:\users\ts120055\anaconda3\lib\site-packages\rpdk\core\cli.py", line 100, in main
args.command(args)
File "c:\users\ts120055\anaconda3\lib\site-packages\rpdk\core\submit.py", line 15, in submit
project.submit(
File "c:\users\ts120055\anaconda3\lib\site-packages\rpdk\core\project.py", line 502, in submit
zip_file.write(self.schema_path, SCHEMA_UPLOAD_FILENAME)
File "c:\users\ts120055\anaconda3\lib\zipfile.py", line 1741, in write
zinfo = ZipInfo.from_file(filename, arcname,
File "c:\users\ts120055\anaconda3\lib\zipfile.py", line 523, in from_file
st = os.stat(filename)
FileNotFoundError: [WinError 2] 指定されたファイルが見つかりません。: 'C:\\Users\\~~~~\\cloudformation_module\\pub_network\\mytraining-network-public-module.json'
どうやらモジュール名に指定していた<Organization>::<Service>::<Name>::MODULEをくっつけたファイルを探しているらしい。
ここを見る限り他にも悩んでる方はそこそこいそうですね。
解決もされていないみたいです。
結構な時間これの調査に費やしましたが、ちゃんとした対処法が現状わからないので
一旦submit実行時に作成されているschema.json
ファイルの名前をmytraining-network-public-module.json
に変更してあげます。
この事象について対処法ご存じの方がいたらご教示いただけると助かります!
―――――――――――――――――――――――――――――――――――――――
以上、公式ドキュメントに載ってない手順です(Trouble Shooting)
―――――――――――――――――――――――――――――――――――――――
気を取り直して、submitし直します。
警告文が出てるけど、一応Submitは成功しました。
CloudFormationのコンソールを見に行くと、
CloudFormationManagedUploadInfrastructure
という名前のスタックが作成されています。
CloudFormation Registry確認
Registryにモジュールが登録されているかを確認しましょう!
以下のコマンドを実行します
(Mytraining::Network::Public::MODULEは自分で指定したモジュール名を指定します)
aws cloudformation describe-type --type MODULE --type-name Mytraining::Network::Public::MODULE
以下のような出力が返ってきます。
{
"Arn": "arn:aws:cloudformation:ap-northeast-1:669389395244:type/module/Mytraining-Network-Public-MODULE/00000001",
"Type": "MODULE",
"TypeName": "Mytraining::Network::Public::MODULE",
"DefaultVersionId": "00000001",
"IsDefaultVersion": true,
"Description": "Schema for Module Fragment of type Mytraining::Network::Public::MODULE",
"Schema": "{\r\n \"typeName\": \"Mytraining::Network::Public::MODULE\",\r\n \"description\": \"Schema for Module Fragment of type Mytraining::Network::Public::MODULE\",\r\n \"properties\": {\r\n \"Parameters\": {\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"VpcCidr\": {\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"Type\": {\r\n \"type\": \"string\"\r\n },\r\n \"Description\": {\r\n
\"type\": \"string\"\r\n }\r\n },\r\n \"required\": [\r\n \"Type\",\r\n \"Description\"\r\n
],\r\n \"description\": \"CIDR Block for the VPC\"\r\n },\r\n \"VpcName\": {\r\n \"type\": \"object\",\r\n
\"properties\": {\r\n \"Type\": {\r\n \"type\": \"string\"\r\n },\r\n \"Description\": {\r\n
\"type\": \"string\"\r\n }\r\n
コンソールから見ると、以下のように追加されていることがわかります。(アアクティブw)
テンプレートファイル作成
モジュールを使用したテンプレートを作成していきます。
モジュール内からの参照もしたいので、パブリックサブネットにEC2インスタンスも立ててみます。
以下のテンプレートでスタックを作成します、
(CLIでもマネコンでも可)
実際に作成するスタックのテンプレート
AWSTemplateFormatVersion: '2010-09-09'
Description: Public Network Template
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "VPC CIDR"
Parameters:
- VpcCidr
- Label:
default: "Public Subnet CIDR"
Parameters:
- PubSubnetCidr
- Label:
default: "Private Subnet CIDR"
Parameters:
- PriSubnetCidr
Parameters:
VpcCidr:
Description: CIDR Block for the VPC
Type: String
MinLength: 9
MaxLength: 18
Default: 10.0.0.0/16
AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
ConstraintDescription: Must be like 10.0.0.0/16
PubSubnetCidr:
Description: CIDR Block for the public subnet
Type: String
MinLength: 9
MaxLength: 18
Default: 10.0.0.0/24
AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
ConstraintDescription: Must be like 10.0.0.0/24
PriSubnetCidr:
Description: CIDR Block for the private subnet
Type: String
MinLength: 9
MaxLength: 18
Default: 10.0.1.0/24
AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
ConstraintDescription: Must be like 10.0.0.0/24
Resources:
PubNetwork:
Type: Mytraining::Network::Public::MODULE
Properties:
VpcCidr: !Ref VpcCidr
VpcName: Mytraining-VPC
PubSubnetCidr: !Ref PubSubnetCidr
PubSubnetName: Mytraining-PubSubnet
PriSubnetCidr: !Ref PriSubnetCidr
PriSubnetName: Mytraining-PriSubnet
IgwName: Mytraining-Igw
PubRouteTableName: Mytraining-PubRouteTable
PriRouteTableName: Mytraining-PriRouteTable
SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
VpcId: !Ref PubNetwork.Vpc
GroupDescription: Security Group for WebServer
Tags:
-
Key: Name
Value: Mytraining-SG
Instance:
Type: "AWS::EC2::Instance"
Properties:
ImageId: ami-0ca38c7440de1749a
InstanceType: t2.micro
NetworkInterfaces:
- AssociatePublicIpAddress: "true"
DeviceIndex : "0"
SubnetId: !Ref PubNetwork.PubSubnet
GroupSet :
- !Ref SecurityGroup
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
VolumeType: gp2
DeleteOnTermination: true
VolumeSize: 8
Monitoring: false
Tags:
-
Key: Name
Value: Mytraining-Instance
Tenancy: default
無事スタックが作成されました。
出力タブを見ると、モジュール内で定義していたoutputsが出力されています。
あとがき
今回はCloudFormation Modulesを使用してみました。
TerraformのModuleと同じ感じかなと思っていましたが、モジュールの登録の部分でかなり違いがありますね。
モジュール自体を作成するのは少し面倒ですが、モジュールを使用する際はかなり簡単だなと思いました。(わざわざリポジトリを用意しなくても共有できる)
これから社内でもいっぱい作って潤沢にしたいですね!(笑)
あとは中盤のトラブルさえ解決すれば…
以上、ありがとうございました。