AWS CloudFormationのテンプレートをLayer別に作成してクロススタック参照を作ってみた。その1 [ Network-Layer編 ]

## AWS CloudFormationとは
CloudFormationはYamlもしくはJSONを使用してAWSリソースを自動で構築するAWSサービスでAWS環境をコードでテンプレート化しておくことで、効率化、ミスの軽減、複製の簡易化などが可能になります。

## 概要
テンプレートと言われる構築環境をコード化させたファイルに記載し、スタックと呼ばれる作業を行うことでGUI上で作成した状況と同じ環境が自動的に作成される。1つのテンプレートに全ての環境を記載することも可能だが、AWSからは各レイヤー別に分けることがベストプラクティスと言われている。

:::note 
今回は以下3つのLayerに分けて作成する。
- **Network Layer　　　←今回はここ！**
- Security Layer
- Application Layer
:::

## クロススタック参照
各レイヤー別にテンプレートを作成し1つの環境構築を行うためには、テンプレート間の互換性が必要になる。そのためには`クロススタック参照`を使用して他レイヤーの値を参照できるようにコードに'組込関数'と呼ばれる関数を活用していく作業がポイント。

## 構成図
まずはベースとなるVPC関連から作成していく。
![Network.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2601549/52db54cb-0894-714d-88d6-c4626898be33.png)






## Network Layer
まずはベースとなるVPC、Subnet（Private/Public),InternetGateway、RouteTable、VPCEndpointを作成する。


### Parameters

:::note 
Parameters内の詳細説明
#### Metadata
AWS::CloudFormation::Interface を記載して入力パラメータのグループ化や順序を指定することで、GUIでスタックを作成する際に表示される順番を指定することが可能になる（しないと順序バラバラで表示される）

#### Metadata AWS::CloudFormation::Interface
　・ParameterGroups（パラメータグループの宣言）
　・Label（パラメータグループのグループ名を指定）
　・Parameters（パラメータグループに含めるパラメータとその表示順を指定）
:::


```yaml:Network-Layer.yaml
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label: 
          default: "PJPrefix"
        Parameters: 
          - PJPrefix
      - Label:
          default: Network Layer Configuration
        Parameters:
          - AvailabilityZoneName
          - VPCCIDR
          - PublicSubnetACIDR
          - PublicSubnetCCIDR
          - PrivateSubnetACIDR
          - PrivateSubnetCCIDR
          - PublicSubnetName
          - PrivateSubnetName

    ParameterLabels: 
      AvailabilityZoneName:
        default: AvailabilityZoneName
      VPCCIDR: 
        default: VPC CIDR
      PublicSubnetACIDR: 
        default: PublicSubnetA CIDR
      PublicSubnetCCIDR: 
        default: PublicSubnetC CIDR
      PrivateSubnetACIDR: 
        default: PrivateSubnetA CIDR
      PrivateSubnetCCIDR: 
        default: PrivateSubnetC CIDR
      PublicSubnetName: 
        default: PublicSubnetName
      PrivateSubnetName: 
        default: PrivateSubnetName

#---------------------------------------------------
# input Parameters
#---------------------------------------------------   
Parameters:
  PJPrefix: 
    Type: String
    Default:"-"

  AvailabilityZoneName:
    Description: input AvailabilityZone
    Type: String
    Default: ap-northeast-1

  VPCCIDR:
    Type: String
    Default: "10.0.0.0/16"

  PublicSubnetACIDR:
    Type: String
    Default: "10.0.10.0/24"

  PublicSubnetCCIDR:
    Type: String
    Default: "10.0.20.0/24"

  PrivateSubnetACIDR:
    Type: String
    Default: "10.0.30.0/24"

  PrivateSubnetCCIDR:
    Type: String
    Default: "10.0.40.0/24"

  PublicSubnetName:
    Description: input PublicSubnet
    Type: String
    Default: PublicSubnet1

  PrivateSubnetName:
    Description: input PrivateSubnet
    Type: String
    Default: PrivateSubnet1
```

### VPC
:::note 
CidrBlockはParametersに設定したものをRef関数で反映させ、同様にValueにはSub関数を使用しプロジェクト名を反映させる。こうすることで、運用時の修正にはParametersの値を変更するだけで済みミスの防止と汎用性が向上する。
[AWS公式：組込関数リファレンス](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html)
:::

```yaml:Network-Layer.yaml
CfnVPC:
    Type: AWS::EC2::VPC
    Properties: 
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: "true"
      EnableDnsHostnames: "true"
      InstanceTenancy: default
      Tags: 
        - Key: Name
          Value: !Sub "${PJPrefix}-CfnVPC"
```
 ※ハードウェア専用インスタンスは今回使用しないのでテナンシーはdefault設定となる。

### Subnet
SubnetはPublic/Privateを各2つずつ作成する。
TagsのValueについて今回はSub関数でParametersの値をテストで引用し後ろにa/cをつけてみる。
```yaml:Network-Layer.yaml
# Public-Subnetを2つ作成(1a/1c)
  CfnPublicSubnet1a:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Ref PublicSubnetACIDR
      MapPublicIpOnLaunch: true
      VpcId: !Ref CfnVPC
      AvailabilityZone: !Sub "${AvailabilityZoneName}a"
      Tags:
        - Key: Name
          Value: !Sub "${PublicSubnetName}a"

  CfnPublicSubnet1c:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Ref PublicSubnetCCIDR
      MapPublicIpOnLaunch: true
      VpcId: !Ref CfnVPC
      AvailabilityZone: !Sub "${AvailabilityZoneName}c"
      Tags:
        - Key: Name
          Value: !Sub "${PublicSubnetName}c"

# Private-Subnetを2つ作成(1a/1c)
  CfnPrivateSubnet1a:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Ref PrivateSubnetACIDR
      MapPublicIpOnLaunch: false
      VpcId: !Ref CfnVPC
      AvailabilityZone: !Sub "${AvailabilityZoneName}a"
      Tags:
        - Key: Name
          Value: !Sub "${PrivateSubnetName}a"

  CfnPrivateSubnet1c:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Ref PrivateSubnetCCIDR
      MapPublicIpOnLaunch: false
      VpcId: !Ref CfnVPC
      AvailabilityZone: !Sub "${AvailabilityZoneName}c"
      Tags:
        - Key: Name
          Value: !Sub "${PrivateSubnetName}c"
```

### InternetGateway
 VPCとInternetGatewayをAttachmentで関連付けを行う。

```yaml:Network-Layer.yaml
  CfnInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-CfnIGW"

  # 作成したIGWをVPCにAttachする。
  CfnAttachCfInternetGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId : !Ref CfnInternetGateway
      VpcId: !Ref CfnVPC
```


### Route Table
:::note 
今回2つのルートテーブルを作成し、関連するIGW、Subnet（Public,Private)に紐付けていく。
- RouteTable For Public （ InternetGateway接続用 ）
- RouteTable For Private （ VPCEndpoint接続用 ）
:::


```yaml:Network-Layer.yaml
  CfnRouteTableForPublic:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref CfnVPC
      Tags:
        - Key: Name
          Value: !Sub "${PublicSubnetName}a"

  CfnRouteTableForPrivate:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref CfnVPC
      Tags:
        - Key: Name
          Value: !Sub "${PrivateSubnetName}a"

# Route先にIGWを定義
  CfnPublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref CfnRouteTableForPublic
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref CfnInternetGateway

# Public-Subnet1a/1cに関連付け
  CfnAssocciateRouteTableForPublicSubnet1a:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref CfnRouteTableForPublicSubnet
      SubnetId: !Ref CfnPublicSubnet1a

  CfnAssocciateRouteTableForPublicSubnet1c:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref CfnRouteTableForPublicSubnet
      SubnetId: !Ref CfnPublicSubnet1c

# Private-Subnet1a/1cに関連付け
  CfnAssocciateRouteTableForPrivateSubnet1a:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref CfnRouteTableForPrivate
      SubnetId: !Ref CfnPrivateSubnet1a

  CfnAssocciateRouteTableForPrivateSubnet1c:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref CfnRouteTableForPrivate
      SubnetId: !Ref CfnPrivateSubnet1c
```


### VPC Endpoint
VPCEndpointを経由してVPC外に設置するS3と接続できるように事前に設定しておく。
```yaml:Network-Layer.yaml.
  VPCEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      RouteTableIds:
        - !Ref CfnRouteTableForPrivate
      ServiceName: com.amazonaws.ap-northeast-1.s3
      VpcEndpointType: Gateway
      VpcId: !Ref CfnVPC
```

### Output
:::note 
クロススタック参照を行うため、後ほど構築するSeculity/Applicationの各Layerで値を引用できるようにOutputsでExport値を定義しておく。また同じテンプレートを使用し異なるスタックを使用することを想定しExportのNameにはSub関数で作成するスタックの名前が反映する仕様とした。
:::


```yaml:Network-Layer.yaml.
Outputs:

  StackVPC:
    Description: VPC's ID
    Value: !Ref CfnVPC
    Export:
      Name: !Sub "${AWS::StackName}-VPCID"

  StackPublicSubnet1a:
    Description: Public Subnet1a's ID
    Value: !Ref CfnPublicSubnet1a
    Export:
      Name: !Sub "${AWS::StackName}-PublicSubnet1a"
  
  StackPublicSubnet1c:
    Description: Public Subnet1c's ID
    Value: !Ref CfnPublicSubnet1c
    Export:
      Name: !Sub "${AWS::StackName}-PublicSubnet1c"

  StackPrivateSubnet1a:
    Description: Private Subnet1a's ID
    Value: !Ref CfnPrivateSubnet1a
    Export:
      Name: !Sub "${AWS::StackName}-PrivateSubnet1a"

  StackPrivateSubnet1c:
    Description: Private Subnet1c's ID
    Value: !Ref CfnPrivateSubnet1c
    Export:
      Name: !Sub "${AWS::StackName}-PrivateSubnet1c"
```

## Security-Layerに続く
続きはこちらから。
[AWS CloudFormationのテンプレートをLayer別に作成してクロススタック参照を作ってみた。
その2 [ Security-Layer編 ]](https://qiita.com/ezakiglico0215/items/272cf7bae52775e6a9f9)



## エラー事例
テンプレートを構築していく中で、多くのエラーに遭遇。
備忘録を兼ねて事象と解決方法をメモとして残しておく。

:::note alert
テンプレート作成を行う段階で遭遇したエラーと解決方法[主にcfn-lintで事前に検知]

[事象1] **cfn-lint  E1001: Top level template section VPCEndpoint is not valid**
[解決法] 作成したコードの深さの設定ミス：論理IDなどを設定するTOPの位置の深さに設定ミスがあり。

[事象2] **cfn-lint  E0000: Duplicate resource found "ParameterLabels"**
[解決法] コード内容が重複していると検出されるエラー。1つ削除して解消させる。

[事象3] **cfn-lint  E2015: Default should be allowed by AllowedPattern**
[解決法] インプットパラメーターで設定している値の表記ミス、正しく表記できていない。

[事象4] **cfn-lint  E1012: Ref CfnEC2 not found as a resource or parameter**
[解決法] インプットパラメーターで設定している値の表記ミス、正しく表記できていない。

[事象5] **Unresolved resource dependencies [ALBSecurityGroup] in the Outputs block of the template**
[解決法] !Ref関数で指定している引用元の論ID名に相違がある。

[事象6] **No export named sstack-PrivateSubnet1a found. Rollback requested by user**
[解決法] クロススタックで参照している値のtypoミス

[事象7] **Bucket name should not contain uppercase characters**
[解決法] Paraetersに記載しているS3のバケット名に大文字を使用していたため（大文字禁止）



:::