AWS CloudFormation のマクロ機能を使ってみた

# 概要

[AWS CloudFormation Macros について](https://aws.amazon.com/jp/about-aws/whats-new/2018/09/introducing-aws-cloudformation-macros/)
ということで、先日発表されました AWS CloudFormation のマクロを早速試してみました。

# マクロとは？

> AWS CloudFormation Macros は、CloudFormation テンプレートで、検索や置換などの単純な操作からテンプレート全体の変換までカスタム処理を実行できます。

> 以前は、AWS::Include 変換および AWS::Serverless 変換を使用して、CloudFormation によってホストされたテンプレートを処理できました。今は、CloudFormation Macros を使用して独自のカスタム変換を作成できるようになりました。

つまり、CloudFormation のテンプレートの実行時に、独自の処理を行うことができるもののようです。
実際にどのようなことができるのかというと、下記のような例がありました。


- テンプレート内に  python コードを記述
- リソース定義表現を短くする
- カスタム CloudWatch metrics の出力
- 文字列を関数としてテンプレート内で使う

([aws-cloudformation-templates/aws/services/CloudFormation/MacrosExamples at master · awslabs/aws-cloudformation-templates · GitHub](https://github.com/awslabs/aws-cloudformation-templates/tree/master/aws/services/CloudFormation/MacrosExamples/) より)

なにやらいろいろできそうな気がします。
自分は、１回の記述で同じリソースを複数個作るマクロを作ってみました。

# マクロを作る

## マクロを知る

まずはどのようにマクロが動くかを調べます。
[AWS CloudFormation を AWS Lambda によるマクロで拡張する](https://aws.amazon.com/jp/blogs/news/cloudformation-macros/) や [Using AWS CloudFormation Macros to Perform Custom Processing on Templates](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-macros.html) によると、

1. マクロの実態は Lambda 関数
2. その関数の ARN を `AWS::CloudFormation::Macro` リソースに指定することでスタックが作られる際に関数が実行される

とのことです。
この時、Lambda 関数が受け取るペイロードは以下のようです。

```json
{
    "region" : "us-east-1", 
    "accountId" : "$ACCOUNT_ID", 
    "fragment" : { ... }, 
    "transformId" : "$TRANSFORM_ID", 
    "params" : { ... }, 
    "requestId" : "$REQUEST_ID",
    "templateParameterValues" : { ... } 
}
```

この中で一番大事なパラメータが `fragment` で、この中に処理するテンプレートの内容が含まれます。

Lambda 関数のレスポンスはというと、

```json
{
    "requestId" : "$REQUEST_ID", 
    "status" : "$STATUS", 
    "fragment" : { ... } 
}
```

こちらの `fragment` が JSON形式の CloudFormation テンプレートとして適用されるようです。

## マクロとなる Lambda 関数の作成

まずはマクロの本体となる Lambda 関数を作ります。
今回は、

```yaml
AWSTemplateFormatVersion: "2010-09-09"

Resources:
  Resource:
    Type: AWS::SNS::Topic
    Properties:
      DisplayName: test topic
      TopicName: TestTopic
```

↑こんな感じのテンプレートの１回の実行で、指定した回数分の SNS トピックが作成できる
という感じのマクロを作ります。

テンプレート＆使いかたをはじめに出しておくと、

```yaml:template.yml
AWSTemplateFormatVersion: "2010-09-09"
Transform: [MultiplexResourceMacro]

Parameters:
  NumberOfRepeats:
    Type: Number
    Default: 1

Resources:
  Resource:
    Type: AWS::SNS::Topic
    Properties:
      DisplayName: test topic
      TopicName: TestTopic
```

このようなテンプレートを実行すると、NumberOfRepeats に指定した個数の SNS トピックが作成できる、といったものです。

Lambda 関数でやることは、Resources 以下をとってきて指定された回数ループし、新しく Resouses
として返すといった内容になります。

実際の処理は以下のような感じです。（Lambda のランタイムは node.js 8.10）

```javascript:index.js
'use strict'

const getPropertyGenerator = (type) => {
  switch (type) {
    case 'AWS::SNS::Topic':
      return createSNSTopic
  }
}

const createSNSTopic = (properties, count) => {
  return {
    DisplayName: properties.DisplayName,
    TopicName: `${properties.TopicName}-${count}`
  }
}

exports.handler = async (event, context, callback) => {
  const numberOfRepeats = event.templateParameterValues.NumberOfRepeats
  const resources = event.fragment.Resources

  const keys = Object.keys(resources)
  if (keys.length > 1) {
    throw Error('Number of resources shouud be 1')
  }

  const resource = resources[keys[0]]

  const resourceType = resource.Type
  const properties = resource.Properties

  for (let i = 0; i < numberOfRepeats; i++) {
    const ret = {
      Type: resourceType,
      Properties: getPropertyGenerator(resourceType)(properties, i)
    }
    resources[`${keys[0]}${i}`] = ret
  }

  delete resources[keys[0]]
  return {
    'requestId': event['requestId'],
    'status': 'success',
    'fragment': event['fragment']
  }
}
```

これをデプロイします。
デプロイに以下の Template を使います。
ここでは、`AWS::Serverless::Function` タイプのリソースを `MultiplexResourceMacro` という名前で作り、Function の ARN を登録しています。

```yaml:template.yml
Transform: AWS::Serverless-2016-10-31
Description: Macro for cloudformation

Resources:
  Function:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./index.js
      Handler: index.handler
      Runtime: nodejs8.10

  Macro:
    Type: AWS::CloudFormation::Macro
    Properties:
      Name: MultiplexResourceMacro
      FunctionName: !GetAtt Function.Arn
```

これを、↓こんな感じでデプロイします。

```sh
aws cloudformation package --template-file macro.yaml --output-template-file cfn-transformed-template.yaml --s3-bucket <S3_BUCKET_NAME>

aws cloudformation deploy --template-file ./cfn-transformed-template.yaml --capabilities CAPABILITY_IAM --stack-name <STACK_NAME>
```

## マクロを使ってみる

といった感じで作ったマクロを使ってみます。
上に書いている SNS 作成テンプレートを指定し以下のコマンドで実行します。

```sh
aws cloudformation deploy --template-file template.yml --stack-name TestSNS --parameter-overrides NumberOfRepeats=5
```

すると、

![20180908-224637.png](https://qiita-image-store.s3.amazonaws.com/0/133201/c8e5f9fb-77e1-4745-d069-517a1eb648a2.png)

↑こんな感じで５個のトピックが無事作成できました。

# 感想

今回作ったマクロはコード見ても分かる通り全く汎用性がないので、改善の余地大ありです。
ただマクロ機能自体は便利なので、うまく使えればかなり色んなことができそうだなと思いました。
パッと思いついたものだと、テンプレート構文的には正しくても運用ポリシー上イケてないテンプレートを弾く、みたいな使い方だと簡単にできそうかなーと感じました。

ちなみに今回作ったマクロ/使ったテンプレートは以下のリポジトリにありますので、よかったら使ってみてください。

- [cloudformation-macro-multiplex-resource](https://github.com/ytakahashi/cloudformation-macro-multiplex-resource)