AWS
CloudFormation
mustache

CloudFormation テンプレートを Mustache で展開しループ処理する

概要

CloudFormation を使う際、ループを使いたい時があったので、Mustache テンプレートエンジンを使って対処しました。

わりと汎用的に使える方法だと思うので、紹介したいと思います。

課題

CloudFormation を使っていて、ループを使いたくなる時があります。
例えば Lambda 関数に対して CloudWatch アラームを設定したいが、Lambda 関数がたくさんあって同じ記述を繰り返さなければならない、というようなケースです。

monitor.yaml
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 でループを実現する方法は、探すといくつかあるようです。

しかし、これらの方法は自分のプロジェクトにとってはちょっと「ゴツい」と感じました。学習コストが高い or システムが複雑になり、チームで採用するにはハードルが高いな、と。

Mustache テンプレートエンジンを使う

そこで考えた方法が、ありふれたテンプレートエンジンを使うというものです。

Mustache テンプレートエンジンを使うと、先ほどの例は以下のように記述できます。

monitor.yaml
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 ファイルに書いておきます。

config-dev.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}}}

多用しすぎない

テンプレートエンジンを使えるようになると、CloudFormation の YAML記述の繰り返しになっている部分を全てテンプレートで処理したくなってしまうかもしれません。

しかしあまりテンプレートエンジンを多用すると、記述が複雑化し、逆に見通しが悪くなってしまいます。

テンプレートエンジンはどうしても使いたいところに必要最小限使うようにし、CloudFormation の YAML記述には多少の冗長性は許すようにしたほうが、メンテナンスはしやすくなると思います。