ヒューマンエラーの少ないCloudFormationテンプレートの作り方

こんばんは、大畑です。

私事ですが、AWSを学び始めて半年ほどが経ちました。

業務でAWSを使用したクラウド環境の構築を行う際に、CloudFormationというサービスをよく触ります。

CloudFormationはクラウド基盤をコード化する（IaC : Infrastructure as Code）ことで


・Git等を使用したインフラ基盤のバージョン管理ができる
・構築を自動化し、業務効率化できる
・開発環境のクラウド基盤を複製し、ステージング環境、本番環境に同等の構成が作れる

など、様々なメリットを受けることができます。

一方で、yamlやjsonでコードを記述する際や、CloudFormationをコンソールから使用する際にはどうしても人の手が加わります。
これはエラーの原因となりえるため、なるべく排除していきたいところです。


ということで今回は

「ヒューマンエラーの少ないCloudFormationテンプレートの作り方」

について書いていきたいと思います。


# 【初心者向け】CloudFormationとは


> AWS CloudFormation は、インフラストラクチャをコードとして扱うことで、AWS およびサードパーティーのリソースをモデル化、プロビジョニング、管理することができます。
>[引用] https://aws.amazon.com/jp/cloudformation/

BlackBelt資料

https://www.slideshare.net/AmazonWebServicesJapan/20200826-aws-black-belt-online-seminar-aws-cloudformation-238501102

簡単にまとめると

「AWSを使ったクラウド基盤をコードで構築できるサービス」

となります。

# 今回修正するCloudFormationテンプレート

今回修正するCloudFormationテンプレートは以下となります。

```templete.yml
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  InstanceType:
    Type: String
    Default: "t2.micro"
    Description: "Instance type for EC2 instance"
Resources:
  VPC:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: "10.0.0.0/16"
      Tags:
        - Key: "Name"
          Value: "MyVPC"
  Subnet:
    Type: "AWS::EC2::Subnet"
    Properties:
      VpcId: !Ref VPC
      CidrBlock: "10.0.1.0/24"
      AvailabilityZone: "ap-northeast-1a"
      Tags:
        - Key: "Name"
          Value: "MySubnet"
  InstanceSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: "Allow SSH access to EC2 instances"
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: "tcp"
          FromPort: 22
          ToPort: 22
          CidrIp: "0.0.0.0/0"
  EC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      InstanceType: "t2.micro"
      ImageId: "ami-06ee4e2261a4dc5c3"
      SecurityGroupIds:
        - !Ref InstanceSecurityGroup
      SubnetId: !Ref Subnet
      Tags:
        - Key: "Name"
          Value: "MyEC2Instance"
```
VPCとEC2を作成するシンプルなテンプレートとなっています。

ではこちらを修正していきます。

# １．パラメータを使用する
CloudFormationではParametersセクションを使うことで、スタック構築時にパラメータを入力することができます。

開発環境(dev)、ステージング環境(stg)、本番環境(pro)でリソース名を分けたい場合、リソースの識別子をパラメータとして定義しておくことで、テンプレートを分けることなく複数環境にデプロイが可能になります。
例えば、EC2の[Name]タグに環境識別子を入れたい場合、ハードコーディングで
CloudFormationテンプレートを書いていると、複数テンプレートを用意する、もしくはその都度環境識別子を修正しなければなりません。
これはテンプレート修正ミスや修正漏れが発生する原因となり得ます。

解決方法として、[Name]タグに入れる環境識別子をパラメータとして置いておくことで、単一のテンプレートで完結することができ、ヒューマンエラーの原因を排除することができます。

他にも、パラメータの[AllowedValues]タグを使用することで、スタック作成時に入力するパラメータをリストから選択させることができます。打ち間違い等がなくなるのでヒューマンエラー対策として有効です。

以下は、Parametersセクションを追加し、修正を加えたCloudFormationテンプレートです。

```templete.yml
AWSTemplateFormatVersion: '2010-09-09'

#Parametersでスタック作成時に入力するパラメータを入力する
Parameters:
  EnviromentType:
    Type: String
    Default: "DEV"
    AllowedValue: ["DEV","STG","PRO"]
    Description: "EnviromentType Allowed DEV,STG,PRO"

  AMI:
    Type: String
    Default: "ami-06ee4e2261a4dc5c3"
    AllowedValue: ["ami-06ee4e2261a4dc5c3","ami-01234567890abcdef"]
    Description: "AMI for EC2 instance"

Resources:
  VPC:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: "10.0.0.0/16"
      # ${EnviromentType}でパラメータで入力した値を参照する
      Tags:
        - Key: Name
          Value: ${EnviromentType}-VPC      
  Subnet:
    Type: "AWS::EC2::Subnet"
    Properties:
      VpcId: !Ref VPC
      CidrBlock: "10.0.1.0/24"
      AvailabilityZone: "ap-northeast-1a"
      Tags:
        - Key: Name
          Value: ${EnviromentType}-SUBNET01
  InstanceSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: "Allow SSH access to EC2 instances"
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: "tcp"
          FromPort: 22
          ToPort: 22
          CidrIp: "0.0.0.0/0"
  EC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      InstanceType: "t2.micro"
      # ${AMI}でパラメータに入力した値を参照する
      ImageId: ${AMI}
      SecurityGroupIds:
        - !Ref InstanceSecurityGroup
      SubnetId: !Ref Subnet
      Tags:
        - Key: Name
          Value: ${EnviromentType}-EC2Instance
```

# ２．マッピングファイルを使用する
CloudFormationではMappingsセクションでキーと値を保存することができます。
参照する際は、!FindInMapをResourceセクションで使用することで参照することができます。

これはリージョンを跨いでCloudFormationを実行する際に役立ちます。

例えば、東京リージョンとバージニア北部リージョンでEC2を立てたい場合を考えます。
１で作成したCloudFormationテンプレートの場合、AMIはリージョンで固有のものとなるため、それぞれのリージョンで別のAMIを指定する必要が出てきます。
Mappingファイルを使用するとリージョンに応じてパラメータの値を入力する必要がなくなります。

以下はMappingファイルの例です。

```mapping.yml
Mappings: 
  RegionMap: 
    us-east-1: 
      "HVM64": "ami-0ff8a91507f77f867"
    us-west-1: 
      "HVM64": "ami-0bdb828fd58c52235"
    eu-west-1: 
      "HVM64": "ami-047bb4163c506cd98"
    ap-southeast-1: 
      "HVM64": "ami-08569b978cc4dfa10"
    ap-northeast-1: 
      "HVM64": "ami-06ee4e2261a4dc5c3"
```


上記の例では東京リージョンでは「ami-06ee4e2261a4dc5c3」を、バージニア北部リージョンでは「ami-0ff8a91507f77f867」を使用するという記述がされています。
Resourceセクションでは以下のように!FindInMapを使用して、Mappingファイルを参照します。


```
!FindInMap [RegionMap,!Ref "AWS::Region", HVM64]
```

以下が１のテンプレートにさらに修正を加えたCloudFormationテンプレートです。

```templete.yml
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  EnviromentType:
    Type: String
    Default: "DEV"
    AllowedValues: ["DEV","STG","PRO"]
    Description: "EnviromentType Allowed DEV,STG,PRO"

#Mappingsセクションで値を指定する
Mappings: 
  RegionMap: 
    us-east-1: 
      "HVM64": "ami-0ff8a91507f77f867"
    us-west-1: 
      "HVM64": "ami-0bdb828fd58c52235"
    eu-west-1: 
      "HVM64": "ami-047bb4163c506cd98"
    ap-southeast-1: 
      "HVM64": "ami-08569b978cc4dfa10"
    ap-northeast-1: 
      "HVM64": "ami-06ee4e2261a4dc5c3"

Resources:
  VPC:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: "10.0.0.0/16"
      Tags:
        - Key: Name
          Value: !Sub ${EnviromentType}-VPC      
  Subnet:
    Type: "AWS::EC2::Subnet"
    Properties:
      VpcId: !Ref VPC
      CidrBlock: "10.0.1.0/24"
      AvailabilityZone: "ap-northeast-1a"
      Tags:
        - Key: Name
          Value: !Sub ${EnviromentType}-SUBNET01
  InstanceSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: "Allow SSH access to EC2 instances"
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: "tcp"
          FromPort: 22
          ToPort: 22
          CidrIp: "0.0.0.0/0"
  EC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      InstanceType: "t2.micro"
      # FindInMapを使用してMappingファイルを参照する
      ImageId: !FindInMap [RegionMap,!Ref "AWS::Region", HVM64]
      SecurityGroupIds:
        - !Ref InstanceSecurityGroup
      SubnetId: !Ref Subnet
      Tags:
        - Key: Name
          Value: !Sub ${EnviromentType}-EC2Instance
```




# ３．クロススタック参照を使用する

クロススタック参照とは、複数のスタック間でリソースを参照できるCloudFormationの機能です。

先ほどまでで使っていたテンプレートでは、VPCやサブネット、セキュリティグループといった「ネットワーク」の部分と、EC2の「コンピューティングリソース」で構成されていました。
実際はこの一つのテンプレートでも動きますが、今後別でスタックが増えた場合を考えると、ネットワークとコンピューティングリソースは分けたほうが無難でしょう。

一方で、別のスタックに記述されているリソースはRef関数で参照できなくなるという問題が発生します。もちろん参照できなくなるリソースのIDをパラメータとして入力させるのもいいですが、やはり人の手で入力するとヒューマンエラーが発生する可能性があります。

このような場合に使えるのが、クロススタック参照です。

クロススタック参照を使用するためには、OutPutsセクションでリソースをExportします。
以下は、ネットワークのみのスタックで分割した上で、セキュリティグループとサブネットをExportしたCloudFormationテンプレートです。

```NW.yml
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  EnviromentType:
    Type: String
    Default: "DEV"
    AllowedValues: ["DEV","STG","PRO"]
    Description: "EnviromentType Allowed DEV,STG,PRO"
Resources:
  VPC:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: "10.0.0.0/16"
      Tags:
        - Key: Name
          Value: !Sub ${EnviromentType}-VPC      
  Subnet:
    Type: "AWS::EC2::Subnet"
    Properties:
      VpcId: !Ref VPC
      CidrBlock: "10.0.1.0/24"
      AvailabilityZone: "ap-northeast-1a"
      Tags:
        - Key: Name
          Value: !Sub ${EnviromentType}-SUBNET01
  InstanceSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: "Allow SSH access to EC2 instances"
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: "tcp"
          FromPort: 22
          ToPort: 22
          CidrIp: "0.0.0.0/0"

#OutPutsセクションでリソースを別スタックから参照できるようにする
Outputs:
  Subnet:
    # Valueには参照させたいリソースを記述
    Value: !Ref Subnet
    # Exportにはどのようなキーで参照させるかを記述
    Export: 
      Name: !Sub ${AWS::StackName}-Subnet
  SecurityGroup:
    Value: !Ref InstanceSecurityGroup
    Export: 
      Name: !Sub ${AWS::StackName}-SecurityGroup
```

別のスタックからExportされた値を参照するには、!ImportValue文を使用します。
以下はNW.ymlからExportされたセキュリティグループとサブネットの値を使用して、EC2インスタンスを立てるCloudFormationテンプレートです。

```EC2.yml
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  EnviromentType:
    Type: String
    Default: "DEV"
    AllowedValues: ["DEV","STG","PRO"]
    Description: "EnviromentType Allowed DEV,STG,PRO"
  NetworkStackName:
    Type: String
    Default: "NW"
    Description: "NetworkStackName"

Mappings: 
  RegionMap: 
    us-east-1: 
      "HVM64": "ami-0ff8a91507f77f867"
    us-west-1: 
      "HVM64": "ami-0bdb828fd58c52235"
    eu-west-1: 
      "HVM64": "ami-047bb4163c506cd98"
    ap-southeast-1: 
      "HVM64": "ami-08569b978cc4dfa10"
    ap-northeast-1: 
      "HVM64": "ami-06ee4e2261a4dc5c3"

Resources:
  EC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      InstanceType: "t2.micro"
      ImageId: !FindInMap [RegionMap,!Ref "AWS::Region", HVM64]
      # ImportValueでExportされているリソースのキーを指定する
      SecurityGroupIds: 
        - !ImportValue 
            'Fn::Sub': '${NetworkStackName}-SecurityGroup'
      SubnetId: !ImportValue 
        'Fn::Sub': '${NetworkStackName}-Subnet'
      Tags:
        - Key: Name
          Value: !Sub ${EnviromentType}-EC2Instance
```

# 終わりに

今回はパラメータ、マッピング、クロススタック参照の３つをご紹介しました。
何かシステムを作る際はCloudFormationのようなIaCサービスを利用することがヒューマンエラーを無くす第一歩ですが、その中でもさらに工夫できることがわかりました。

同じリソースを作る場合でも、CloudFormationテンプレートの書き方はたくさんあります。それだけ属人性が高いということです。

参考になれば幸いです。

以上、「ヒューマンエラーの少ないCloudFormationテンプレートの作り方」でした。