ForgeVision Engineer Blog

フォージビジョン エンジニア ブログ

CloudFormationの機能を活用しよう!~Fn::ForEach 組み込み関数による繰り返し処理の実装~

こんにちは。伊藤です。
CloudFormationからIaCに入門した私ですが、最近はTerraformに触れる時間が多くなっています。
Terraformは繰り返し処理やモジュール化などによりプログラミングライクで複雑な処理を実装することが可能です。

何かと比較されがちなCloudFormationとTerraformですが、
今回はCloudFormationでも繰り返し処理ができるんだぞ!というお話になります。

前提

Fn::ForEach関数は今年の7月に追加されたものになります。 aws.amazon.com

ワークショップが用意されているため、今回はこちらを実施していきます。 catalog.workshops.aws

CloudFormationのワークショップを初めて実施する方は、以下のコマンドでワークショップ用のコードを準備します。

git clone https://github.com/aws-samples/cfn101-workshop


AWSアカウントやCloud9など、環境の準備が必要な方は事前準備の項目を実施しましょう。

https://catalog.workshops.aws/cfn101/ja-JP/prerequisites

概要

Fn::ForEach関数について簡単に説明させていただきます。

CloudFormationにはデフォルトで使用されている言語機能を拡張するための仕組みが実装されており、
以下の記述をテンプレートに記載することで有効になります。

Transform: AWS::LanguageExtensions


Fn::ForEach関数はこの言語拡張機能の中に新しく追加されたものになります。

これまで、同様のリソースを複数作成する場合はどうしても冗長的な書き方になってしまいました。
テンプレートに持たせる汎用性との両立という意味でも悩ましい問題だったのではないでしょうか。
この関数の導入によって、こういった処理の実現に近づけたのではと嬉しく思います。

ワークショップの実施

ワークショップを実施していきます。

ワークショップは以下の項目から構成されています。
1. S3リソース作成の繰り返し処理
2. VPCリソース作成の繰り返し処理
3. チャレンジ

まずは1. S3リソース作成の繰り返し処理です。
この項目では設定が同じS3バケットを別々の名前で3つ作成します。

ワークショップに沿って作成したテンプレートが以下になります。

AWSTemplateFormatVersion: "2010-09-09"

Description: AWS CloudFormation workshop lab for looping over collections (uksb-1q9p31idr) (tag:looping-over-collections).

Transform: AWS::LanguageExtensions

Resources:
  Fn::ForEach::S3Buckets:
    - S3BucketLogicalId
    - [S3Bucket1, S3Bucket2, S3Bucket3]
    - ${S3BucketLogicalId}:
        Type: AWS::S3::Bucket
        Properties:
          BucketEncryption:
            ServerSideEncryptionConfiguration:
              - ServerSideEncryptionByDefault:
                  SSEAlgorithm: aws:kms
          LifecycleConfiguration:
            Rules:
              - Id: Example Glacier Rule
                ExpirationInDays: 365
                Status: Enabled
                Transitions:
                  - TransitionInDays: 30
                    StorageClass: GLACIER
          PublicAccessBlockConfiguration:
            BlockPublicAcls: true
            BlockPublicPolicy: true
            IgnorePublicAcls: true
            RestrictPublicBuckets: true
          Tags:
            - Key: Name
              Value: aws-cloudformation-workshop


5行目~8行目あたりが通常のS3テンプレートと異なることがわかりますね。

5行目は繰り返し処理の名前になります。
これはテンプレート内で一意である必要があります。
今回で言うとS3Bucketsが名前になります。

6行目は繰り返し処理の中で置き換えられる識別子です。
7行目で定義される値がこの識別子と置き換えられる形で処理されます。

7行目は繰り返し処理の要となる値のリストです。ドキュメントではコレクションと呼ばれています。
このリストの数だけ繰り返し処理が発生し、それぞれの処理の中でリスト内の値が読み込まれます。

8行目は繰り返し処理で生成されるテンプレートのキーです。
この値には必ず6行目に定義した値が含まれていなければなりません。
8行目と9行目でキーバリューの形になっています。

9行目以降は繰り返し処理で複製されるテンプレートの値になります。
9行目以降に記載されたテンプレートをベースとして、6行目で定義した識別子にコレクションの値が置き換えられて処理が行われます。

テンプレートを実行してみましょう。
作成されたリソースは以下のようになります。
繰り返し処理で作成されたリソースの確認01
7行目のコレクションで定義した値が置換され、論理IDとして機能しています。

スタック内のテンプレートを確認すると以下のようになっています。 テンプレートの確認01
テンプレート自体は上記の内容と変わりませんが、
処理されたテンプレートの表示という見慣れない画面表示が出ています。

これをオンにすることで、繰り返し処理によって処理された状態のテンプレートを確認することができます。 処理されたテンプレートの確認01
(長いですね…)
この画像を比較するだけでも、Fn::ForEach関数による簡略化の効果がわかります。

次に2. VPCリソース作成の繰り返し処理を実施します。
この項目ではVPC関連リソースを作成するのですが、
繰り返し処理の中で値の異なるサブネットを作成していきます。

テンプレートの内容を記載しますが、長くなるため抜粋しております。

AWSTemplateFormatVersion: "2010-09-09"

Description: AWS CloudFormation workshop lab for looping over collections (uksb-1q9p31idr) (tag:looping-over-collections).

Transform: AWS::LanguageExtensions

Mappings:
  SubnetAzIndexes:
    Public:
      "1": 0
      "2": 1
    Private:
      "1": 0
      "2": 1

  SubnetCidrs:
    Public:
      "1": 172.31.1.0/24
      "2": 172.31.2.0/24
    Private:
      "1": 172.31.11.0/24
      "2": 172.31.12.0/24

Resources:
  Fn::ForEach::SubnetTypes:
    - SubnetType
    - [Public, Private]
    - Fn::ForEach::SubnetNumbers:
        - SubnetNumber
        - ["1", "2"]
        - ${SubnetType}Subnet${SubnetNumber}:
            Type: AWS::EC2::Subnet
            Properties:
              AvailabilityZone: !Select
                - !FindInMap
                  - SubnetAzIndexes
                  - !Ref SubnetType
                  - !Ref SubnetNumber
                - !GetAZs ""
              CidrBlock: !FindInMap
                - SubnetCidrs
                - !Ref SubnetType
                - !Ref SubnetNumber
              Tags:
                - Key: Name
                  Value: aws-cloudformation-workshop
              VpcId: !Ref Vpc
          ${SubnetType}RouteTable${SubnetNumber}:
            Type: AWS::EC2::RouteTable
            Properties:
              Tags:
                - Key: Name
                  Value: aws-cloudformation-workshop
              VpcId: !Ref Vpc
          ${SubnetType}SubnetRouteTableAssociation${SubnetNumber}:
            Type: AWS::EC2::SubnetRouteTableAssociation
            Properties:
              RouteTableId: !Ref
                Fn::Sub: ${SubnetType}RouteTable${SubnetNumber}
              SubnetId: !Ref
                Fn::Sub: ${SubnetType}Subnet${SubnetNumber}

Fn::ForEach関数を入れ子にしたうえでMappingsを使用してコレクションの置換を制御しています。
このテンプレートからわかるように、繰り返し処理の中で差分を出すためにはコレクションによる制御が必要です。
個人的にはここがFn::ForEach関数使用の肝だと考えています。

テンプレートを実行すると次のようになります。
繰り返し処理で作成されたリソースの確認02
VPCを始め、複数のサブネットやNAT Gatewayが作成されています。

ワークショップは以上になります。

チャレンジの項目ではOutputsセクションでFn::ForEach関数を使用したテンプレート作成に挑戦できます。
余裕のある方はぜひチャレンジしてみましょう。

まとめ

ワークショップを実施してFn::ForEach関数への理解を深めることができました。
所感としては、差分の少ないリソースの複製が主な用途だと感じています。

異なるCIDRのサブネットを複数作成するといったような差分の実装は可能なのですが、
差分が大きくなるほどテンプレートが複雑化してしまいそうです。

Fn::ForEach関数の導入によって冗長性が改善されたとしても、
その一方で複雑性や可読性に問題が出てきてしまっては元も子もありませんので、
使いどころを見極めて導入していきましょう。