【小ネタ】CloudFormationをYAMLで書くときは短縮記法を使うとよいという話
はじめに
こんにちは、中山です。
みなさんCloudFormationをYAMLで書いてますか。JSONで書いている場合はすぐに以下のエントリを見てYAMLに変換しましょう。
2017年3月11日追記
CloudFormationをJSONからYAMLに変更したい場合は、AWSがGitHubで公開しているaws-cfn-template-flipもご検討ください。短縮記法への変換もサポートされています。
私もバリバリYAMLで書いてるのですが、ちょっとだけツライ部分があります。それは利用するリソースが増えてくると行数がどんどん増えていく問題です。単純に行数が増えるだけでもテンプレートを読み解く時に萎えますが、YAMLの場合インデントが文法上重要な意味を持ちます。行数が多いとディスプレイに入り切らずに何個インデントを付ければいいのか結構迷うことが多いです。
CloudFormationは残念ながら現時点でループ機能をサポートしていません。そのため、例えばEC2インスタンスを2つ作ろうとすると愚直に2つ AWS::EC2::Instance
リソースを定義する必要があります。また、最近のアップデートでクロススタック参照が入ったので、リソース毎にファイルを分割することも可能ですが、いちいち参照させる/する変数をエクスポート/インポートするの結構シンドいと感じています。Terraformつか(ry
こういった問題点があるのでどうしたものかと考えていたところ、解決策とまではいかないですが、有効な対策を見つけたのでブログのネタにしてみます。それは「組み込み関数の短縮記法」です。YAMLサポートに合わせて組み込み関数(例えば Fn::FindInMap
など)を !FuncName
という短縮記法で記述できるようになりました。
これだけ見ると Fn::
を !
と書けるだけ?と勘違いするかと思いますが、違います。そんなふうに考えていた時期が俺にもありました案件です。この記法は要するに関数を「埋め込む」記法だと考えるとよいと思います。関数の戻り値を変数のように埋め込めるので、全て1行で記述可能です。
また、関数の引数に Ref
や Fn::GetAtt
を利用して属性を参照することはよくあると思います。通常の記述方法ではコロンを利用する必要があるので、1度改行させる必要があります。そこも短縮記法を利用すれば1行で書けるので行数の短縮が捗るという訳です。
あらいいですね、という訳で実際に試してみましょう。
使ってみる
今回はCloudFormationで以下の構成を短縮記法と通常の記述方法で書いた場合に、どの程度行数が短くなるのかを検証してみてその威力をお伝えしてみようと思います。
まずは通常の記述方法で書いた場合のCloudFormationテンプレートです。179行あります。
--- AWSTemplateFormatVersion: '2010-09-09' Description: YAML Long form Parameters: NameTagPrefix: Type: String Default: test Description: Prefix of Name tags. KeyPair: Description: KeyPair Name Type: AWS::EC2::KeyPair::KeyName Mappings: StackConfig: VPC: CIDR: 10.0.0.0/16 PublicSubnet: CIDR: 10.0.0.0/24 EC2: InstanceType: t2.nano ImageId: ami-1a15c77b Resources: VPC: Type: AWS::EC2::VPC Properties: CidrBlock: Fn::FindInMap: - StackConfig - VPC - CIDR EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: Fn::Join: - "-" - - Ref: NameTagPrefix - vpc InternetGateway: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: Fn::Join: - "-" - - Ref: NameTagPrefix - igw AttachGateway: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: Ref: VPC InternetGatewayId: Ref: InternetGateway PublicRouteTable: Type: AWS::EC2::RouteTable DependsOn: AttachGateway Properties: VpcId: Ref: VPC Tags: - Key: Name Value: Fn::Join: - "-" - - Ref: NameTagPrefix - public-route-table PublicRoute: Type: AWS::EC2::Route DependsOn: AttachGateway Properties: RouteTableId: Ref: PublicRouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: Ref: InternetGateway PublicSubnet: Type: AWS::EC2::Subnet DependsOn: AttachGateway Properties: VpcId: Ref: VPC AvailabilityZone: Fn::Select: - 0 - Fn::GetAZs: "" CidrBlock: Fn::FindInMap: - StackConfig - PublicSubnet - CIDR Tags: - Key: Name Value: Fn::Join: - "-" - - Ref: NameTagPrefix - public-subnet PublicSubnetRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: Ref: PublicSubnet RouteTableId: Ref: PublicRouteTable EC2SG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Enable ssh access to the instances VpcId: Ref: VPC SecurityGroupIngress: - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: 0.0.0.0/0 Tags: - Key: Name Value: Fn::Join: - "-" - - Ref: NameTagPrefix - sg EC2: Type: AWS::EC2::Instance Properties: InstanceType: Fn::FindInMap: - StackConfig - EC2 - InstanceType KeyName: Ref: KeyPair ImageId: Fn::FindInMap: - StackConfig - EC2 - ImageId NetworkInterfaces: - AssociatePublicIpAddress: true DeviceIndex: 0 GroupSet: - Ref: EC2SG SubnetId: Ref: PublicSubnet UserData: Fn::Base64: | #!/bin/bash yum update -y yum install nginx -y service nginx start Tags: - Key: Name Value: Fn::Join: - "-" - - Ref: NameTagPrefix - ec2 Outputs: PublicSubnet: Value: Fn::GetAtt: - EC2 - PublicIp
続いて短縮記法で記述したテンプレートです。117行で書けました。変更点をハイライトしておきます。
--- AWSTemplateFormatVersion: '2010-09-09' Description: YAML Short form Parameters: NameTagPrefix: Type: String Default: test Description: Prefix of Name tags. KeyPair: Description: KeyPair Name Type: AWS::EC2::KeyPair::KeyName Mappings: StackConfig: VPC: CIDR: 10.0.0.0/16 PublicSubnet: CIDR: 10.0.0.0/24 EC2: InstanceType: t2.nano ImageId: ami-1a15c77b Resources: VPC: Type: AWS::EC2::VPC Properties: CidrBlock: !FindInMap [ StackConfig, VPC, CIDR ] EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: !Join [ "-", [ !Ref NameTagPrefix, vpc ] ] InternetGateway: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: !Join [ "-", [ !Ref NameTagPrefix, igw ] ] AttachGateway: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref VPC InternetGatewayId: !Ref InternetGateway PublicRouteTable: Type: AWS::EC2::RouteTable DependsOn: AttachGateway Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Join [ "-", [ !Ref NameTagPrefix, public-route-table ] ] PublicRoute: Type: AWS::EC2::Route DependsOn: AttachGateway Properties: RouteTableId: !Ref PublicRouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway PublicSubnet: Type: AWS::EC2::Subnet DependsOn: AttachGateway Properties: VpcId: !Ref VPC AvailabilityZone: !Select - 0 - Fn::GetAZs: !Ref "AWS::Region" CidrBlock: !FindInMap [ StackConfig, PublicSubnet, CIDR ] Tags: - Key: Name Value: !Join [ "-", [ !Ref NameTagPrefix, public-subnet ] ] PublicSubnetRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PublicSubnet RouteTableId: !Ref PublicRouteTable EC2SG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Enable ssh access to the instances VpcId: !Ref VPC SecurityGroupIngress: - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: 0.0.0.0/0 Tags: - Key: Name Value: !Join [ "-", [ !Ref NameTagPrefix, sg ] ] EC2: Type: AWS::EC2::Instance Properties: InstanceType: !FindInMap [ StackConfig, EC2, InstanceType ] KeyName: !Ref KeyPair ImageId: !FindInMap [ StackConfig, EC2, ImageId ] NetworkInterfaces: - AssociatePublicIpAddress: true DeviceIndex: 0 GroupSet: - !Ref EC2SG SubnetId: !Ref PublicSubnet UserData: !Base64 | #!/bin/bash yum update -y yum install nginx -y service nginx start Tags: - Key: Name Value: !Join [ "-", [ !Ref NameTagPrefix, ec2 ] ] Outputs: PublicSubnet: Value: !GetAtt [ EC2, PublicIp ]
比較すると62行も短縮できました(厳密に言うと単に別のYAML記法を使っている箇所もありますが)。やはりタグの記述が効いてますね。まぁ違いを強調するために「分かりやすい」形にしてはいますが。
ハマリポイント
便利な記述なので Fn::
と書いていた部分を全てこの短縮記法に変えたいところですが、1つ注意点があります。それは短縮記法で書いた関数の直後の引数に、同じように短縮記法で書いた関数を渡すとエラーとなるという点です。文章で説明すると難しいので、具体例で解説してみます。
まずNGな例を出します。 AWS::EC2::Subnet
リソースでAZを指定している箇所です。
AvailabilityZone: !Select [ 0, [ !GetAZs !Ref "AWS::Region" ] ]
Fn::GetAZs
を短縮記法で記述し、その直後の引数で Ref
を短縮記法で記述して呼び出しているためエラーとなります。エラー内容は以下のようなものです。
An error occurred (ValidationError) when calling the CreateStack operation: Template format error: YAML not well-formed. (line 65, column 48)
では、どう記述するかというとこちらのドキュメントにあるように、どちらかの関数を通常の記法で記述する必要があります。具体的には以下2つの方法で記述すればOKです。
AvailabilityZone: !Select - 0 - !GetAZs Ref: "AWS::Region"
AvailabilityZone: !Select - 0 - Fn::GetAZs: !Ref "AWS::Region"
最初の方のコードでは Ref
を1つ分改行させる必要があります。通常の記述方法でコロンを含んでいるためです。無駄に1行増えるので2つ目の書き方の方がよいと思います。
まとめ
いかがだったでしょうか。
目立たないアップデートかもしれませんが、YAMLでテンプレートを書いていくとこの記法は本当に便利です。ハマりポイントもありますが積極的に使っていくことをオススメします。
本エントリがみなさんの参考になれば幸いです。