AWS CloudFormationでCloudFormationテンプレートのマクロを構築しよう

# はじめに
AWS CloudFormationを利用して、CloudFormationマクロを構築するテンプレートのサンプルです。

テンプレートの概要が分からない場合は、[はじめてのAWS CloudFormationテンプレートを理解する](https://qiita.com/miyabiz/items/eb7b0fa1675b5199b9b7)を参考にしてください。

コードは[GitHub](https://github.com/miyabiz-room/CloudFormation-Template/tree/macro-01)にもあります。

今回は、akane というシステムの all 環境を想定しています。
同じ構成で違う環境を作成する場合は、{環境名}-parameters.jsonを別途作成します。

```text:ディレクトリ構成
akane (システム)
  └─ macro (スタック)
      ├─ macro.yml (CFnテンプレート)
      └─ all-parameters.json (all 環境のパラメータ)
```

# マクロとは
- [マクロ](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-macros.html)を使用すると、検索して置換操作のような単純なアクションからテンプレート全体の広範な変換まで、テンプレートに対してカスタム処理を実行できるようになります。

# AWS リソース構築内容
  1. macroスタック
      - Lambda (マクロ)
      - Lambdaのアクセス許可 (CloudFormationが利用するため)
      - IAMロール (Lambda用)

# 実行環境の準備
[AWS CloudFormationを動かすためのAWS CLIの設定](https://qiita.com/miyabiz/items/fed11796f0ea2b7608f4)を参考にしてください。

# AWS リソース構築手順
1. 下記を実行してスタックを作成

    ```
    ./create_stacks.sh
    ```

2. 下記を実行してスタックを削除

    ```
    ./delete_stacks.sh
    ```

# 構築テンプレート
**1. macroスタック**
 
```yaml:macro.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Macro For Akane

# Metadata:

Parameters:
  SystemName:
    Type: String
    AllowedPattern: '[a-zA-Z0-9-]*'
  EnvType:
    Description: Environment type.
    Type: String
    AllowedValues: [all, dev, stg, prod]
    ConstraintDescription: must specify all, dev, stg, or prod.

# Mappings

# Conditions

# Transform

Resources:
  # ロール作成
  akaneRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: 
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Description: !Sub
        - ${SystemName}-${EnvType}-role-macro
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Path: /
      RoleName: !Sub
        - ${SystemName}-${EnvType}-role-macro
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - logs:*
                Resource: arn:aws:logs:*:*:*
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-role-macro
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  # Lambda作成
  akaneLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import traceback
          def handler(event, context):
              response = {
                  "requestId": event["requestId"],
                  "status": "success"
              }
              try:
                  operation = event["params"]["Operation"]
                  input = event["params"]["InputString"]
                  no_param_string_funcs = ["Upper", "Lower", "Capitalize", "Title", "SwapCase"]
                  if operation in no_param_string_funcs:
                      response["fragment"] = getattr(input, operation.lower())()
                  elif operation == "Strip":
                      chars = None
                      if "Chars" in event["params"]:
                          chars = event["params"]["Chars"]
                      response["fragment"] = input.strip(chars)
                  elif operation == "Replace":
                      old = event["params"]["Old"]
                      new = event["params"]["New"]
                      response["fragment"] = input.replace(old, new)
                  elif operation == "MaxLength":
                      length = int(event["params"]["Length"])
                      if len(input) <= length:
                          response["fragment"] = input
                      elif "StripFrom" in event["params"]:
                          if event["params"]["StripFrom"] == "Left":
                              response["fragment"] = input[len(input)-length:]
                          elif event["params"]["StripFrom"] != "Right":
                              response["status"] = "failure"
                      else:
                          response["fragment"] = input[:length]
                  else:
                      response["status"] = "failure"
              except Exception as e:
                  traceback.print_exc()
                  response["status"] = "failure"
                  response["errorMessage"] = str(e)
              return response
      FunctionName: !Sub
        - ${SystemName}-${EnvType}-macro-lambda
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Handler: index.handler
      Runtime: python3.7
      Role: !GetAtt akaneRole.Arn
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-macro-lambda
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  # LambdaPermission作成
  akaneLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref akaneLambdaFunction
      Principal: cloudformation.amazonaws.com
  # Macro作成
  akaneCloudFormationMacro:
    Type: AWS::CloudFormation::Macro
    Properties:
      Name: convertString
      Description: Provides various string processing functions
      FunctionName: !Ref akaneLambdaFunction

# Outputs
```

```json:all-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "all"
        }
    ],
    "Capabilities": [
        "CAPABILITY_NAMED_IAM"
    ]
}
```

**2. 実行ファイル**

```sh:create_stacks.sh
#!/bin/sh
SYSTEM_NAME=akane
ENV_TYPE=all

create_stack () {
    STACK_NAME=$1
    aws cloudformation create-stack \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME} \
    --template-body file://./${SYSTEM_NAME}/${STACK_NAME}/${STACK_NAME}.yml \
    --cli-input-json file://./${SYSTEM_NAME}/${STACK_NAME}/${ENV_TYPE}-parameters.json

    aws cloudformation wait stack-create-complete \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}
}

create_stack macro
```

```sh:delete_stacks.sh
#!/bin/sh
SYSTEM_NAME=akane
ENV_TYPE=all

delete_stack () {
    STACK_NAME=$1
    aws cloudformation delete-stack \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}

    aws cloudformation wait stack-delete-complete \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}
}

delete_stack macro
```

# マクロの利用方法
1. CloudFormationのテンプレート内で、組み込み関数```Transform```を利用して参照する

    ```yml:テンプレート.yml
    Fn::Transform:
      - Name: convertString
        Parameters:
          InputString: 文字列
          Operation: Upper (パラメータ)
    ```

2. 利用できるパタメータ

- Upper
- Lower
- Capitalize
- Title
- SwapCase