複数環境のCloudFormationテンプレートをまとめたいときに【ネストされたスタック vs Mappings】
はじめに
こんにちは、株式会社SHIFT ITソリューション部の三好です。
みなさん、CloudFormation使ってますか(再)。
前回記事ではここで困ったCloudFormation その1と銘打ち、オムニバス形式でCloudFormationの困りごとをつらつらと書き留めていました。流れとしては「その2」を書くのが筋なのですが、この形式だといかんせんテーマに一貫性がなさすぎる……
そこで、今回は、「同じ役割のリソースを複数環境で作成する際、テンプレートをまとめるのに最適な方法は?」 というテーマに的を絞り、書いていくことにします。具体的には、「ネストされたスタック」 および、「Mappings」 という二つの方式を取り上げ、それぞれの利点と欠点を比較する形で進めていきます。
題材
今回は、同じ役割のEC2インスタンスを複数の環境に構築する例を題材とします。(正直、EC2インスタンス単品をCloudFormationで立てる運用例はあまり存在しないと思いますが、今回は説明をよりシンプルにするため。)
まずは、環境ごとにそれぞれ異なるテンプレートファイルを用いて構築する場合のファイルを見てみましょう。
環境A
Resources:
myInstance:
Type: 'AWS::EC2::Instance'
Properties:
ImageId: ami-0aaaaaaaaaaaaaaaa
InstanceType: t2.micro
KeyName: my-key-pair
SecurityGroupIds:
- sg-0aaaaaaaaaaaaaaaa
環境B
Resources:
myInstance:
Type: 'AWS::EC2::Instance'
Properties:
ImageId: ami-0bbbbbbbbbbbbbbbb
InstanceType: t2.micro
KeyName: my-key-pair
SecurityGroupIds:
- sg-0bbbbbbbbbbbbbbbb
上記2つのテンプレートで異なる値を持つのは、
ImageId
SecurityGroupIds
の2項目です。
この差分を、ネストされたスタックの方式、およびMappingsを用いた方式でそれぞれどのように吸収していくのか、見ていきましょう。
ネストされたスタック(親スタック & 子スタック)
「ネストされたスタック」とは、スタックによって作成されたスタックのことです。
……といってもわかりづらいですね。 まずはテンプレートを見てみましょう。
親テンプレート(環境A)
Parameters:
BucketName:
Type: String
Description: The S3 Bucket Name where template file is put.
TemplateName:
Type: String
Description: The CFn template file name.
Resource:
myInstanceStack:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.ap-northeast-1.amazonaws.com/${BucketName}/${TemplateName}
Parameters:
ImageId: ami-0aaaaaaaaaaaaaaaa
SecurityGroupIds: sg-0aaaaaaaaaaaaaaaa
親テンプレート(環境B)
Parameters:
BucketName:
Type: String
Description: The S3 Bucket Name where template file is put.
TemplateName:
Type: String
Description: The CFn template file name.
Resource:
myInstanceStack:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.ap-northeast-1.amazonaws.com/${BucketName}/${TemplateName}
Parameters:
ImageId: ami-0bbbbbbbbbbbbbbbb
SecurityGroupIds: sg-0bbbbbbbbbbbbbbbb
子テンプレート(共通)
Parameters:
ImageId:
Type: String
Description: AMI Id (different between environments)
SecurityGroupIds:
Type: String
Description: SecurityGroup Id (different between environments)
Resources:
myInstance:
Type: 'AWS::EC2::Instance'
Properties:
ImageId: !Ref ImageId
InstanceType: t2.micro
KeyName: my-key-pair
SecurityGroupIds:
- !Ref SecurityGroupIds
親テンプレートは、環境によって異なる変数の値と、子テンプレートの格納されたS3バケットの値のみを持っています。Resourcesセクションでは作成対象のリソースとしてStackのみが定義されており、その内部のParametersで各環境のファイルごとに異なる変数の値が定義されています。
一方、子テンプレートは今回作成したいリソースであるインスタンスの情報を持っていますが、環境により異なる変数の部分の値だけが空っぽになっています。変数の入れ物の名前はParametersのセクションで定義し、Resourcesセクション内でその名前をRef関数により引っ張ってくるという仕組みになっています。
まとめると、環境依存部分を親に、共通部分を子に分けて持たせている、というのがこの方式の特徴です。環境依存部分は当然環境によって異なる値を持つため、環境の数だけ親テンプレートが必要になります。
実際にリソースを作成するときの挙動は以下のようになります。
親テンプレートを用いてCFnスタックを作成する(=親スタックの作成)。
作成された親スタックは、リソースとしてもう一つスタックを作成する(=子スタックの作成)。
作成された子スタックは、親スタックから受け取った値を用いて実際のリソースを作成する(今回の場合、インスタンスの作成)。
表題の「ネストされたスタック」というのは、ずばり子スタックのことです。CloudFormationのスタック一覧画面で"ネストされたスタック"と表示があるスタックは子スタックであり、必ず別の親スタックによって作成されたものであるということです。(この親子スタックを用いた方式を端的に表す用語がないため、分かりづらいタイトルとなってしまいました。。。)
さて、この親子方式、環境ごとにテンプレートを用意する元の方式に比べ、テンプレートの数もスタックの数も増えており、一見するとむしろ管理が複雑になったように感じます。そこまでしてテンプレートを分離すると、何がうれしいのでしょうか?
答えは、「共通の項目に変更が入った際、修正する箇所が一か所で済む」 ということです。例えば、全環境でインスタンスタイプをt2.microからt3.mediumにスケールアップすることになったとします。環境ごとに完全に別個のテンプレートで管理していた場合、すべてのテンプレートにその修正箇所を反映させなければなりません。半面、親子方式の場合、共通項目であるインスタンスタイプは子テンプレートによって管理されているため、子テンプレートのみ書き換えればよいということになります。
今回の例は単純化のため環境はA、Bの2つのみにしており、恩恵を感じにくくなっていますが、実際にはC、D、E、……といくつも環境があるシチュエーションのほうが多いはず。環境の数に比例してちょっとした修正の手間や、その漏れ・ミスの発生率も上がることを考えれば、なるべく共通項目は子テンプレートの側でまとめて管理したほうが吉と言えそうです。
Mappings
Mappingsは、同じの変数名(Key)に対して環境ごとに異なる値(Value)を持つ場合、環境ごとで変数の値のデータセットを作っておくことで一括で代入することができるような機能です。
……やはり言葉で言ってもわかりにくいので、テンプレートを見てみましょう。
環境A・B共通
Parameters:
Environment:
Type: String
AllowedValues:
- EnvA
- EnvB
Mappings:
EnvMapping:
EnvA:
ImageId: ami-0aaaaaaaaaaaaaaaa
SecurityGroupIds: sg-0aaaaaaaaaaaaaaaa
EnvB:
ImageId: ami-0bbbbbbbbbbbbbbbb
SecurityGroupIds: sg-0bbbbbbbbbbbbbbbb
Resources:
myInstance:
Type: 'AWS::EC2::Instance'
Properties:
ImageId: !FindInMap [EnvMapping, !Ref Environment, ImageId]
InstanceType: t2.micro
KeyName: my-key-pair
SecurityGroupIds:
- !FindInMap [EnvMapping, !Ref Environment, SecurityGroupIds]
ParametersやResourcesのほかに、新しくMappingsというセクションが追加されています。Mappingsセクション内では、各環境のごとに異なる値を持つ変数(今回はImageIdとSecurityGroupIds)について、それぞれ値を定義しています。
ResourcesセクションではFindInMap関数を用い、以下の形式で値を引っ張ってきます。
!FindInMap [MapName, TopLevelKey, SecondLevelKey]
以下、各項目の簡易な解説。
「MapName」は、その名の通りMappingsセクションで定義したMapの名前です。今回の例ではEnvMappingが入ります。
「TopLevelKey」には、Parameters:で設定した環境を指定する変数を入力します。今回の例ではAllowedValuesであらかじめ入力できる値をEnvAおよびEnvBのみに絞っておくことで、打ち間違いを防いでいます。
「SecondLevelKey」には、Mappings内で羅列している項目名のうち、引用したい変数の名前を入れればOKです。AMIのIDの箇所ではImageIdを、セキュリティグループIDの箇所ではSecurityGroupIdsが該当します。
Mappingsの良いところは分かりやすいですね。すべての環境のデータを一つのテンプレートにまとめることができるという点です。環境の数が増えたとしても、新しいEnvのフィールド上記のテンプレートに付け加えていけばよいだけなので、相変わらず1テンプレートで管理が可能です。
欠点があるとすれば、Mappingsというセクションが挿入されることによりテンプレートの行数が増え、全体として可視性が下がるという点です。環境が増えるだけ、またMappingsで管理する変数が増えるだけ行数が純増するため、単一の環境について見たい際のノイズになります。また、すべての情報が1テンプレートに収まっている都合上、情報過多感は否めないかもしれません。
比較
ファイル管理のしやすさ:ネストされたスタック方式でもMappings方式でも、環境間で共通の部分を同じテンプレート内にまとめらています。ファイル数の観点でMappings方式にやや分があるかも。
ファイル数:Mappings方式は1テンプレートで済むため非常に楽です。ネストされたスタック方式では親テンプレート×環境数と、子テンプレートが必要になります。
環境間差異の見やすさ:ネストされたスタック方式のほうが圧倒的に見やすいです。親テンプレートどうしをdiffすれば一発で差分が見て取れます。
おまけ(ネストとMappingsの合体版)
実はネストとMappingsの合わせ技でテンプレートを組むこともできます。
親テンプレート(環境A・B共通)
Parameters:
BucketName:
Type: String
Description: The S3 Bucket Name where template file is put.
TemplateName:
Type: String
Description: The CFn template file name.
Environment:
Type: String
AllowedValues:
- EnvA
- EnvB
Mappings:
EnvMapping:
EnvA:
ImageId: ami-0aaaaaaaaaaaaaaaa
SecurityGroupIds: sg-0aaaaaaaaaaaaaaaa
EnvB:
ImageId: ami-0bbbbbbbbbbbbbbbb
SecurityGroupIds: sg-0bbbbbbbbbbbbbbbb
Resource:
myInstanceStack:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.ap-northeast-1.amazonaws.com/${BucketName}/${TemplateName}
Parameters:
ImageId: !FindInMap [EnvMapping, !Ref Environment, ImageId]
SecurityGroupIds: !FindInMap [EnvMapping, !Ref Environment, SecurityGroupIds]
子テンプレート
Parameters:
ImageId:
Type: String
Description: AMI Id (different between environments)
SecurityGroupIds:
Type: String
Description: SecurityGroup Id (different between environments)
Resources:
myInstance:
Type: 'AWS::EC2::Instance'
Properties:
ImageId: !Ref ImageId
InstanceType: t2.micro
KeyName: my-key-pair
SecurityGroupIds:
- !Ref SecurityGroupIds
上記のネストされたスタック方式の子テンプレートはそのままに、親テンプレートをMappingsを用いることで1つにまとめています。
環境の数だけあった親テンプレートが1テンプレートに収まったことで、ネストされたスタック方式の問題であったファイル数の問題を解決しています。また、リソースの具体的な記述や共通部分については子テンプレートに括り出しているため、Mappings方式に比べテンプレート内の情報過多感も緩和できているかも。
総括
今回はネストされたスタックの方式と、Mappingsを用いた方式について見てきました。どちらの方式が適しているかは、環境や変数の数といった要素のほかに、テンプレートファイルの管理方法・レビュー方法など、プロジェクト内のルールによっても適した方法は変わってくると思います。
たとえば、検証環境→ステージング環境→本番環境と順々にリソースを作成していくルールがあるプロジェクトの場合。各環境に適用する前に、直前に適用した環境との差分を確認するレビューが必須なのであれば、親テンプレートどうしの比較で差分が見れるネストされたスタック方式のほうが圧倒的に使いやすいと思います。 あるいは、ドキュメント管理の方針が、とにかくファイル数は少なく! というものなのであれば、Mappings方式を重宝することになるでしょう。
また、ここまで長々と書いておいて身も蓋もない話ですが、今回の題材程度の条件(2環境、環境差分となる変数は2つのみ)であれば、わざわざテンプレートをまとめるまでもないと思います。各テンプレート内で1環境のリソースに必要な情報が過不足なく完結しているという観点では、一番初めに挙げたテンプレートが最も優れているので。
月並みな締め方となってしまいますが、「これがベスト!」「この方法が絶対!」 ということはありません。プロジェクトで置かれた状況と相談して使い分けてみてください。
参考ドキュメント
お問合せはお気軽に
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/
PHOTO:UnsplashのOskar Yildiz