CloudFormation で ForEach と FindInMap を組み合わせて使ってみた

2023.07.28

こんにちは、シマです。
先日、CloudFormationでForEach組み込み関数がリリースされ、CloudFormation内でループ処理が実装出来るようになりました。

ForEach組み込み関数については、弊社いわさの以下記事がとてもわかりやすいのでそちらをご確認ください。

実際のユースケースとしては、サブネットやEC2インスタンス作成のように類似するパラメータで複数作成するリソースなどに活用できそうです。しかし、与えたいパラメータは複数ある場合が多いため、ForEachの単純なループ処理では実装が難しいと感じました。そこで今回は、ForEachとFindInMapを利用して複数のパラメータをセットとして利用することができるのではと思ったので試してみました。

テンプレートファイル

作成対象はシンプルに、1つのVPCを作成して、その中に複数のサブネットを作成しています。早速ですが、今回作成したテンプレートファイルは以下です。

template.yml

AWSTemplateFormatVersion: 2010-09-09
Transform: 'AWS::LanguageExtensions'

Parameters:
  snList:
    Type: List<String>
    Default: snPubA,snWebA,snDbA,snPubC,snWebC,snDbC

Mappings: 
  snMappings: 
    snPubA: 
      cidrBlock: "192.168.1.0/24"
      az: ap-northeast-1a
      nameTags: sn-pub-a
    snWebA: 
      cidrBlock: "192.168.2.0/24"
      az: ap-northeast-1a
      nameTags: sn-web-a
    snDbA: 
      cidrBlock: "192.168.3.0/24"
      az: ap-northeast-1a
      nameTags: sn-db-a
    snPubC: 
      cidrBlock: "192.168.4.0/24"
      az: ap-northeast-1c
      nameTags: sn-pub-c
    snWebC: 
      cidrBlock: "192.168.5.0/24"
      az: ap-northeast-1c
      nameTags: sn-web-c
    snDbC: 
      cidrBlock: "192.168.6.0/24"
      az: ap-northeast-1c
      nameTags: sn-db-c

Resources:

#  VPC
# ------------------------------------------------------------#
  vpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: "192.168.0.0/16"
      Tags:
      - Key: Name
        Value: "vpc"

#  Subnet
# ------------------------------------------------------------#
  Fn::ForEach::snLoop:
    - snItems
    - !Ref snList
    - ${snItems}:
        Type: AWS::EC2::Subnet
        Properties:
          VpcId: !Ref vpc
          CidrBlock: !FindInMap
            - snMappings
            - !Ref snItems
            - cidrBlock
          AvailabilityZone: !FindInMap
            - snMappings
            - !Ref snItems
            - az
          Tags:
          - Key: Name
            Value: !FindInMap
              - snMappings
              - !Ref snItems
              - nameTags

ForEachを使わなかった場合の参考として、処理されたテンプレートを以下に記載いたします。
※折りたたんであります。

処理されたテンプレート
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Parameters": {
    "snList": {
      "Type": "List<String>",
      "Default": "snPubA,snWebA,snDbA,snPubC,snWebC,snDbC"
    }
  },
  "Mappings": {
    "snMappings": {
      "snPubA": {
        "cidrBlock": "192.168.1.0/24",
        "az": "ap-northeast-1a",
        "nameTags": "sn-pub-a"
      },
      "snWebA": {
        "cidrBlock": "192.168.2.0/24",
        "az": "ap-northeast-1a",
        "nameTags": "sn-web-a"
      },
      "snDbA": {
        "cidrBlock": "192.168.3.0/24",
        "az": "ap-northeast-1a",
        "nameTags": "sn-db-a"
      },
      "snPubC": {
        "cidrBlock": "192.168.4.0/24",
        "az": "ap-northeast-1c",
        "nameTags": "sn-pub-c"
      },
      "snWebC": {
        "cidrBlock": "192.168.5.0/24",
        "az": "ap-northeast-1c",
        "nameTags": "sn-web-c"
      },
      "snDbC": {
        "cidrBlock": "192.168.6.0/24",
        "az": "ap-northeast-1c",
        "nameTags": "sn-db-c"
      }
    }
  },
  "Resources": {
    "vpc": {
      "Type": "AWS::EC2::VPC",
      "Properties": {
        "CidrBlock": "192.168.0.0/16",
        "Tags": [
          {
            "Key": "Name",
            "Value": "vpc"
          }
        ]
      }
    },
    "snPubA": {
      "Type": "AWS::EC2::Subnet",
      "Properties": {
        "VpcId": {
          "Ref": "vpc"
        },
        "CidrBlock": "192.168.1.0/24",
        "AvailabilityZone": "ap-northeast-1a",
        "Tags": [
          {
            "Key": "Name",
            "Value": "sn-pub-a"
          }
        ]
      }
    },
    "snWebA": {
      "Type": "AWS::EC2::Subnet",
      "Properties": {
        "VpcId": {
          "Ref": "vpc"
        },
        "CidrBlock": "192.168.2.0/24",
        "AvailabilityZone": "ap-northeast-1a",
        "Tags": [
          {
            "Key": "Name",
            "Value": "sn-web-a"
          }
        ]
      }
    },
    "snDbA": {
      "Type": "AWS::EC2::Subnet",
      "Properties": {
        "VpcId": {
          "Ref": "vpc"
        },
        "CidrBlock": "192.168.3.0/24",
        "AvailabilityZone": "ap-northeast-1a",
        "Tags": [
          {
            "Key": "Name",
            "Value": "sn-db-a"
          }
        ]
      }
    },
    "snPubC": {
      "Type": "AWS::EC2::Subnet",
      "Properties": {
        "VpcId": {
          "Ref": "vpc"
        },
        "CidrBlock": "192.168.4.0/24",
        "AvailabilityZone": "ap-northeast-1c",
        "Tags": [
          {
            "Key": "Name",
            "Value": "sn-pub-c"
          }
        ]
      }
    },
    "snWebC": {
      "Type": "AWS::EC2::Subnet",
      "Properties": {
        "VpcId": {
          "Ref": "vpc"
        },
        "CidrBlock": "192.168.5.0/24",
        "AvailabilityZone": "ap-northeast-1c",
        "Tags": [
          {
            "Key": "Name",
            "Value": "sn-web-c"
          }
        ]
      }
    },
    "snDbC": {
      "Type": "AWS::EC2::Subnet",
      "Properties": {
        "VpcId": {
          "Ref": "vpc"
        },
        "CidrBlock": "192.168.6.0/24",
        "AvailabilityZone": "ap-northeast-1c",
        "Tags": [
          {
            "Key": "Name",
            "Value": "sn-db-c"
          }
        ]
      }
    }
  }
}

上記のように実装することで、ParametersとMappingsに対して変更を行うだけで、リソースの増減に対応可能である点がメリットに感じました。また、ForEachにより無駄なコピペが発生しないため、ヒューマンエラーを減らすことも可能です。

テンプレートファイルの簡単な説明

ForEachで使用する要素をParametersにてリスト値として記載します。

Parameters:
  snList:
    Type: List<String>
    Default: snPubA,snWebA,snDbA,snPubC,snWebC,snDbC

使用するパラメータをMappingsとして、snMappingsセクション内で、Parametersのリスト値(snPubA,snWebA...)と対応させたキーを記載します。Mappings内では、複数の値をマッピングできるため、使用したいパラメータを複数記載しています。

Mappings: 
  snMappings: 
    snPubA: 
      cidrBlock: "192.168.1.0/24"
      az: ap-northeast-1a
      nameTags: sn-pub-a

ForEachのループ処理で、FindInMapのsnMappingsセクションをループさせてFindInMapより使用したいパラメータを指定します。

  Fn::ForEach::snLoop:
    - snItems
    - !Ref snList
    - ${snItems}:
        Type: AWS::EC2::Subnet
        Properties:
          CidrBlock: !FindInMap
            - snMappings
            - !Ref snItems
            - cidrBlock

最後に

今回はForEachとFindInMapを組み合わせて使用してみました。実際の利用ケースではForEach単体だとかゆいところに手が届かない印象でしたが、他の組み込み関数とセットで利用することでさらに柔軟なテンプレートファイルが作れると感じました。

本記事がどなたかのお役に立てれば幸いです。