CloudFormationで汎用的な構成を作る

この記事では、CloudFormationで様々な環境構築に転用できそうな、ベースとなる「基本のだし」のような構成を作成します。

仕事柄、様々な製品のデモを行うことが多いのですが、AWS学習の狙いとしてはCloudFormationテンプレートのストックを溜めておき、デモのシナリオや製品の動作要件に応じて、ささっと環境を作れるようになるのが当面の目標であり、願望です(^^
※構成やテンプレートの書き方等、至らない点も多々あると思いますがご容赦ください・・・・

##構成図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/668791/3ed7c22e-5e20-e478-3bfd-b3a14b803aa0.png)


##通信要件
- AWS東京リージョン(ap-northeast-1)固定
- Public SubnetとPrivate Subnetを1つずつ作成
- それぞれのSubnetにEC2インスタンスを1つずつ作成
- AMIはAmazon Linux 2、インスタンスタイプはt2.micro固定
- Private SubnetのEC2インスタンスは、Public Subnetに設置したNAT Gatewayを介してインターネット接続
- Public Subnetに設置されたEC2インスタンスには、Elastic IPを設定する
- Security Groupは、SSHインバウンド通信のみ許可する

##CloudFormationテンプレート
今回は以下の4つのStackで環境構築を行いました。
備忘録のため、テンプレートにやや多めにコメントを入れています。
- stack_Network.yml
- stack_NATgateway.yml
- stack_RouteTable.yml
- stack_Instance.yml

######テンプレートを分けた理由
ルーティングは、今後汎用的に作るであろう構成によって柔軟に変更できるようにしたいのと、NAT GatewayとElastic IPおよびEC2インスタンスは、Runしたままだと課金が絡んでくるため、不要時はStackごと削除できるようにテンプレートを分離しました。

```stack_Network.yml
AWSTemplateFormatVersion: 2010-09-09
Resources: 
  # Create a VPC
  myVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/21
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: myVPC

  # Create an Internet Gateway
  internetGW:
      Type: AWS::EC2::InternetGateway
      Properties:
        Tags:
          - Key: Name
            Value: internetGW

  # Attach the internet gateway to the VPC
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref myVPC
      InternetGatewayId: !Ref internetGW  

  # Create a Public Subnet
  publicsubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: "ap-northeast-1a"
      VpcId: !Ref myVPC
      CidrBlock: 10.0.0.0/24
      Tags:
        - Key: Name
          Value: PublicSubnetFromCF
 
  # Create a Private Subnet
  privatesubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: "ap-northeast-1a"
      VpcId: !Ref myVPC
      CidrBlock: 10.0.1.0/24
      Tags:
        - Key: Name
          Value: PrivateSubnetFromCF

  # Create a Security Group allowing Inbound SSH only
  SecGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: mySGdesc
      GroupName: mySGName
      VpcId: !Ref myVPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: mySG
    
# List Outputs for cross reference
Outputs:
    VPC:
      Value: !Ref myVPC
      Export:
        Name: VPCId
    inetGW:
      Value: !Ref internetGW
      Export:
        Name: internetGWName
    Subnet1:
      Value: !Ref publicsubnet
      Export: 
        Name: Subnet1Name
    Subnet2:
      Value: !Ref privatesubnet
      Export: 
        Name: Subnet2Name        
    SG1:
      Value: !GetAtt SecGroup.GroupId
      Export:
        Name: SG1Name
```

ここまでで
- VPC
- Internet Gateway
- Subnet
- Security Group
の作成完了です。

次に、NAT Gatewayの設定を行います。

```stack_NATgateway.yml
AWSTemplateFormatVersion: 2010-09-09
Resources: 
  # Create a NAT Gateway in Public Subnet
  NATGW:
    Type: AWS::EC2::NatGateway
    Properties: 
      AllocationId: !GetAtt EIpNATGW.AllocationId # Associate EIP with NATGW
      SubnetId: !ImportValue Subnet1Name # Associate NATGW with the Public Subnet
      Tags:
        - Key: Name
          Value: NATGW
  EIpNATGW:
    Type: AWS::EC2::EIP # Allocate an Elastic IP with NAT Gateway
    Properties:
      Domain: vpc
  
Outputs:
    NatGW:
      Value: !Ref NATGW
      Export:
        Name: NATGWId
```

ここまでで
- NAT Gatewayの作成
が完了しました。

次は、
- Public SubnetがInternet Gatewayを経由してインターネット接続を行うためのルートテーブル作成およびルートの設定
- Private SubnetがNAT Gatewayを経由してインターネット接続を行うためのルートテーブル作成およびルートの設定
を行います。

```stack_RouteTable.yml
AWSTemplateFormatVersion: 2010-09-09
Resources: 
  # Create a Route Table for Public Subnet
  RouteTablePublic:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !ImportValue VPCId
      Tags:
        - Key: Name
          Value: RouteTablePublic

 # Attach the Route Table to Public Subnet
  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref RouteTablePublic
      SubnetId: !ImportValue  Subnet1Name

# Create a Route from Public Subnet to Internet
  routeINET:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref RouteTablePublic
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !ImportValue internetGWName

  # Create a Route Table for Private Subnet
  RouteTablePrivate:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !ImportValue VPCId
      Tags:
        - Key: Name
          Value: RouteTablePrivate

 # Attach the Route Table to Private Subnet
  PrivateSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref RouteTablePrivate
      SubnetId: !ImportValue  Subnet2Name

# Create a Route from Private Subnet to NATGateway
  routeNATGW:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref RouteTablePrivate # Associate NATGW with RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !ImportValue NATGWId # Property Name must be NatGatewayID when NAT Gateway is the next hop.

Outputs:
    RouteTableName:
      Value: !Ref RouteTablePrivate
      Export:
        Name: RouteTablePrivate # NATGW needs to refer this RouteTable Name
```

ここまでで、
- Route Tableの作成およびルートの設定
- が完了しました。


最後に、Public SubnetとPrivate SubnetのそれぞれでEC2インスタンスを起動します。

```stack_Instance.yml
AWSTemplateFormatVersion: 2010-09-09
Resources: 
  # Create a Public EC2 instance 
  PublicEC2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: mykey01 # Set keypair
      ImageId: ami-09d28faae2e9e7138 # Set Amazon Linux 2 free AMI
      NetworkInterfaces: 
        - AssociatePublicIpAddress: true # Attach IPv4 Public Address
          DeviceIndex: 0
          GroupSet: [!ImportValue SG1Name] # Refer external SG, need to set as List
          SubnetId: !ImportValue Subnet1Name # Refer external Subnet
      InstanceType: t2.micro
      Monitoring: false
      Tags:
        - Key: Name
          Value: PublicEC2fromCF
  EIPPublic:
    Type: AWS::EC2::EIP #Allocate an EIP with Public EC2
    Properties:
      Domain: vpc
      InstanceId: !Ref PublicEC2
 
  # Create a Private EC2 instance 
  PrivateEC2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: mykey01 # Set keypair
      ImageId: ami-09d28faae2e9e7138 # Set Amazon Linux 2 free AMI
      NetworkInterfaces: 
        - AssociatePublicIpAddress: true
          DeviceIndex: 0
          GroupSet: [!ImportValue SG1Name] # Refer external SG, need to set as List
          SubnetId: !ImportValue Subnet2Name # Refer external Subnet
      InstanceType: t2.micro
      Monitoring: false
      # SecurityGroupIds:
      Tags:
        - Key: Name
          Value: PrivateEC2fromCF
```

#ハマった点
AWS Management Consoleでの操作はわかっても、CloudFormationでの記法がわからない点が多々あり、多くの時間を費やしました。
- EC2インスタンスにPublicIPv4を割り当てる設定の記法がわからず、特にGroupSetにSecurity Groupを設定する際はリストの形で渡さなくてはならない点
- Elastic IPをEC2インスタンスやNAT Gatewayに割り当てる際、Type::EC2::EIPをどのインデントに書けばいいのかがわからなかった点
- Public SubnetからInternetへのルーティングを記載するRoute Tableは、CloudFormationでは、VPC DefaultのRoute Tableを参照することはできない(別のRoute Tableを作らないといけない) ＜ これが最もハマった（[参考URL](https://serverfault.com/questions/588904/aws-vpc-default-route-table-in-cloudformation))
- NAT GatewayをNext Hopにするルートを記載する際、Property NameはGatewayIdではなく、NatGatewayIdを使用しなくてはならない点

#感想
AWSもCloudFormationもほぼ初見で、あれこれ調べながらやりましたが、非常に勉強になりました。今後のAWS学習もぜひCloudFormationとセットで進めていきたいです。Qiita投稿も初めてだったので読みづらかったかもしれません。今後少しずつ改善したいと思います。

その他感想としては、
- LogicalIDやOutputなどの名前付けが難しい
- 当初長大なテンプレートを作ってしまい、Stack実行時のエラーのデバッグが大変になり、考えた結果テンプレートを４つに分けることにしたこと。
- テンプレートを修正し、エラーを確認し、ドキュメントを調べて修正し、また試す。という繰り返し作業は（大変ですが）面白かったこと

#最後に
この記事はAWS初学者を導く体系的な動画学習サービス
「AWS CloudTech」の課題カリキュラムで作成しました。
https://aws-cloud-tech.com