CloudFormation で ループ機能を実現（Fn::ForEach 組み込み関数）

# 内容

https://aws.amazon.com/jp/about-aws/whats-new/2023/07/accelerate-cloudformation-authoring-experience-looping-function/

- AWS CloudFormation が Fn::ForEach 組み込み関数によるループ機能を発表
- Fn:: ForEach を使用するには、AWS::LanguageExtensions 変換を宣言する。

というのがざっくりした内容です。

リソースが少ない場合は、1　つ　1　つ書いていけば良いですが、リソースが多数存在する場合は少ないコード数で記載することができます。
AWS CDK を利用すればプログラミング言語内でループ処理の実現は可能ですが、プログラミング言語に熟知していないインフラエンジニアの方などにとっては　CloudFormation　でループ処理ができるため嬉しいアップデートですね。

# やってみた

公式の Examples を元にいくつか動作確認してみました。

## Resources

### Resources1

success,Failure,Timeout,Unknown の 4 つの SnsTopic を作る例です。
FifoTopic: true では動かなかったためコメントアウトしています。

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

Resources:
  'Fn::ForEach::Topics':
    - TopicName
    - - Success
      - Failure
      - Timeout
      - Unknown
    - 'SnsTopic${TopicName}':
        Type: 'AWS::SNS::Topic'
        Properties:
          TopicName: !Ref TopicName
          # FifoTopic: true
```

Cloudformation の「処理されたテンプレートの表示」から確認すると次の通り処理されました。

```json
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Resources": {
    "SnsTopicSuccess": {
      "Type": "AWS::SNS::Topic",
      "Properties": {
        "TopicName": "Success"
      }
    },
    "SnsTopicFailure": {
      "Type": "AWS::SNS::Topic",
      "Properties": {
        "TopicName": "Failure"
      }
    },
    "SnsTopicTimeout": {
      "Type": "AWS::SNS::Topic",
      "Properties": {
        "TopicName": "Timeout"
      }
    },
    "SnsTopicUnknown": {
      "Type": "AWS::SNS::Topic",
      "Properties": {
        "TopicName": "Unknown"
      }
    }
  }
```

### Resources2

Points,Score,Name,Leaderboard の 4 つの　DynamoDB　Table　を作成する例です。

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

Resources:
  'Fn::ForEach::Tables':
    - TableName
    - [Points, Score, Name, Leaderboard]
    - 'DynamoDB${TableName}':
        Type: 'AWS::DynamoDB::Table'
        Properties:
          TableName: !Ref TableName
          AttributeDefinitions:
            - AttributeName: id
              AttributeType: S
          KeySchema:
            - AttributeName: id
              KeyType: HASH
          ProvisionedThroughput:
            ReadCapacityUnits: '5'
            WriteCapacityUnits: '5'
```

Cloudformation の「処理されたテンプレートの表示」から確認すると次の通り処理されました。

```json
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Resources": {
    "DynamoDBPoints": {
      "Type": "AWS::DynamoDB::Table",
      "Properties": {
        "TableName": "Points",
        "AttributeDefinitions": [
          {
            "AttributeName": "id",
            "AttributeType": "S"
          }
        ],
        "KeySchema": [
          {
            "AttributeName": "id",
            "KeyType": "HASH"
          }
        ],
        "ProvisionedThroughput": {
          "ReadCapacityUnits": "5",
          "WriteCapacityUnits": "5"
        }
      }
    },
    "DynamoDBScore": {
      "Type": "AWS::DynamoDB::Table",
      "Properties": {
        "TableName": "Score",
        "AttributeDefinitions": [
          {
            "AttributeName": "id",
            "AttributeType": "S"
          }
        ],
        "KeySchema": [
          {
            "AttributeName": "id",
            "KeyType": "HASH"
          }
        ],
        "ProvisionedThroughput": {
          "ReadCapacityUnits": "5",
          "WriteCapacityUnits": "5"
        }
      }
    },
    "DynamoDBName": {
      "Type": "AWS::DynamoDB::Table",
      "Properties": {
        "TableName": "Name",
        "AttributeDefinitions": [
          {
            "AttributeName": "id",
            "AttributeType": "S"
          }
        ],
        "KeySchema": [
          {
            "AttributeName": "id",
            "KeyType": "HASH"
          }
        ],
        "ProvisionedThroughput": {
          "ReadCapacityUnits": "5",
          "WriteCapacityUnits": "5"
        }
      }
    },
    "DynamoDBLeaderboard": {
      "Type": "AWS::DynamoDB::Table",
      "Properties": {
        "TableName": "Leaderboard",
        "AttributeDefinitions": [
          {
            "AttributeName": "id",
            "AttributeType": "S"
          }
        ],
        "KeySchema": [
          {
            "AttributeName": "id",
            "KeyType": "HASH"
          }
        ],
        "ProvisionedThroughput": {
          "ReadCapacityUnits": "5",
          "WriteCapacityUnits": "5"
        }
      }
    }
  }
}
```

### Resources3

次の例では、
　インスタンスA は、　t2.micro（default）
　インスタンスB は、　t2.small
　インスタンスC は、　t3.small
とそれぞれ異なったプロパティを参照する例です。

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

Mappings:
  Instances:
    InstanceType:
      B: t2.small
      C: t3.small

Resources:
  'Fn::ForEach::Instances':
  - Identifier
  - [A, B, C]
  - 'Instance${Identifier}':
      Type: 'AWS::EC2::Instance'
      Properties:
        InstanceType: !FindInMap [Instances, InstanceType, !Ref 'Identifier', {DefaultValue: t2.micro}]
        ImageId: ami-04beabd6a4fb6ab6f

Outputs:
  SecondInstanceId:
    Description: Instance Id for InstanceB
    Value: !Ref 'InstanceB'
  SecondPrivateIp:
    Description: Private IP for InstanceB
    Value: !GetAtt [InstanceB, PrivateIp]
```

Cloudformation の「処理されたテンプレートの表示」から確認すると次の通り処理されました。

```json
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Mappings": {
    "Instances": {
      "InstanceType": {
        "B": "t2.small",
        "C": "t3.small"
      }
    }
  },
  "Outputs": {
    "SecondInstanceId": {
      "Description": "Instance Id for InstanceB",
      "Value": {
        "Ref": "InstanceB"
      }
    },
    "SecondPrivateIp": {
      "Description": "Private IP for InstanceB",
      "Value": {
        "Fn::GetAtt": [
          "InstanceB",
          "PrivateIp"
        ]
      }
    }
  },
  "Resources": {
    "InstanceA": {
      "Type": "AWS::EC2::Instance",
      "Properties": {
        "InstanceType": "t2.micro",
        "ImageId": "ami-04beabd6a4fb6ab6f"
      }
    },
    "InstanceB": {
      "Type": "AWS::EC2::Instance",
      "Properties": {
        "InstanceType": "t2.small",
        "ImageId": "ami-04beabd6a4fb6ab6f"
      }
    },
    "InstanceC": {
      "Type": "AWS::EC2::Instance",
      "Properties": {
        "InstanceType": "t3.small",
        "ImageId": "ami-04beabd6a4fb6ab6f"
      }
    }
  }
}
```

### Resources4

次の例では、
　インスタンスA は、　t2.micro
　インスタンスB は、　t2.small
　インスタンスC は、　t3.small
とそれぞれ異なったプロパティを繰り返し処理する例です。
結果は、Resources3 と同じです

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

Mappings:
  InstanceA:
    Properties:
      ImageId: ami-04beabd6a4fb6ab6f
      InstanceType: t2.micro
  InstanceB:
    Properties:
      ImageId: ami-04beabd6a4fb6ab6f
      InstanceType: t2.small
  InstanceC:
    Properties:
      ImageId: ami-04beabd6a4fb6ab6f
      InstanceType: t3.small

Resources:
  'Fn::ForEach::Instances':
  - InstanceLogicalId
  - [InstanceA, InstanceB, InstanceC]
  - '${InstanceLogicalId}':
      Type: 'AWS::EC2::Instance'
      Properties:
        'Fn::ForEach::Properties':
          - PropertyName
          - [ImageId, InstanceType]
          - '${PropertyName}':
             'Fn::FindInMap':
               - Ref: 'InstanceLogicalId'
               - Properties
               - Ref: 'PropertyName'
               - {DefaultValue: !Ref 'AWS::NoValue'}
```

Cloudformation の「処理されたテンプレートの表示」から確認すると次の通り処理されました。


```json
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Mappings": {
    "InstanceA": {
      "Properties": {
        "ImageId": "ami-04beabd6a4fb6ab6f",
        "InstanceType": "t2.smicro"
      }
    },
    "InstanceB": {
      "Properties": {
        "ImageId": "ami-04beabd6a4fb6ab6f",
        "InstanceType": "t2.small"
      }
    },
    "InstanceC": {
      "Properties": {
        "ImageId": "ami-04beabd6a4fb6ab6f",
        "InstanceType": "t3.small"
      }
    }
  },
  "Resources": {
    "InstanceA": {
      "Type": "AWS::EC2::Instance",
      "Properties": {
        "ImageId": "ami-04beabd6a4fb6ab6f",
        "InstanceType": "t2.micro"
      }
    },
    "InstanceB": {
      "Type": "AWS::EC2::Instance",
      "Properties": {
        "ImageId": "ami-04beabd6a4fb6ab6f",
        "InstanceType": "t2.small"
      }
    },
    "InstanceC": {
      "Type": "AWS::EC2::Instance",
      "Properties": {
        "ImageId": "ami-04beabd6a4fb6ab6f",
        "InstanceType": "t3.small"
      }
    }
  }
}
```

## Outputs

### Outputs1

次の例では、
　ForEach_S3Bucket_A
　ForEach_S3Bucket_B
という S3バケットを作成し、
S3バケットの　Arn　と　DomainName　を　出力しています。

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

Resources:
  'Fn::ForEach::Buckets':
    - Identifier
    - [A, B]
    - 'S3Bucket${Identifier}':
        Type: 'AWS::S3::Bucket'

Outputs:
  'Fn::ForEach::BucketOutputs':
    - Identifier
    - [A, B]
    - 'Fn::ForEach::GetAttLoop':
        - Property
        - [Arn, DomainName]
        - 'S3Bucket${Identifier}${Property}':
            Value: !GetAtt [!Sub 'S3Bucket${Identifier}', !Ref Property]
```

Cloudformation の「処理されたテンプレートの表示」から確認すると次の通り処理されました。

```json
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Outputs": {
    "S3BucketAArn": {
      "Value": {
        "Fn::GetAtt": [
          "S3BucketA",
          "Arn"
        ]
      }
    },
    "S3BucketADomainName": {
      "Value": {
        "Fn::GetAtt": [
          "S3BucketA",
          "DomainName"
        ]
      }
    },
    "S3BucketBArn": {
      "Value": {
        "Fn::GetAtt": [
          "S3BucketB",
          "Arn"
        ]
      }
    },
    "S3BucketBDomainName": {
      "Value": {
        "Fn::GetAtt": [
          "S3BucketB",
          "DomainName"
        ]
      }
    }
  },
  "Resources": {
    "S3BucketA": {
      "Type": "AWS::S3::Bucket"
    },
    "S3BucketB": {
      "Type": "AWS::S3::Bucket"
    }
  }
}
```