CloudFormationテンプレートのベストプラクティスな書き方について考えてみた

# ご挨拶
BeeX Advent Calendar 2022はじまるよおおおおお(/・ω・)/
どうもトップバッターのワンワンです🐶🐶🐶
[BeeX Advent Calendar 2022](https://qiita.com/advent-calendar/2022/beex)


# はじめに
普段、仕事柄CloudFormationを使用することが多くあるが、CloudFormationテンプレートのベストプラクティスな書き方って何だろうなと思うことが良くあるので、自分なりに思いついたベストプラクティスな書き方についてこの機会に色々と書いてみようと思います。

# 本日作成する環境

本日構築する環境としては、VPCを作って、EC2インスタンスを作成するというシンプルな構成です。詳細な構成図は下記となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252895/6655533c-f570-90aa-1e7c-743160cfa2b0.png)



また、上記とは別で、EC2インスタンスでSystemsManagerを活用したプライベートな接続を実施する為に、下記4つのVPCエンドポイントも作成します。

|エンドポイントサービス名  |エンドポイントタイプ  |
|---|---|
|com.amazonaws.[region].ssm  |インターフェイス型  |
|com.amazonaws.[region].ec2messages  |インターフェイス型  |
|com.amazonaws.[region].ssmmessages  |インターフェイス型  |
|com.amazonaws.[region].s3  |ゲートウェイ型  |

■参考URL
[プライベートサブネットに配置したEC2にAWS Systems Manager Session Managerを使ってアクセスする](https://dev.classmethod.jp/articles/ssm-session-manager-private/)
[Systems Manager を使用してインターネットアクセスなしでプライベート EC2 インスタンスを管理できるように、VPC エンドポイントを作成するにはどうすればよいですか?](https://aws.amazon.com/jp/premiumsupport/knowledge-center/ec2-systems-manager-vpc-endpoints/)

# 本日使用するCloudFormationテンプレート

**(1)RootStack.yml**

```php:RootStack.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: "RootStack"

Parameters:
  ProjectName:
    Type: String
    Default: qiita-dev
  TemplateS3BucketName:
    Type: String
    Default: ""
  VPCFileName:
    Type: String
    Default: "VPC.yml"
  EC2FileName:
    Type: String
    Default: "EC2.yml"
  KeyPairFileName:
    Type: String
    Default: "KeyPair.yml"

Resources:
  VPC: 
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub "https://${TemplateS3BucketName}.s3.${AWS::Region}.amazonaws.com/${VPCFileName}"
      Parameters:
        ProjectName: !Ref ProjectName

  KeyPair:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub "https://${TemplateS3BucketName}.s3.${AWS::Region}.amazonaws.com/${KeyPairFileName}"
      Parameters:
        ProjectName: !Ref ProjectName

  EC2:
    Type: AWS::CloudFormation::Stack
    DependsOn:
      - VPC
      - KeyPair
    Properties:
      TemplateURL: !Sub "https://${TemplateS3BucketName}.s3.${AWS::Region}.amazonaws.com/${EC2FileName}"
      Parameters:
        ProjectName: !Ref ProjectName
        VpcId: !GetAtt VPC.Outputs.vpcId
        PrivateSubnetAId: !GetAtt VPC.Outputs.PrivateSubnetAId
        PrivateSubnetCId: !GetAtt VPC.Outputs.PrivateSubnetCId
        PublicSubnetAId: !GetAtt VPC.Outputs.PublicSubnetAId
        PublicSubnetCId: !GetAtt VPC.Outputs.PublicSubnetCId
        BastionSecurityGroup: !GetAtt VPC.Outputs.BastionSecurityGroup
        LinuxEC2SecurityGroup: !GetAtt VPC.Outputs.LinuxEC2SecurityGroup
        KeyPair: !GetAtt KeyPair.Outputs.KeyName

```

**(2)VPC.yml**

```php:VPC.yml
AWSTemplateFormatVersion: "2010-09-09"
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Environment Setting
        Parameters:
          - ProjectName
      - Label:
          default: Network Configuration
        Parameters:
          - VPCCIDR
          - PrivateSubnetACIDR
          - PrivateSubnetCCIDR
          - PublicSubnetACIDR
          - PublicSubnetCCIDR

Parameters:
  ProjectName:
    Type: String
  VPCCIDR:
    Type: String
    Default: 10.192.0.0/16
  PrivateSubnetACIDR:
    Type: String
    Default: 10.192.0.0/24
  PrivateSubnetCCIDR:
    Type: String
    Default: 10.192.1.0/24
  PublicSubnetACIDR:
    Type: String
    Default: 10.192.10.0/24
  PublicSubnetCCIDR:
    Type: String
    Default: 10.192.11.0/24

Resources:
# ------------------------------------------------------------#
# Create VPC
# ------------------------------------------------------------# 
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-VPC
# ------------------------------------------------------------#
# Create InternetGateway
# ------------------------------------------------------------# 

  InternetGateway: 
    Type: "AWS::EC2::InternetGateway"
    Properties: 
      Tags: 
        - Key: Name
          Value: !Sub "${ProjectName}-igw"

  InternetGatewayAttachment: 
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties: 
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC 
# ------------------------------------------------------------#
# Create Subnet
# ------------------------------------------------------------# 
  PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select 
        - 0
        - Fn::GetAZs: ""
      CidrBlock: !Ref PrivateSubnetACIDR
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-PrivateSubnetA

  PrivateSubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select 
        - 1
        - Fn::GetAZs: ""
      CidrBlock: !Ref PrivateSubnetCCIDR
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-PrivateSubnetC

  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select 
        - 0
        - Fn::GetAZs: ""
      CidrBlock: !Ref PublicSubnetACIDR
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-PublicSubnetA

  PublicSubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select 
        - 1
        - Fn::GetAZs: ""
      CidrBlock: !Ref PublicSubnetCCIDR
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-PublicSubnetC

# ------------------------------------------------------------#
# Create Route Table
# ------------------------------------------------------------# 
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-PrivateRouteTable

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-PublicRouteTable

# ------------------------------------------------------------#
#  CREATE NAT Gateway AZ:A
# ------------------------------------------------------------#
  NATGatewayA: 
    Type: "AWS::EC2::NatGateway"
    Properties: 
      AllocationId: !GetAtt NATGatewayAEIP.AllocationId 
      SubnetId: !Ref PublicSubnetA
      Tags: 
        - Key: Name
          Value: !Sub "${ProjectName}-natgw-a"
  NATGatewayAEIP: 
    Type: "AWS::EC2::EIP"
    Properties: 
      Domain: VPC

# ------------------------------------------------------------#
# Route table settings
# ------------------------------------------------------------# 
  PublicRoute: 
    Type: "AWS::EC2::Route"
    Properties: 
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway 

  PrivateRoute: 
    Type: "AWS::EC2::Route"
    Properties: 
      DestinationCidrBlock: "0.0.0.0/0"
      RouteTableId: !Ref PrivateRouteTable
      NatGatewayId: !Ref NATGatewayA

# ------------------------------------------------------------#
# Associate Routetable with Subnet
# ------------------------------------------------------------# 

  PrivateSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnetA
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnetCRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnetC
      RouteTableId: !Ref PrivateRouteTable

  PublicSubnetRouteATableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetA
      RouteTableId: !Ref PublicRouteTable

  PublicSubnetRouteCTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetC
      RouteTableId: !Ref PublicRouteTable
# ------------------------------------------------------------#
# Create Security Group
# ------------------------------------------------------------# 
  EndpointSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub ${ProjectName}-EndpointSecurityGroup
      GroupDescription: EndpointSecurityGroup
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-EndpointSecurityGroup
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Ref VPCCIDR
      SecurityGroupEgress:
        - IpProtocol: '-1'
          CidrIp: 0.0.0.0/0

  BastionSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub ${ProjectName}-BastionSecurityGroup
      GroupDescription: WindowsEC2SecurityGroup
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-BastionSecurityGroup
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 389
          ToPort: 389
          CidrIp: !Ref VPCCIDR
        - IpProtocol: tcp
          FromPort: 3389
          ToPort: 3389
          CidrIp: !Ref VPCCIDR
      SecurityGroupEgress:
        - IpProtocol: '-1'
          CidrIp: 0.0.0.0/0

  LinuxEC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub ${ProjectName}-LinuxEC2SecurityGroup
      GroupDescription: LinuxEC2SecurityGroup
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-LinuxEC2SecurityGroup
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref VPCCIDR
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref VPCCIDR
        - IpProtocol: tcp
          FromPort: 3000
          ToPort: 3000
          CidrIp: !Ref VPCCIDR
      SecurityGroupEgress:
        - IpProtocol: '-1'
          CidrIp: 0.0.0.0/0
# ------------------------------------------------------------#
# Create VPCEndpoint
# ------------------------------------------------------------# 
  EndpointSSM:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref EndpointSecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm
      SubnetIds:
        - !Ref PrivateSubnetA
#        - !Ref PrivateSubnetC
      VpcEndpointType: Interface
      VpcId: !Ref VPC


  EndpointSSMMessages:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref EndpointSecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages
      SubnetIds:
        - !Ref PrivateSubnetA
#        - !Ref PrivateSubnetC
      VpcEndpointType: Interface
      VpcId: !Ref VPC


  EndpointEC2Messages:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref EndpointSecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ec2messages
      SubnetIds:
        - !Ref PrivateSubnetA
#        - !Ref PrivateSubnetC
      VpcEndpointType: Interface
      VpcId: !Ref VPC


  EndpointS3:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      RouteTableIds:
        - !Ref PrivateRouteTable
      ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
      VpcEndpointType: Gateway
      VpcId: !Ref VPC

# ------------------------------------------------------------#
# Output
# ------------------------------------------------------------# 
Outputs:
  vpcId:
    Value: !Ref VPC
  VPCCIDR:
    Value: !Ref VPCCIDR

  PrivateSubnetAId:
    Value: !Ref PrivateSubnetA
  PrivateSubnetCId:
    Value: !Ref PrivateSubnetC
  PublicSubnetAId:
    Value: !Ref PublicSubnetA
  PublicSubnetCId:
    Value: !Ref PublicSubnetC

  PrivateSubnetACIDR:
    Value: !Ref PrivateSubnetACIDR
  PrivateSubnetCCIDR:
    Value: !Ref PrivateSubnetCCIDR
  PublicSubnetACIDR:
    Value: !Ref PublicSubnetACIDR
  PublicSubnetCCIDR:
    Value: !Ref PublicSubnetCCIDR

  PrivateRouteTable:
    Value: !Ref PrivateRouteTable
  PublicRouteTable:
    Value: !Ref PublicRouteTable

  EndpointSSM:
    Value: !Ref EndpointSSM
  EndpointSSMMessages:
    Value: !Ref EndpointSSMMessages
  EndpointEC2Messages:
    Value: !Ref EndpointEC2Messages
  EndpointS3:
    Value: !Ref EndpointS3

  EndpointSecurityGroup:
    Value: !Ref EndpointSecurityGroup
  BastionSecurityGroup:
    Value: !Ref BastionSecurityGroup
  LinuxEC2SecurityGroup:
    Value: !Ref LinuxEC2SecurityGroup
```

**(3)KeyPair.yml**

```php:KeyPair.yml
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  ProjectName:
    Type: String

# ------------------------------------------------------------#
# Create KeyPair
# ------------------------------------------------------------# 
Resources:
  KeyPair1:
    Type: AWS::EC2::KeyPair
    Properties:
      KeyName:  !Sub ${ProjectName}-KeyPair

Outputs:
  KeyName:
    Value: !Ref KeyPair1
```

**(4)EC2.yml**

下記2台の構築をおこないます。
・WindowsServer2022の踏み台サーバ
・Redhat8の業務用システム

```php:EC2.yml
AWSTemplateFormatVersion: "2010-09-09"
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Environment Setting
        Parameters:
          - ProjectName
      - Label:
          default: Network Configuration
        Parameters:
          - VpcId
          - PrivateSubnetAId
          - PrivateSubnetCId
          - PublicSubnetAId
          - PublicSubnetCId
      - Label:
          default: EC2 Configuration
        Parameters:
          - Redhat8Ami
          - LinuxEc2InstanceType
          - LinuxEC2SecurityGroup
          - Windows2022Ami
          - BastionEc2InstanceType
          - BastionSecurityGroup
          - KeyPair

Parameters:
  ProjectName:
    Type: String
  VpcId:
    Type: AWS::EC2::VPC::Id
  PrivateSubnetAId:
    Type: String
  PrivateSubnetCId:
    Type: String
  PublicSubnetAId:
    Type: String
  PublicSubnetCId:
    Type: String
  Redhat8Ami:
    Type : String
    Default: ami-0f903fb156f24adbf
  LinuxEc2InstanceType:
    Type: String
    Default: t3.small
  LinuxEC2SecurityGroup:
    Type: String
  Windows2022Ami:
    Type : AWS::SSM::Parameter::Value<String>
    Default: /aws/service/ami-windows-latest/Windows_Server-2022-Japanese-Full-Base
  BastionEc2InstanceType:
    Type: String
    Default: t3.small
  BastionSecurityGroup:
    Type: String
  KeyPair:
    Type: String


Resources:
# ------------------------------------------------------------#
# EC2 settings
# ------------------------------------------------------------# 
  EC2IAMRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${ProjectName}-SSM-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
        - arn:aws:iam::aws:policy/AmazonS3FullAccess

  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles:
        - Ref: EC2IAMRole
      InstanceProfileName: !Sub ${ProjectName}-EC2InstanceProfile

# ------------------------------------------------------------#
# Create EC2 Instance
# ------------------------------------------------------------# 
  EC2Instance1:
    Type: AWS::EC2::Instance
    Properties:
      NetworkInterfaces:
        - SubnetId: !Ref PublicSubnetAId
          GroupSet:
            - !Ref BastionSecurityGroup
          AssociatePublicIpAddress: true
          DeviceIndex : 0
      InstanceType: !Ref BastionEc2InstanceType
      ImageId: !Ref Windows2022Ami
      IamInstanceProfile: !Ref EC2InstanceProfile
      BlockDeviceMappings: 
      - DeviceName: '/dev/sda1'
        Ebs: 
          VolumeSize: 50
          VolumeType: gp3
          Encrypted: true
      KeyName: !Ref KeyPair
      Tags:
      - Key: Name
        Value: !Sub ${ProjectName}-Bastion
      UserData:
        Fn::Base64: !Sub |
          <powershell>
          # SSM Agent
          $dir = $env:TEMP + "\ssm"
          New-Item -ItemType directory -Path $dir -Force
          cd $dir
          (New-Object System.Net.WebClient).DownloadFile("https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/windows_amd64/AmazonSSMAgentSetup.exe", $dir + "\AmazonSSMAgentSetup.exe")
          Start-Process .\AmazonSSMAgentSetup.exe -ArgumentList @("/q", "/log", "install.log") -Wait

          # set timezone
          Set-TimeZone -Id "Tokyo Standard Time"
          
          # Windows Firewall
          Set-NetFirewallProfile -Enabled false
          
          # Add Windows Firewall Inboundrule
          New-NetFirewallRule -DisplayName "ALL TCP V4" -Direction Inbound -Protocol TCP -LocalPort 0-65535 -Action Allow -Enabled true
          New-NetFirewallRule -DisplayName "ALL UDP V4" -Direction Inbound -Protocol UDP -LocalPort 0-65535 -Action Allow -Enabled true

          # hostname
          Rename-Computer -NewName ${ProjectName}-Bastion -Force -Restart
          </powershell>

  EC2Instance2:
    Type: AWS::EC2::Instance
    DependsOn:
      - EC2InstanceProfile
    Properties:
      SubnetId: !Ref PrivateSubnetAId
      InstanceType: !Ref LinuxEc2InstanceType
      ImageId: !Ref Redhat8Ami
      SecurityGroupIds:
        - !Ref LinuxEC2SecurityGroup
      IamInstanceProfile: !Ref EC2InstanceProfile
      BlockDeviceMappings:
        - DeviceName: '/dev/sda1'
          Ebs:
            VolumeSize: 50
            VolumeType: gp3
            Encrypted: true
      KeyName: !Ref KeyPair
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-Zabbix
      UserData:
        Fn::Base64: |
          #!/bin/bash
          REGION_NAME=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone | sed -e 's/.$//')
          dnf install -y "https://s3.${REGION_NAME}.amazonaws.com/amazon-ssm-${REGION_NAME}/latest/linux_amd64/amazon-ssm-agent.rpm"
          systemctl enable amazon-ssm-agent
          systemctl start amazon-ssm-agent

          # Register the Microsoft RedHat repository
          sudo curl https://packages.microsoft.com/config/rhel/8/prod.repo | sudo tee /etc/yum.repos.d/microsoft.repo

          # Install PowerShell
          sudo dnf install -y powershell
```

# 解説

以降は、上記のCloudFormationテンプレートでどの点を工夫しているのかについて**にょろにょろ🐍**と書いていきます。

## (1)RootStack.yml 解説

### (1.1)ネストされたスタックを用いた展開

AWSでは、ネストされたスタックという手法が存在します。複数のリソースを1つのCloudFormationで展開する際は、どうしてもコード量が多くなりがちがちで管理がしづらくなってしまいます。それを改善する手段の一つとして、リソース毎にテンプレートを分けて、親スタックから該当リソースを呼び出す手法があげられます。
そうすることで、1ファイルあたりのコード量が少なくなり、管理がしやすくなります。この手法を活用する際は、親スタック(今回はRootStack.yml)と子スタックに分け、展開していく感じです。今回はその手法を使うのですが、イメージ図は下記となります。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252895/b3075949-c6de-1914-49a6-28aa9202ef1b.png)



参考URL：[ネストされたスタックの操作](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/using-cfn-nested-stacks.html)

### (1.2)可変的な値は変数を利用する

「Parameters」セクションで変数を活用することで、**何度も使用する値**や**可変的な値**を変数に格納し、誤字の防止や汎用性を高めている。

```
Parameters:
  ProjectName:
    Type: String
    Default: qiita-dev
  TemplateS3BucketName:
    Type: String
    Default: ""
  VPCFileName:
    Type: String
    Default: "VPC.yml"
  EC2FileName:
    Type: String
    Default: "EC2.yml"
  KeyPairFileName:
    Type: String
    Default: "KeyPair.yml"
```

### (1.3)子スタックの呼び出し
「Resources」セクションで、各子スタックを呼び出す展開にしている。また、子スタックを呼び出す際に、親スタックで定義した「ProjectName」という変数(システム名)を代入している。

```
Resources:
  VPC: 
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub "https://${TemplateS3BucketName}.s3.${AWS::Region}.amazonaws.com/${VPCFileName}"
      Parameters:
        ProjectName: !Ref ProjectName
```

## (2)VPC.yml 解説

### (2.1)サブネット作成時のAZ指定に関して

サブネット作成時にどこのAZに所属するかを指定するのだが、その時にAZ名を直接指定するのではなく、AWS側で用意されている組み込み関数の「Fn::GetAZs」を利用することで、指定したリージョン内のアベイラビリティーゾーンのリストを自動的に割り振ることができる。このような記述をすることで、別リージョンで同じテンプレートを使用する際に修正箇所が少なくなる為、このような書き方が推奨されている。
また、リージョン内でどのAZを選択できるかについては下記コマンドを参照してください。
※Fn::GetAZs 関数は、指定されたリージョンのAZをアルファベット順に並べるArrayを返す

```
■Fn::GetAZsを用いた定義
      AvailabilityZone: !Select 
        - 0
        - Fn::GetAZs: ""
```

```
■AWS CLIで利用できるAZを確認(東京リージョンの場合)
[cloudshell-user@ip-10-0-24-90 ~]$ aws ec2 describe-availability-zones \
>   --region ap-northeast-1 \
>   --query 'AvailabilityZones[].{RegionName:RegionName, ZoneName:ZoneName, ZoneId:ZoneId}' \
>   --output table
----------------------------------------------------
|             DescribeAvailabilityZones            |
+----------------+-------------+-------------------+
|   RegionName   |   ZoneId    |     ZoneName      |
+----------------+-------------+-------------------+
|  ap-northeast-1|  apne1-az4  |  ap-northeast-1a  |
|  ap-northeast-1|  apne1-az1  |  ap-northeast-1c  |
|  ap-northeast-1|  apne1-az2  |  ap-northeast-1d  |
+----------------+-------------+-------------------+
```

■参考URL
[Fn::GetAZs](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getavailabilityzones.html)
[Cloudformationのテンプレートに使える関数をまとめてみました ( 1 )](https://dev.classmethod.jp/articles/lim-cloudformation-function-1/)
[AWS CLIで自アカウントのAZ名とAZ IDのマッピングを確認する](https://dev.classmethod.jp/articles/confirm-your-az-name-mapping-using-cli/)


### (2.2)VPC.yml内でシステムで使用するSGを全て作成

私は、SGを作成する際は、一番初めに作成するVPCと合わせて作成するようにしてます。理由は、別のテンプレートそれぞれでSGを作成した場合、どこで作成したSGかが分かりづらくなってしますのでそうしてます。まあ、ここの部分に関しては好みの問題です。


### (2.3)インタフェース型のエンドポイントを作成する際は、片AZのみに作成する(検証用の場合のみ)

インターフェイス型のエンドポイントは、立てているだけで1時間当たり0.014USDの費用が発生します。(東京リージョンの料金)
なので、検証用で使用する時などでは、下記のように2個目のサブネットはコメントアウトし、片AZのみで使用することで節約してます。(これはただの貧乏性なだけです(笑))

```
  EndpointSSM:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref EndpointSecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm
      SubnetIds:
        - !Ref PrivateSubnetA
#        - !Ref PrivateSubnetC
      VpcEndpointType: Interface
      VpcId: !Ref VPC
```

## (3)KeyPair.yml 解説

### (3.1)CloudFormationでキーペアを作成する

2022年4月29日にCloud Formation テンプレートを使用してキーペアを作成することができるようになりました。キーペアを作成後は、AWS Systems Manager の パラメータストアにキー情報が保管されるので、そこからキー情報をコピーし、サーバーへのログインが行えるようになりました。なので、キーペアの紛失などは無くなるのかなという感じです。実際にコピペするだけで、結構ラクなので、私はこの手法を採用してます。


参考URL：
[AWS は EC2 キーペア用に新しい管理機能を追加](https://aws.amazon.com/jp/about-aws/whats-new/2022/04/aws-management-features-ec2-key-pairs/)
[AWS::EC2::KeyPair](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-keypair.html)


## (4)EC2.yml 解説

### (4.1)Windows内のユーザデータに関して

**Windowsサーバーの作成時に以下の内容をユーザデータとして指定してます。**

- SSM Agentインストール
- タイムゾーンを「asia/tokyo」に変更
- WindowsFirewallを無効化
- WindowsFirewallのインバウンドルールでTCP/UDPを全許可(念のため)

### (4.2)Linux内のユーザデータに関して

**Linuxサーバーの作成時に以下の内容をユーザデータとして指定してます。**

- SSM Agentインストール
- Microsoft RedHat repositoryをインストールし、powershellをインストール　※理由については、以降の解説を参照。

■解説
SSM Agentが古い場合、System Manageを使用したログインが急にできるなくなることがあります。
その為、SSM Agentは定期的にバージョンアップすることが推奨されています。
※以下、画面参照。

![SSM Agent接続エラー.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252895/9ae3b070-fe4d-8d7f-45f4-3b1d31d0c297.png)

SSM Agentのアップデートは、RunCommandの「AWS-UpdateSSMAgent」を実行し、アップデートすることができます。
しかし、デフォルトのままのRedHat8のインスタンスに、「AWS-UpdateSSMAgent」を実行すると、下記のエラーが発生します。
>failed to run commands: fork/exec /usr/bin/pwsh: no such file or directory

上記エラーを解消する手段として、事前にpowershellをインストールする必要があります。
その為、CloudFormationのユーザデータ内に、powershellをインストールしてます。
詳細については、下記記事を参考にしてください。
[LinuxインスタンスのSSM Run CommandでPowerShellスクリプトを実行する](https://dev.classmethod.jp/articles/execute-aws-runpowershellscript-on-linux-pwsh/)
[AWS Systems Manager エージェント（SSM Agent）の現行のバージョンを確認して最新バージョンにアップデートする](https://dev.classmethod.jp/articles/aws-systems-manager-agent-update/)

# 最後に

私がCloudFormationテンプレートを書く際は、管理のしやすさや使いまわしの良さを意識し、作成するようにしてます。私自身まだまだベストプラクティスな書き方なんてできていないと思いますが、日々勉強しながら良くしていこうと思ってます。この記事を読んでここを改善した方が良いなどの意見などあれば、色々とコメントいただければ嬉しいです。最後までお読みいただき、ありがとうございました。ペコm(_ _)m