CloudFormationの条件関数を利用する

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

こんにちは、藤本です。
先日、CloudFormationの条件関数を初めて利用したので、どのようなことが出来るかメモ代わりにブログを書きます。

概要

CloudFormationのテンプレートはJSONで記述します。パラメータ宣言、設定値のマッピング宣言、リソース構成、アウトプットを全てJSONで記述します。ただ全てを固定値でしか記述できないわけではなく、JSON形式の組み込み関数が用意されており、組み込み関数を利用することでロジカルに記述することが可能です。今回は条件関数を色々と環境差分を吸収する書き分けを行いました。

条件関数と関連設定

まず簡単に利用可能な条件関数と関連するプロパティをご紹介します。

Fn::If

条件句といえば、ifですね。条件式、条件式がtrueの場合の処理、条件式がfalseの場合の処理をそれぞれ指定します。elifelseifはありません。その場合、Fn::Ifをネストするしかありません。CloudFormationでそんなに頑張るなよ、という意図なのでしょうか。。

{ "Fn::If" : [ "条件式", "条件式の結果が true 時の値", "条件式の結果が false 時の値"] }

Fn::Equals

条件式に利用します。2つの値が一致すればtrue、不一致であればfalseを返します。

{ "Fn::Equals" : [ "比較値1", "比較値2" ] }

Fn::Not

条件式の結果を反転します。条件式の結果がtrueであればfalseを、falseであればtrueを返します。

{ "Fn::Not" : ["条件式"] }

Fn::And

複数の条件式が全てtrueの場合のみtrueを返します。

{ "Fn::And" : [ "条件式1", "条件式2", ... ]

Fn::Or

複数の条件式のいずれかがtrueの場合にtrueを返します。

{ "Fn::Or" : [ "条件式1", "条件式2", ... ]

Conditionsセクション

セクションの一つです。Fn::Ifを利用して条件式を書くわけですが、同じ条件式を何回も書くのは面倒ですし、テンプレートの可読性を落としてしまいます。その場合、Conditionsセクションに条件式をLogical IDにマッピングして定義することで、条件式をLogical IDで利用することができます。

"Conditions": { "条件式" }

AWS::NoValue

リソースプロパティの値に利用します。リソースプロパティの削除を行います。例えば、Fn::Ifと組み合わせることである条件に合致、もしくは合致しない場合にリソースプロパティの指定を削除することができます。

{ "Ref" : "AWS::NoValue" }

Condition属性

リソース属性の一つです。リソースを作成するかどうか判断します。条件式の結果がtrueの場合、リソースを作成し、falseの場合、リソースを作成しません。

"Resources": {
  "resouce": {
    "Condition": "条件式"
:

ユースケース別利用方法

ここからは上記で紹介した条件関数と関連設定を利用して、実際にどのような制御ができるのかユースケースを例にご紹介します。

リソース作成の有無

各種リソースにはリソースプロパティにConditionが用意されています。Conditionの条件式がtrueの場合、リソースを作成し、falseの場合、リソースを作成しません。

例 : 本番環境のみインスタンスを作成する
  "Parameters": {
    "Env": {
      "Description": "System Environment",
      "AllowedValues": ["dev", "stg", "prd"],
      "Type": "String"
    }
  },
  "Conditions": {
    "IsProduction": { "Fn::Equals" : [ { "Ref" : "Env" }, "prd" ] }
  },
  "Resources": {
    "Instance": {
      "Type": "AWS::EC2::Instance",
      "Condition": "IsProduction",
:

パラメータに環境を選択できるようにしておきます。
パラメータの値が本番環境かどうか判定する条件式を定義します。
EC2インスタンスのConditionに定義した条件のキーを指定します。

この設定により環境にprdを選択した場合のみインスタンスを作成し、devstgを選択した場合はインスタンスを作成しません。

リソースプロパティの書き分け

条件式によって、リソースプロパティの値を書き分けます。複数の条件値がある場合はネストします。

例 : 本番環境、ステージング環境、開発環境でEC2インスタンスのインスタンスタイプを分ける
  "Parameters": {
    "Env": {
      "Description": "System Environment",
      "AllowedValues": ["dev", "stg", "prd"],
      "Type": "String"
    }
  },
  "Conditions": {
    "IsProduction": { "Fn::Equals" : [ { "Ref" : "Env" }, "prd" ] },
    "IsStaging":    { "Fn::Equals" : [ { "Ref" : "Env" }, "stg" ] }
  },
  "Resources": {
    "Instance": {
      "Type": "AWS::EC2::Instance",
      "InstanceType": { "Fn::If": [ 
        "IsProduction", 
        "m4.xlarge", 
        { "Fn::If": [
          "IsStaging",
          "m3.medium"
          "t2.micro"
        ] }
      ] },
:

ちなみにこの例だと、Mappingsを利用した方がいいです。

リソースプロパティ指定の有無

AWS::NoValueを利用して、条件式によって不要なリソースプロパティは削除します。

例 : 本番環境のみEC2インスタンスにインスタンスプロファイルを適用する
  "Parameters": {
    "Env": {
      "Description": "System Environment",
      "AllowedValues": ["dev", "stg", "prd"],
      "Type": "String"
    }
  },
  "Conditions": {
    "IsProduction": { "Fn::Equals" : [ { "Ref" : "Env" }, "prd" ] }
  },
  "Resources": {
    "Instance": {
      "Type": "AWS::EC2::Instance",
      "IamInstanceProfile": { "Fn::If": [ 
        "IsProduction", 
        "iamprofile", 
        { "Ref" : "AWS::NoValue" }
      ] },
:

cfn-initの利用

今回は CloudFormation Stack のみによりミドルウェアレイヤまで構築することが目的だったため、cfn-init内でゴリゴリに書きました。cfn-init内でも上記同様の記述が可能です。

設定ファイルの書き分け

Fn::IfAWS:NoValueを利用して、設定ファイルの内容や、作成の有無を書き分けることができます。もちろん、filesキーだけでなく、commandspackagessettingsなどでも利用可能です。

例 : 名前解決先を書き分ける
  "Parameters": {
    "Env": {
      "Description": "System Environment",
      "AllowedValues": ["dev", "stg", "prd"],
      "Type": "String"
    }
  },
  "Conditions": {
    "IsProduction": { "Fn::Equals" : [ { "Ref" : "Env" }, "prd" ] }
  },
  "Resources": {
    "Instance": {
      "Type": "AWS::EC2::Instance",
      "Metadata": {
        "AWS::CloudFormation::Init": {
          "configSets": {
            "default" : [ "initialize", "setup" ]
          },
          "initialize": {
:
          },
          "setup": {
            "files": {
              "/etc/dnsmasql.conf": {
                "content": {"Fn::Join": ["", [
                  {"Fn::If" : [ "IsProduction", "#", ""]},
                  "resolv-file=/etc/prd-resolv.conf", "\n",
:
                ]]},
                "mode"  : "000644",
                "owner": "root",
                "group": "root"
              },
              "/etc/prd-resolv.conf": { "Fn::If": [
                "IsProduction",
                {
                  "content": "nameserver xxx.xxx.xxx.xxx\nnameserver xx.xx.xx.xx",
                  "mode"  : "000644",
                  "owner": "root",
                  "group": "root"
                },
                { "Ref" : "AWS::NoValue" }
              ] }
:

configSetsの選択

cfn-initconfigSetsを指定することでキー(設定内容)を自由に組み合わせることができます。
2択ぐらいであれば、条件関数を利用できそうですが、選択肢が増えてくると可読性が悪くなります。今回のテーマの条件関数とは関係なくなりますが、cfn-initの実行パラメータで渡すのが良さそうです。

例 : 本番環境のみCloudWatch Logsをセットアップする
  "Parameters": {
    "Env": {
      "Description": "System Environment",
      "AllowedValues": ["dev", "stg", "prd"],
      "Type": "String"
    }
  },

  "Resources": {
    "Instance": {
      "Type": "AWS::EC2::Instance",
      "Metadata": {
        "AWS::CloudFormation::Init": {
          "configSets": {
            "dev" : [ "initialize" ],
            "stg" : [ { "ConfigSet" : "dev" }, "if_test" ],
            "prd" : [ { "ConfigSet" : "stg" }, "install_cloudwatchlogs" ]
          },
          "initialize": {
:
          },
          "setup": {
:
          },
          "install_cloudwatchlogs": {
:
          }
        }
      },
      "UserData": {"Fn::Base64": {"Fn::Join": ["", [
:
        "/usr/local/bin/cfn-init -v ",
          "         --stack ", {"Ref": "AWS::StackName"},
          "         --resource Instance ",
          "         --configsets ", {"Ref": "Env"}, 
          "         --region ", {"Ref": "AWS::Region"}, "\n",
:
        ] ]} },

まとめ

いかがでしたでしょうか?

CloudFormationはJSON形式ですが、マッピング、条件関数、カスタムリソースを利用することで環境差分を吸収する記述を行うことが可能です。とは言え、やり過ぎはただでさえ可読性が良くないJSON(個人的見解)を複雑にすることで更に可読性を下げてしまいがちなのでご注意ください。Nested構成にするなどして可読性向上に努めましょう。