【小ネタ】CloudFormationをYAMLで書くときは短縮記法を使うとよいという話

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは、中山です。

みなさん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行で記述可能です。

また、関数の引数に RefFn::GetAtt を利用して属性を参照することはよくあると思います。通常の記述方法ではコロンを利用する必要があるので、1度改行させる必要があります。そこも短縮記法を利用すれば1行で書けるので行数の短縮が捗るという訳です。

あらいいですね、という訳で実際に試してみましょう。

使ってみる

今回はCloudFormationで以下の構成を短縮記法と通常の記述方法で書いた場合に、どの程度行数が短くなるのかを検証してみてその威力をお伝えしてみようと思います。

cfn-short-form

まずは通常の記述方法で書いた場合の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でテンプレートを書いていくとこの記法は本当に便利です。ハマりポイントもありますが積極的に使っていくことをオススメします。

本エントリがみなさんの参考になれば幸いです。