【CloudFormation】!Subと!Refの使い分け

CloudFormationのテンプレートでよく使う組み込み関数に!Refと!Subがあります。 これらの違いを整理します。

基本的な使い方

基本的な作用

基本的な作用は同一で、論理名を参照し、規定の値に変換します 変換可能な値は、!Refの公式ドキュメントで参照できる他、各リソースの「戻り値」の項目で確認できる(例えば、Lambdaの公式ドキュメント

なお、ドキュメントでは!Refの戻り値しか紹介されていませんが、!Subを使っても全く同じ値が帰ってきます。

使い方

値を参照するに当たり、論理名を記載します。各関数は、リソースのタイプごとに定められた値を参照し、置換します。 使い方の注意点として、!Refは論理名を直接表記するのに対し、!Subは論理名を${}でくくって使用します。

Parameters:
  TestLambdaDescription:
    Type: String
    Default: This is a test message
Resources:
  SampleA:
    Type: AWS::Serverless::Function
    Properties:
      Description: !Ref TestLambdaDescription
      Handler: test.lambda_handler
      Runtime: python3.6
      CodeUri: calcData
  SampleB:
    Type: AWS::Serverless::Function
    Properties:
      Description: !Sub ${TestLambdaDescription}
      Handler: test.lambda_handler
      Runtime: python3.6
      CodeUri: calcData
#LambdaのDescriptionにはこのように表記される
- SampleA:
  - This is a test message
#LambdaのDescriptionにはこのように表記される
- SampleB:
  - This is a test message

誤った使い方

!Ref で参照する論理名を${}でくくると、CloudFormationはエラーを返します。

SampleA:
    Type: AWS::Serverless::Function
    Properties:
      Description: !Ref ${TestLambdaDescription}
      Handler: test.lambda_handler
      Runtime: python3.6
      CodeUri: calcData
FAILED - Template format error: Unresolved resource dependencies [${TestLambdaDescription}] in the Resources block of the template

また、!Subで${}を忘れると、エラーは発生しないが、後述の文字列を結合する作用のせいで、論理名が文字列として認識されるため、変換が行われません。

  SampleB:
    Type: AWS::Serverless::Function
    Properties:
      Description: !Sub TestLambdaDescription
      Handler: test.lambda_handler
      Runtime: python3.6
      CodeUri: calcData

そのため、論理名がそのまま表記されます。

#LambdaのDescriptionにはこのように表記される
- SampleB:
  -  TestLambdaDescription

!Subの問題点

今回は Descriptionで実験したため、特に影響はありませんが、実際にリソースの構築を行う際にこのミスのせいでMember must satisfy regular expression patternが発生する場合があります。

例として、私がたまに起こすケアレスミスを紹介します。 Serverless変換を用いてLambdaにLayerを適用するためには、以下のような項目が必要となります。

ExampleFunction:
    Type: AWS::Serverless::Function      
    Properties:
      #略
      Layers:
        - !Ref layerLogicalID

正常に動作するためには、上記のように使うLayerのARNをリストにして表記する必要があります。

しかしここで、Layerを!Subで参照し、!Refで参照するときの感覚で${}を忘れてしまう場合があります。

ExampleFunction:
    Type: AWS::Serverless::Function      
    Properties:
      #略
      Layers:
        - !Sub layerLogicalID

すると、以下のようなエラーが発生します

#Error message
1 validation errors detected: Value 'testRole' at 'role' failed to satisfy constraint: Member must satisfy regular expression pattern: arn:(aws[a-zA-Z-]*)?:iam::\d{12}:role/?[a-zA-Z_0-9+=,.@\-_/]+; (Service: AWSLambdaInternal; Status Code: 400; Error Code: ValidationException; Request ID: e8ba965e-8554-11e9-a28d-5b2bc395254c)

文字列との結合

!Subの場合

!Subは、単体で変数名と文字列を結合することができます。

Properties:
      Description: !Sub ${TestLambdaDescription}-test
      Handler: calcData.lambda_handler
      Runtime: python3.6
      Role: !GetAtt SQSRoleOfCalcData.Arn
      CodeUri: calcData
#LambdaのDescriptionにはこのように表記される
- SampleB:
  -  This is a test message-test

また、複数の値を置換することもできます。

Properties:
      Description: !Sub ${TestLambdaDescription}-test-${AWS::Partition}
      Handler: calcData.lambda_handler
      Runtime: python3.6
      Role: !GetAtt SQSRoleOfCalcData.Arn
      CodeUri: calcData
#LambdaのDescriptionにはこのように表記される
- SampleB:
  -  This is a test message-test-aws

!Refの場合

!Ref では文字列の結合を行うことが行うことができません。無理矢理やろうとしてもエラーメッセージが出ます。参照する値を複数にしても同様となります。

FAILED - Template format error: Unresolved resource dependencies [${TestLambdaDescription}-test] in the Resources block of the template

そのため、このような使い方を行う場合は別に!Join関数を使う必要があります。

!Join
  - '-'
  - !Ref TestLambdaDescription
  - test
  - !Ref AWS::Partition
#結果
This is a test message-test-aws

結論

!Ref と!Subの違いは次の通りです。

項目 !Ref !Sub
作用 論理名を規定の値に変換する 論理名を規定の値に変換する
値の参照方法 論理名をそのまま記載 論理名を${}でくくる
文字列の結合 単体ではできない 単体でできる

このため、このような使い分けをしております

  • 単体で値を参照する必要があるとき
    • 文字数が少なくて済むほか、ポカヨケになるので!Refを使う
  • 他の文字列と参照した値を結合する必要があるとき
    • !Subを使う

何かの参考になればと思います