CloudFormationの条件関数を利用する
こんにちは、藤本です。 先日、CloudFormationの条件関数を初めて利用したので、どのようなことが出来るかメモ代わりにブログを書きます。
概要
CloudFormationのテンプレートはJSONで記述します。パラメータ宣言、設定値のマッピング宣言、リソース構成、アウトプットを全てJSONで記述します。ただ全てを固定値でしか記述できないわけではなく、JSON形式の組み込み関数が用意されており、組み込み関数を利用することでロジカルに記述することが可能です。今回は条件関数を色々と環境差分を吸収する書き分けを行いました。
条件関数と関連設定
まず簡単に利用可能な条件関数と関連するプロパティをご紹介します。
Fn::If
条件句といえば、if
ですね。条件式、条件式がtrue
の場合の処理、条件式がfalse
の場合の処理をそれぞれ指定します。elif
、elseif
はありません。その場合、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
を選択した場合のみインスタンスを作成し、dev
、stg
を選択した場合はインスタンスを作成しません。
リソースプロパティの書き分け
条件式によって、リソースプロパティの値を書き分けます。複数の条件値がある場合はネストします。
例 : 本番環境、ステージング環境、開発環境で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::If
、AWS:NoValue
を利用して、設定ファイルの内容や、作成の有無を書き分けることができます。もちろん、files
キーだけでなく、commands
、packages
、settings
などでも利用可能です。
例 : 名前解決先を書き分ける
"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-init
はconfigSets
を指定することでキー(設定内容)を自由に組み合わせることができます。
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構成にするなどして可読性向上に努めましょう。