概要
CloudFormation を使う際、ループを使いたい時があったので、Mustache テンプレートエンジンを使って対処しました。
わりと汎用的に使える方法だと思うので、紹介したいと思います。
課題
CloudFormation を使っていて、ループを使いたくなる時があります。
例えば Lambda 関数に対して CloudWatch アラームを設定したいが、Lambda 関数がたくさんあって同じ記述を繰り返さなければならない、というようなケースです。
Resources:
HogeFunctionAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmActions:
- !Ref AlarmSNSTopic
MetricName: Duration
Namespace: AWS/Lambda
Statistic: Average
Period: 60
EvaluationPeriods: 3
Threshold: 100
ComparisonOperator: GreaterThanThreshold
Dimensions:
- Name: FunctionName
Value: HogeFunction
FugaFunctionAlarm:
Type: AWS::CloudWatch::Alarm
似たような繰り返し..... # つらみ
PiyoFunctionAlarm:
Type: AWS::CloudWatch::Alarm
似たような繰り返し..... # つらみ
既存の方法
Terraform などではリストや変数をサポートしているので、そういったフレームワークを使うのは一つの手です。ただ今回は CloudFormation を使うと決めていたので、CloudFormation でなんとかする方法を考えます。
CloudFormation 単体では、ループを実現する機能はありません(2018年5月現在)。
YAML にはアンカー/エイリアスの機能がありますが、CloudFormation では使えないようです1。
CloudFormation でループを実現する方法は、探すといくつかあるようです。
- Lambda-backed Custom Resourceを利用してCloudFormationにループ処理を擬似的に実装する | Developers.IO
- troposphere: Python の DSL
- SparkleFormation: Ruby の DSL
- Kumogata2: Ruby の DSL
しかし、これらの方法は自分のプロジェクトにとってはちょっと「ゴツい」と感じました。学習コストが高い or システムが複雑になり、チームで採用するにはハードルが高いな、と。
Mustache テンプレートエンジンを使う
そこで考えた方法が、ありふれたテンプレートエンジンを使うというものです。
Mustache テンプレートエンジンを使うと、先ほどの例は以下のように記述できます。
Resources:
{{#functions}}
{{name}}Alarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmActions:
- !Ref AlarmSNSTopic
MetricName: Duration
Namespace: AWS/Lambda
Statistic: Average
Period: {{duration.period}}
EvaluationPeriods: {{duration.evaluationPeriods}}
Threshold: {{duration.threshold}}
ComparisonOperator: GreaterThanThreshold
Dimensions:
- Name: FunctionName
Value: {{name}}
{{/functions}}
{{#functions}}
〜 {{/functions}}
で囲まれた部分がループになります(詳細は Mustache のドキュメント参照)。
テンプレートに埋める値は、別途 YAML ファイルに書いておきます。
functions:
- name: HogeFunction
duration:
period: 60
evaluationPeriods: 3
threshold: 100
- name: FugaFunction
duration:
period: 60
evaluationPeriods: 3
threshold: 200
- name: PiyoFunction
duration:
period: 60
evaluationPeriods: 5
threshold: 150
これだけだと逆に冗長になったような感じを受けるかもしれませんが、実際にはもっと Lambda 関数が多く、またそれぞれの Lambda 関数に対して CloudWatch の設定がいくつもあると想像してください。
これら2つのファイルから、実際に CloudFormation に食わせるテンプレートファイルを生成するには、次のようにします。
npm install mustache --save-dev
npm install yamljs --save-dev
$(npm bin)/mustache <($(npm bin)/yaml2json config-dev.yaml) template.yaml > .template.yaml
実質1行ですね。
データ形式の変換用に yamljs も使っています。
生成された yaml ファイルは通常通り CloudFormation で処理できます。
aws cloudformation deploy --stack-name MyMonitor --template-file .template.yaml
本方法のメリット
CloudFormation テンプレートにおいて繰り返しがなくなり、可変の値だけが別の YAML ファイルに抜き出されるので、見通しが良くなります。
開発環境・本番環境などで設定値を変えたい場合、config-dev.yaml、config-prod.yaml などパラメータファイルを分けることで簡単に対応できます。
Webアプリケーション開発者にはおなじみのテクノロジーを使っているので、挙動が把握しやすく、学習コストが低いです。また導入も簡単です。
なお、あまたあるテンプレートエンジンの中で Mustache を採用したのは、以下の理由からです。
- ループを簡潔に表現できる
- Mustache の npm モジュールには CLI が付いているので、シェルスクリプトから簡単に使える
- 機能が少なく、複雑なことができない(しづらい)ので、テンプレートの複雑化を避けられる
注意事項
エスケープ処理
Mustache によるテンプレートの展開は、YAML の文法とは関係なく行われます。したがって、以下の注意点があります。
- 展開したい値に <>&'" などを含む場合、HTMLエスケープされてしまう
- この場合3重の中括弧でくくる必要があります。
例:AlarmDescription: {{{description}}}
- この場合3重の中括弧でくくる必要があります。
多用しすぎない
テンプレートエンジンを使えるようになると、CloudFormation の YAML記述の繰り返しになっている部分を全てテンプレートで処理したくなってしまうかもしれません。
しかしあまりテンプレートエンジンを多用すると、記述が複雑化し、逆に見通しが悪くなってしまいます。
テンプレートエンジンはどうしても使いたいところに必要最小限使うようにし、CloudFormation の YAML記述には多少の冗長性は許すようにしたほうが、メンテナンスはしやすくなると思います。