CloudFormation の組み込み関数 Fn::Sub のちょっとイイ使い方

こんにちは。デジタル・トランスフォーメーション推進開発部の保田(ほだ)と申します。

前回書いた記事からかなり間が空いてしまいました。 blog.css-net.co.jp

デジタル・トランスフォーメーションということで、似た名前の CloudFormation の話をします。

要約

Fn::Sub の値にリストを持ってくるといい感じにマッピングしてくれる

前提知識

前提知識は以下の通りです。

  • CloudFormation の初歩

本題

真面目な話は公式ドキュメントにすべて書いてあります。

■参考 マッピングで Fn::Sub

以降はザックリ噛み砕いて語っていきます。

CFn を語りたい

蛇足ですが、まず組み込み関数の基本的な説明をします。 知っとるわい、という方は適当に読み飛ばしていただいて大丈夫です。

まず Fn::Sub は CloudFormation テンプレートの組み込み関数の一つで、代入( Substitution)が出来ます。

例えば例を挙げると以下のような CFn のテンプレートのような使い方があります。

Parameters:
  MyBucketName:
    Type: String
    Description: "Enter a MyBucket Name"

Resources:
  MyBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName:
        Fn::Sub: ${MyBucketName} # ここ

Parameters セクションに書いたパラメータ MyBucketName の値を ${MyBucketName} に代入しているわけです。

YAML では省略記法として !Sub が使えますので、以降後者の書き方に統一します。

Parameters:
  MyBucketName:
    Type: String
    Description: "Enter a MyBucket Name"

Resources:
  MyBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${MyBucketName} # ここ

ネストが一つ浅くなるメリットもありますので、使えるところでは省略記法の方が良いでしょう。

ちなみにこの例だと、参照 Ref (省略記法で !Ref )で済むので本当はこっちの方が良いです。

Parameters:
  MyBucketName:
    Type: String
    Description: "Enter a MyBucket Name"

Resources:
  MyBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref MyBucketName # ここ

■参考 Ref

細かいところで言うと !Sub はパラメータへの代入なので${} で囲った ${MyBucketName} を、 !Ref は論理 ID の参照なので ${} を付けずに MyBucketName と書くところもポイントですね。

まぁ多分みなさん無意識に使い分けることが出来ていると思いますが…

次のような例だと Sub の出番です。

((前半は省略))

Resources:
  MyBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${MyBucketName}-${AWS::AccountId}

AWS::AccountId擬似パラメータ参照 です。 ザックリ言うと CFn スタックの作成・更新・削除を行った人(あるいは AWS リソース)の認証情報に応じて自動的にアカウント ID を参照してくれます。

AWS アカウント ID はセキュリティ面や可搬性の観点からもハードコーディングしないことが推奨されますので、この書き方はよく使われます。 また、重複 NG で世界で唯一でないといけない S3 バケット名を簡単に一意にする方法としても末尾に AWS アカウントを付けるのはよくありますよね。

一応 Ref でも書けますが、ダサいです。

((前半は省略))

Resources:
  MyBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName:
        Fn::Join:
          - "-"
          - - !Ref MyBucketName
            - !Ref AWS::AccountId

Fn::Join はその名の通り結合です。これ複雑ですよね。いつも間違えます。

■参考 Fn::Join

本当に本題

語りたい欲が出てしまい、かなり脇道に逸れましたがようやく本題です。

状況設定

まず具体的なユースケースを以下に列挙します。

  • S3 バケットのテンプレートを作る
  • 本番環境(1つ)、開発環境(複数)をひとつの CFn テンプレートで定義する
  • 本番環境のバケット名は MyBucket とする
  • 開発環境のバケット名は MyBucket-012345678901 とする
    • ※ 012345678901 は開発者の AWS アカウント ID

答え

というわけで、結論から言うと答えはこうなります。

AWSTemplateFormatVersion: 2010-09-09

Parameters:
  Env:
    Type: String
    Default: dev
    AllowedValues:
      - dev
      - prod
    Description: "Enter an environment name"

Conditions:
  IsProd: !Equals [!Ref Env, prod]

Resources:
  MyBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub
        - "MyBucket${Suffix}"
        - Suffix : !If [IsProd, "", !Sub "-${AWS::AccountId}"]

パブリックアクセスのブロック設定や暗号化設定(※)も大事ですがここでは見かけ上省きます。

(※) ↓ を追加しよう!

PublicAccessBlockConfiguration:
  BlockPublicAcls: true
  BlockPublicPolicy: true
  IgnorePublicAcls: true
  RestrictPublicBuckets: true
BucketEncryption:
  ServerSideEncryptionConfiguration:
    - ServerSideEncryptionByDefault:
        SSEAlgorithm: AES256

解説

では解説です。

まず Parameters セクションです。 半分おまけ程度ですが、パラメータ Env として利用できる値を devprod に限定しています。 マネジメントコンソール上からスタックを作成するときはドロップダウンリストとして表示されます。便利!

Parameters:
  Env:
    Type: String
    Default: dev
    AllowedValues:
      - dev
      - prod
    Description: "Enter an environment name"

次に Conditions セクションです。

Conditions:
  IsProd: !Equals [!Ref Env, prod]

■参考 条件

ここでは組み込み関数 Fn::Equals を使っていますが、これは後ろに来る2つの値が同じであれば true を、そうでない場合は false を返します。

要するに !Ref Envprod が等しいかどうかを見ているわけでして、無論 Env は直前のパラメータにして指定した dev もしくは prod が来るやつです。

まとめるとこうです。

  • パラメータ Envdev とする → IsProdfalse
  • パラメータ Envprod とする → IsProdtrue

IsProd という名前もしっくりきますね。

最後に Resources セクションです。

Resources:
  MyBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub
        - "MyBucket${Suffix}"
        - Suffix : !If [IsProd, "", !Sub "-${AWS::AccountId}"]

はい、ここがポイントです。 !Sub の後ろがリストになっています。

リストの一つ目の要素を見ると MyBucket${Suffix} となっており、唐突にパラメータ ${Suffix} が使われています。

じゃあこれはどこで定義したのよ、というとそれは二つ目の要素、すなわちすぐ下の行にあります。

Suffix: !If [IsProd, "", !Sub "-${AWS::AccountId}"]

組み込み関数 Fn::If がいますが、使い方は何のことはない、三項演算子そのものです。

!If [条件(truefalse になる変数), true の場合に採用する値, false の場合に採用する値]

■参考 Fn::If

今回の場合ですと、「 IsProd が true なら "" (空文字)を、 false なら -012345678901 」となります。

そして、この結果がパラメータ Suffix の値として MyBucket${Suffix} に代入されるわけですね。

ドキュメントではこの書き方をマッピングと呼んでいるみたいです。わかるようなわからんような…。

何はともあれ、状況設定に挙げた項目 ↓ がすべて満たされたのでした。

  • S3 バケットのテンプレートを作る
  • 本番環境(1つ)、開発環境(複数)をひとつの CFn テンプレートで定義する
  • 本番環境のバケット名は MyBucket とする
  • 開発環境のバケット名は MyBucket-012345678901 とする
    • ※ 012345678901 は開発者の AWS アカウント ID

まとめ

CFn も意外と(?)柔軟にイイ感じに書ける。