【AWS】CloudFormation Modulesでパブリックネットワークを作成してみる

# CloudFormation Modulesとは
以下、公式ドキュメントより
> Modules are a way for you to package resource configurations for inclusion across stack templates, in a transparent, manageable, and repeatable way. 

リソースをパッケージ化（カプセル化）して再利用できるようにしましょう！
というものですね。
## ユースケース
パッと思いつくユースケースでは以下のようなものがあげられそうです。
>・固定パラメータを各個人にいじらせたくない
・同じような構成を複数作成する際のコード量削減
・専門知識がある方が一度書けば、深い知識がなくても誰でも作成可能になる

# 本記事で行うこと
今回は、以下のようなパブリックネットワーク構成を、モジュール化してみようと思います。
![pub_net.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1123830/cbe06bf6-d78a-b985-2788-50b5236e84e5.png)

## 準備
### 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`

実行完了後、ファイル（フォルダ）が追加されていることを確認します。
![cfn000001.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1123830/e3e69908-77d6-ba77-4df2-048c28a48e64.jpeg)
それぞれの役割は以下の通りです。
>・モジュールに使用する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/ ）

以下のように書いてみました。
先にモジュール用のテンプレートが正常に動くかを確認しておくといいと思います。

<details><summary>Module側のテンプレート</summary><div>

```yml
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
```

</div></details>

## 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をくっつけたファイルを探しているらしい。
[ここ](https://github.com/aws-cloudformation/cloudformation-cli/issues/793)を見る限り他にも悩んでる方はそこそこいそうですね。
解決もされていないみたいです。

結構な時間これの調査に費やしましたが、ちゃんとした対処法が現状わからないので
一旦submit実行時に作成されている`schema.json`ファイルの名前を`mytraining-network-public-module.json`に変更してあげます。

この事象について対処法ご存じの方がいたらご教示いただけると助かります！

―――――――――――――――――――――――――――――――――――――――
以上、公式ドキュメントに載ってない手順です（Trouble Shooting）
―――――――――――――――――――――――――――――――――――――――

気を取り直して、submitし直します。
![２.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1123830/4f3799d2-d835-9ee4-8a31-43dffe77dd22.png)
警告文が出てるけど、一応Submitは成功しました。
CloudFormationのコンソールを見に行くと、
`CloudFormationManagedUploadInfrastructure`という名前のスタックが作成されています。
![３.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1123830/e1cc55a5-a942-4ea7-4abd-3c2bb8f5c819.png)

## 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      
```
コンソールから見ると、以下のように追加されていることがわかります。(アアクティブｗ)
![cfn000006.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1123830/23807a40-808a-5b42-26ef-4e3a1fed2a89.jpeg)

## テンプレートファイル作成
モジュールを使用したテンプレートを作成していきます。
モジュール内からの参照もしたいので、パブリックサブネットにEC2インスタンスも立ててみます。
![pub_net2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1123830/86f28f60-347c-89a5-0e96-15696faac918.png)

以下のテンプレートでスタックを作成します、
（CLIでもマネコンでも可）

<details><summary>実際に作成するスタックのテンプレート</summary><div>

```yml
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
```

</div></details>

無事スタックが作成されました。

![４1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1123830/10c7f547-a6c8-e0db-1797-694fe86c6819.png)

出力タブを見ると、モジュール内で定義していたoutputsが出力されています。
![５.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1123830/a45aac62-77db-7761-7a95-7683689dbc30.png)

# あとがき
今回はCloudFormation Modulesを使用してみました。
TerraformのModuleと同じ感じかなと思っていましたが、モジュールの登録の部分でかなり違いがありますね。
モジュール自体を作成するのは少し面倒ですが、モジュールを使用する際はかなり簡単だなと思いました。（わざわざリポジトリを用意しなくても共有できる）
これから社内でもいっぱい作って潤沢にしたいですね！（笑）

あとは中盤のトラブルさえ解決すれば…

以上、ありがとうございました。