こんにちは、シマです。
先日、CloudFormationでForEach組み込み関数がリリースされ、以下の記事を書きました。
今回はもう少し踏み込んで、普段作るようなテンプレートをなるべくForEachを利用して作成するとどこまでできて、どこから難しいのか気になったので試してみました。
構成
今回、テンプレートで作成した構成は一般的なWeb三層構造をイメージした以下です。
ALBやEC2、RDSを配置するとより実践的ですが、ForEachを試すという観点では冗長になりそうだったので、今回は割愛しています。
テンプレートファイル
なるべくForEachを利用して作成したテンプレートファイルは以下です。
※折りたたんであります。
template.yml
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::LanguageExtensions
Parameters:
azList:
Type: List<String>
Default: az1,az2
snList:
Type: List<String>
Default: snPub,snWeb,snDb
rtList:
Type: List<String>
Default: rtPub,rtWeb,rtDb
sgList:
Type: List<String>
Default: sgAlb
sgIngressList:
Type: List<String>
Default: albHttps,albHttp
Mappings:
azMappings:
az1:
availabilityZone: ap-northeast-1a
name: a
cidrBlock: 192.168.1
az2:
availabilityZone: ap-northeast-1c
name: c
cidrBlock: 192.168.2
snMappings:
snPub:
cidrBlock: 1.0/24
nameTags: sn-pub-
rt: rtPub
snWeb:
cidrBlock: 2.0/24
nameTags: sn-web-
rt: rtWeb
snDb:
cidrBlock: 3.0/24
nameTags: sn-db-
rt: rtDb
rtMappings:
rtPub:
nameTags: rt-pub-
rtWeb:
nameTags: rt-web-
rtDb:
nameTags: rt-db-
sgMappings:
sgAlb:
groupName: sgalb
sgIngressMappings:
albHttps:
fromPort: 443
toPort: 443
cidrIp: xx.xx.xx.xx/32
groupId: sgAlb
Description: https from xxx
albHttp:
fromPort: 80
toPort: 80
cidrIp: xx.xx.xx.xx/32
groupId: sgAlb
Description: http from xxx
Resources:
# eip
# ------------------------------------------------------------#
Fn::ForEach::eipLoop:
- azItems
- !Ref azList
- npingw${azItems}:
Type: AWS::EC2::EIP
Properties:
Tags:
- Key: Name
Value: !Sub
- eip-ngw-${az}
- az: !FindInMap [azMappings ,!Ref azItems ,name]
# vpc
# ------------------------------------------------------------#
vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 192.168.0.0/16
Tags:
- Key: Name
Value: vpc
# RouteTable
# ------------------------------------------------------------#
Fn::ForEach::rtLoop:
- rtItems
- !Ref rtList
- Fn::ForEach::azLoop:
- azItems
- !Ref azList
- ${rtItems}${azItems}:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref vpc
Tags:
- Key: Name
Value: !Sub
- ${rtName}${azName}
- rtName: !FindInMap [rtMappings, !Ref rtItems, nameTags]
azName: !FindInMap [azMappings, !Ref azItems, name]
# Subnet
# ------------------------------------------------------------#
Fn::ForEach::snLoop:
- snItems
- !Ref snList
- Fn::ForEach::azLoop:
- azItems
- !Ref azList
- ${snItems}${azItems}:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref vpc
CidrBlock: !Sub
- ${azCidrBlock}${snCidrBlock}
- azCidrBlock: !FindInMap [azMappings, !Ref azItems, cidrBlock]
snCidrBlock: !FindInMap [snMappings, !Ref snItems, cidrBlock]
AvailabilityZone: !FindInMap [azMappings, !Ref azItems, availabilityZone]
Tags:
- Key: Name
Value: !Sub
- ${snName}${azName}
- snName: !FindInMap [snMappings, !Ref snItems, nameTags]
azName: !FindInMap [azMappings, !Ref azItems, name]
Association${snItems}${azItems}:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref
Fn::Sub: ${snItems}${azItems}
RouteTableId: !Ref
Fn::Sub:
- ${rt}${azItems}
- rt: !FindInMap [snMappings, !Ref snItems, rt]
# InternetGateway
# ------------------------------------------------------------#
igw:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: igw
AttachmentIgw:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref vpc
InternetGatewayId: !Ref igw
Fn::ForEach::azIgwRtLoop:
- azItems
- !Ref azList
- rtIgw${azItems}:
Type: AWS::EC2::Route
DependsOn: AttachmentIgw
Properties:
RouteTableId: !Ref
Fn::Sub: rtPub${azItems}
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref igw
# NatGateway
# ------------------------------------------------------------#
Fn::ForEach::azNgwLoop:
- azItems
- !Ref azList
- ngw${azItems}:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt
- Fn::Sub: npingw${azItems}
- AllocationId
SubnetId: !Ref
Fn::Sub: snPub${azItems}
Tags:
- Key: Name
Value: !Sub
- ngw-${azName}
- azName: !FindInMap [azMappings, !Ref azItems, name]
Fn::ForEach::azNgwRtLoop:
- azItems
- !Ref azList
- rtNgw${azItems}:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref
Fn::Sub: rtWeb${azItems}
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref
Fn::Sub: ngw${azItems}
# Security Group
# ------------------------------------------------------------#
Fn::ForEach::sgLoop:
- sgItems
- !Ref sgList
- ${sgItems}:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !FindInMap [sgMappings, !Ref sgItems, groupName]
GroupDescription: !FindInMap [sgMappings, !Ref sgItems, groupName]
VpcId: !Ref vpc
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !FindInMap [sgMappings, !Ref sgItems, groupName]
Fn::ForEach::sgIngressLoop:
- sgIngressItems
- !Ref sgIngressList
- ${sgIngressItems}:
Type: AWS::EC2::SecurityGroupIngress
Properties:
IpProtocol: !FindInMap [sgIngressMappings, !Ref sgIngressItems, ipProtocol, DefaultValue: tcp]
FromPort: !FindInMap [sgIngressMappings, !Ref sgIngressItems, fromPort]
ToPort: !FindInMap [sgIngressMappings, !Ref sgIngressItems, toPort]
CidrIp: !FindInMap [sgIngressMappings, !Ref sgIngressItems, cidrIp]
GroupId: !Ref
Fn::FindInMap: [sgIngressMappings, !Ref sgIngressItems, groupId]
Description: !FindInMap [sgIngressMappings, !Ref sgIngressItems, Description]
感想
今回作成したテンプレート内では、RouteTableを作成しているところのように、単純に複数作成するようなケースはとても有用であると感じました。しかし一方で、変化するパラメータの数が多くなると、Mappingsで定義する数も増え、ForEachで楽をした分Mappingsが増えるということになってしまいます。また、当たり前ですが、全てにおいてForEachを利用すると複雑化してしまい、可読性の低下やバグの原因になってしまうため、無理をせずに便利だなと感じれるところで利用することがよいです。さらに複雑な条件分岐でループ処理をするなら素直にCDKの利用がベストだと感じました。
せっかくなので、少し複雑になってしまったところや、やりたかったけど出来なかったところについて、下記で簡単にご紹介いたします。
①計算ができない
サブネットのCIDRを設定する際に、VPCのCIDRから計算が出来れば便利だなと感じましたが、テンプレート内では計算ができません。今回はパラメータとして文字列を与え、文字列の結合でパターンが分けられるようにテンプレートに都合の良いパラメータで作成しています。
②ForEachの変数でIFが使えない
ForEach内の変数により条件分岐をさせて、作成するリソースを変化させたいことがありました。しかし、組み込み関数にIFではParametersの内容による分岐は可能ですがForEach内の変数を使った条件分岐はできませんでした。
③組み込み関数の中での組み込み関数利用
Subで文字列結合や変数置き換えや、Refで指定する文字列が、Mappingsの変数と絡むケースが多かったです。例えば、以下のようなケースです。
RouteTableId: !Ref
Fn::Sub:
- ${rt}${azItems}
- rt: !FindInMap [snMappings, !Ref snItems, rt]
RouteTableId は物理IDを与える必要があるため、論理IDに対してRefを使用すること(!Ref xxxx)が一般的です。今回のケースでは、Mappingsに格納しているためFindInMapで取りに行く必要があり、その値をSubで結合して論理IDを生成しています。
また、組み込み関数の中で組み込み関数を利用する場合はサポートされているかどうかを意識する必要があります。例えば、Refの中でサポートされている組み込み関数は以下のページの最下部に記載されています。
サポートされている関数
AWS::LanguageExtensions 変換トランスフォームを使用すると、Ref関数内で次の関数を使用できます。
Fn::Base64
Fn::FindInMap
Fn::If
Fn::Join
Fn::Sub
Fn::ToJsonString
Ref
記載されている通り、「AWS::LanguageExtensions 変換トランスフォーム」を利用する前提で利用可能です。また、前述の前提の場合は短縮形式で利用できないことに注意が必要です。
短縮形式の YAML 構文は、AWS::LanguageExtensions 変換でのみ使用できる組み込み関数のテンプレート内ではサポートされていません。
そのため、Refの中で利用するSubは !Sub では利用できず、Fn::Sub: として指定する必要があります。一方で、例の中でそのあとに続いているSubの中で利用するFindInMapは、AWS::LanguageExtensions 変換が不要で利用できるため、短縮形式での利用が可能です。
論理ID単位でのみForEachが可能
例えばタグを複数与える際には以下のようにテンプレートファイルに記載します。
Tags:
- Key: Name
Value: xxx
- Key: Env
Value: prd
- Key: Bill
Value: xxxSystem
なるべくForEachを利用しようという気持ちで上記を見ると、ForEachを使いたくなる見た目をしています。しかし、ForEachは論理ID単位でループ処理をさせる必要があるため、上記のような繰り返しにはForEachを利用することはできませんでした。
最後に
今回はなるべくForEachを利用しようという気持ちでテンプレートを作成してみました。その中で、できること、難しいところが見えてきたのでやってみて良かったと思っています。
本記事がどなたかのお役に立てれば幸いです。