CloudFormationのOutputsでForEach関数を使う

# 1. はじめに
CloudFormationのOutputsセクションでForEachを使ってサブネットIDをエクスポートするときに、最初うまくいかなかったので備忘録として

# 2. 何をしたいのか
CloudFormationテンプレートで作成したリソースを他のテンプレートで参照したい場合、参照元のテンプレートのOutputsセクションにリソースを記述すると思います。
その記述をリソースの数が多いと面倒なので、ForEach関数を使ってコードをきれいにしたいと思いました。

# 3. 失敗したテンプレート
```yaml
AWSTemplateFormatVersion: 2010-09-09
Description: Create VPC, Subent
Transform: AWS::LanguageExtensions

# Parameters Section
Parameters:
  System:
    Type: String
    Default: Sample
  VpcCidr:  
    Type: String
    Default: 10.0.0.0/24

# Mappings Section
Mappings:
  # SubnetMappings
  SubnetMappings:
    SampleSubnet01:
      Cidr: 10.0.0.0/27
      Az: ap-northeast-1c
      Number: 01
    SampleSubnet02:
      Cidr:  10.0.0.32/27
      Az: ap-northeast-1a
      Number: 02
    SampleSubnet03:
      Cidr: 10.0.0.64/27
      Az: ap-northeast-1c
      Number: 03
    SampleSubnet04:
      Cidr: 10.0.0.96/27 
      Az: ap-northeast-1a
      Number: 04
    SampleSubnet05:
      Cidr: 10.0.0.128/27
      Az: ap-northeast-1c
      Number: 05
    SampleSubnet06:
      Cidr: 10.0.0.160/27
      Az: ap-northeast-1a
      Number: 06  

# Resources Section
Resources:
# Sample Prd Vpc
  SampleVpc:
    Type: AWS::EC2::VPC
    Properties: 
      CidrBlock:  !Ref VpcCidr
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub ${System}-vpc

# Subnet
# Private Subnets
  'Fn::ForEach::SubnetLoop':
    - SubnetName
    - - SampleSubnet01
      - SampleSubnet02
      - SampleSubnet03
      - SampleSubnet04
      - SampleSubnet05
      - SampleSubnet06
    - '${SubnetName}':
        Type: AWS::EC2::Subnet
        Properties:
          AvailabilityZone: !FindInMap [SubnetMappings, !Ref SubnetName, Az]
          CidrBlock: !FindInMap [SubnetMappings, !Ref SubnetName, Cidr]
          VpcId: !Ref SampleVpc
          Tags:
            - Key: Name
              Value: !Sub 
                - ${System}-subnet-${SubnetNumber}
                - SubnetNumber: !FindInMap [SubnetMappings, !Ref SubnetName, Number]

# Outputs Section
Outputs:
#  VpcId
  SampleVpcId:
    Value: !Ref SampleVpc
    Export:
      Name: !Sub ${System}Vpc-Id

# SubnetId
  'Fn::ForEach::ExportSubnetLoop':
    - ExportSubnetName
    - - SampleSubnet01
      - SampleSubnet02
      - SampleSubnet03
      - SampleSubnet04
      - SampleSubnet05
      - SampleSubnet06
    - '${ExportSubnetName}Id':
         ###↓↓↓ここが誤り↓↓↓###
          Value: !Ref ExportSubnetName
         ###↑↑↑ここが誤り↑↑↑###
          Export: 
            Name: !Sub ${ExportSubnetName}-Id
```

間違い部分は`Value: !Ref ExportSubnetName`となります。
ForEach関数を使用した場合、Ref関数でExportSubnetNameの値を取得しても、IDを取得できず、スタックの出力結果は下の画像のようになってしまします。
これでは、VPCは参照できますが、サブネットは参照できません。

![スクリーンショット 2023-11-25 133019.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2222596/a4dc314b-e09d-c1fa-76e0-d2360d35657b.png)

# 4. ではどうればよいか
```yaml
# Outputs Section
Outputs:
#  VpcId
  SampleVpcId:
    Value: !Ref SampleVpc
    Export:
      Name: !Sub ${System}Vpc-Id
      
# SubnetId
  'Fn::ForEach::ExportSubnetLoop':
    - ExportSubnetName
    - - SampleSubnet01
      - SampleSubnet02
      - SampleSubnet03
      - SampleSubnet04
      - SampleSubnet05
      - SampleSubnet06
    - '${ExportSubnetName}Id':
          Value: !GetAtt [!Ref ExportSubnetName, SubnetId]
          Export: 
            Name: !Sub ${ExportSubnetName}-Id
```

`Value`は`!GetAtt`を使って記述します。
こうすれば`ExportSubnetName`のサブネットIDを取得して、値として出力してくれるようになります。

![スクリーンショット 2023-11-25 132658.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2222596/d78d7c3b-1ec9-1f90-9847-ab289433f636.png)

# 5. 終わりに
ForEachを使った場合、Mappingsにパラメータをまとめるので、Resourcesは短くなりますが、Mappingsが以外と長くなります。
ForEachを使ってResourcesを短くしたほうがいいのか、通常どおり書いたほうがいいのかは、人によると思いました。

あと、`Transform: AWS::LanguageExtensions`を忘れやすいので、気を付けましょう。