入力されたパラメーターの数に応じてCloudFormationテンプレートファイルを動的に変化させてみた

sedとyqでゴリ押すシェルスクリプトを準備したらAWS CloudFormationでもループ処理ができました。
2022.03.08

AWS CloudFormationでループ処理させたいな

こんにちは、のんピ(@non____97)です。

AWS CloudFormationを使って同じようなリソースを作る際にループ処理があれば良いなぁと思ったことはありますか? 私はあります。

ループ処理はマクロやAWS CDKを使えば対応可能ですが、以下のような要件がある場合は難しいです。

  • AWS CloudFormationもしくは、AWS SAMを使う必要がある
  • ループして作成されたリソース毎にパラメーターが異なる

ループ処理をするマクロの例

そこで、完全な力技ですが、シェルスクリプトを使って、入力されたパラメーターの数に応じてCloudFormationテンプレートファイルを動的に変化させてみました。

構成の紹介

ベースとなる環境は以下の記事で紹介したAWS Step FunctionsのCI/CD環境です。

CodeCommit上のリポジトリに変更が発生すると、CodeBuildでAWS SAMを使ってステートマシンやEventBridgeルールなどの各種リソースを作成しています。

構成図

CodeBuildでは以下の処理を行なっています。

  • yqやAWS SAM CLIなどをインストールする
  • 事前にS3バケットにアップロードしていたpre_build_command.shbuild_command.shをダウンロードする
  • pre_build_command.shを実行する
    • リポジトリ上の設定ファイルからCron式やイベントパターン、タグなどの情報を読み込む
    • 事前にS3バケットにアップロードしていたsam-template.ymlをダウンロードする
  • build_command.shを実行する
    • 必要があればAssume Roleをし、一時認証情報を環境変数にセットする
    • AWS SAM CLIで設定ファイルに記入されたリソースをデプロイする

yqはjqのYAML版のようなものです。yqはPython製とGo製の2つがあります。

Python製のyq

Go製のyq

今回はPython製のyqを使用しています。yqの操作方法については上述のリポジトリや以下記事をご覧ください。

Python製のyqの場合、基本的な操作はjqと同じなのでjqのマニュアルもオススメです。

変更前の各種ファイル

sam-template.ymlが使用するCloudFormationのテンプレートファイルです。このファイルは698行もあります。

CloudFormationのテンプレートファイルでは、EventBridgeルールに設定するCron式やイベントパターン、イベントバスをそれぞれ最大5つずつ設定できるようにしています。CloudFormationではループ処理ができないので、それぞれ5つ分のリソースを定義しており、かなりのボリュームがあります。

Cron式で制御するEventBridgeルールの定義

  ScheduledRule1: 
    Condition: ExistsCron1
    Type: AWS::Events::Rule
    Properties: 
      Description: ScheduledRule
      ScheduleExpression: !Sub ${Cron1}
      State: ENABLED
      Targets: 
        - Arn: !Ref StateMachine
          Id: !GetAtt StateMachine.Name
          RoleArn: !GetAtt ExecuteStateMachineRole.Arn
  ScheduledRule2: 
    Condition: ExistsCron2
    Type: AWS::Events::Rule
    Properties: 
      Description: ScheduledRule
      ScheduleExpression: !Sub ${Cron2}
      State: ENABLED
      Targets: 
        - Arn: !Ref StateMachine
          Id: !GetAtt StateMachine.Name
          RoleArn: !GetAtt ExecuteStateMachineRole.Arn
  ScheduledRule3: 
    Condition: ExistsCron3
    Type: AWS::Events::Rule
    Properties: 
      Description: ScheduledRule
      ScheduleExpression: !Sub ${Cron3}
      State: ENABLED
      Targets: 
        - Arn: !Ref StateMachine
          Id: !GetAtt StateMachine.Name
          RoleArn: !GetAtt ExecuteStateMachineRole.Arn
  ScheduledRule4: 
    Condition: ExistsCron4
    Type: AWS::Events::Rule
    Properties: 
      Description: ScheduledRule
      ScheduleExpression: !Sub ${Cron4}
      State: ENABLED
      Targets: 
        - Arn: !Ref StateMachine
          Id: !GetAtt StateMachine.Name
          RoleArn: !GetAtt ExecuteStateMachineRole.Arn
  ScheduledRule5: 
    Condition: ExistsCron5
    Type: AWS::Events::Rule
    Properties: 
      Description: ScheduledRule
      ScheduleExpression: !Sub ${Cron5}
      State: ENABLED
      Targets: 
        - Arn: !Ref StateMachine
          Id: !GetAtt StateMachine.Name
          RoleArn: !GetAtt ExecuteStateMachineRole.Arn

また、pre_build_command.shbuild_command.shは以下の通りです。※ 長いので折り畳みます。

pre_build_command.shbuild_command.sh

pre_build_command.sh

#!/bin/bash

# -x to display the command to be executed
set -x

bucket_name="$1"
sam_file_name="$2"
repository_path="$3"

deployment_destination_account_iam_role_arn=$(yq -r ".Settings.deployment_destination_account_iam_role_arn" ${repository_path}StateMachineSettings.yml)
echo deployment_destination_account_iam_role_arn : ${deployment_destination_account_iam_role_arn}

i=0
IFS=$'\n'; for cron in $(yq -rc ".Settings.event_bridge_rule[].cron? | select(.!=null)" ${repository_path}StateMachineSettings.yml); do
  cron_array[$((i++))]=$(echo ${cron})
done
echo "${cron_array[@]}"

i=0
IFS=$'\n'; for event_pattern in $(yq -rc ".Settings.event_bridge_rule[].event_pattern? | select(.!=null)" ${repository_path}StateMachineSettings.yml); do
  event_pattern_array[$((i++))]=$(echo ${event_pattern})
done
echo "${event_pattern_array[@]}"

i=0
IFS=$'\n'; for event_bus_arn in $(yq -rc ".Settings.event_bridge_rule[].event_bus_arn? | select(.!=null)" ${repository_path}StateMachineSettings.yml); do
  event_bus_arn_array[$((i++))]=$(echo ${event_bus_arn})
done
echo "${event_bus_arn_array[@]}"

i=0
IFS=$'\n'; for target_event_bus_arn in $(yq -rc ".Settings.target_event_bus_arn[]" ${repository_path}StateMachineSettings.yml); do
  target_event_bus_arn_array[$((i++))]=$(echo ${target_event_bus_arn})
done
echo "${target_event_bus_arn_array[@]}"

xray_tracing=$(yq -r ".Settings.xray_tracing" ${repository_path}StateMachineSettings.yml)
echo xray_tracing : ${xray_tracing}

iam_policy_document=$(yq -r ".Settings.iam_policy_document" ${repository_path}StateMachineSettings.yml)
echo iam_policy_document : ${iam_policy_document}

IFS=$'\n'; for tag in $(yq -rc ".Settings.tags[]" ${repository_path}StateMachineSettings.yml); do
  key=$(echo ${tag} | jq -r .Key)
  value=$(echo ${tag} | jq -r .Value)
  tags_list+=$(echo "'${key}'"="'${value}' ")
done
echo tags_list : ${tags_list}

# Download the AWS SAM template file from the S3 bucket
aws s3 cp s3://${bucket_name}/${sam_file_name} ${sam_file_name}

cat StateMachineWorkFlow.asl.json

# Move the necessary files to the AWS SAM directory
mkdir -p sam-sfn/state_machine

cp -p ${repository_path}StateMachineWorkFlow.asl.json ./sam-sfn/state_machine/StateMachineWorkFlow.asl.json
cp -p ${sam_file_name} ./sam-sfn/${sam_file_name}

_cron_array=$(IFS=';'; echo "${cron_array[*]}")
_event_pattern_array=$(IFS=';'; echo "${event_pattern_array[*]}")
_event_bus_arn_array=$(IFS=';'; echo "${event_bus_arn_array[*]}")
_target_event_bus_arn_array=$(IFS=';'; echo "${target_event_bus_arn_array[*]}")

build_command.sh

#!/bin/bash

# -x to display the command to be executed
set -x

bucket_name="$1"
sam_file_name="$2"
state_machine_name="$3"
stack_unique_id="$4"

echo deployment_destination_account_iam_role_arn : ${deployment_destination_account_iam_role_arn}

IFS=';'; cron_array=(${_cron_array}); unset IFS
IFS=';'; event_pattern_array=(${_event_pattern_array}); unset IFS
IFS=';'; event_bus_arn_array=(${_event_bus_arn_array}); unset IFS
IFS=';'; target_event_bus_arn_array=(${_target_event_bus_arn_array}); unset IFS

echo "${cron_array[@]}"
echo "${event_pattern_array[@]}"
echo "${event_bus_arn_array[@]}"
echo "${target_event_bus_arn_array[@]}"

echo xray_tracing : ${xray_tracing}
echo iam_policy_document : ${iam_policy_document}
echo tags_list : ${tags_list}

cd sam-sfn

ls -l ./state_machine/StateMachineWorkFlow.asl.json

if [[ "${deployment_destination_account_iam_role_arn}" != null ]]; then
  before=$(aws sts get-caller-identity | jq -r .Arn)
  output=$(aws sts assume-role --role-arn ${deployment_destination_account_iam_role_arn} --role-session-name sam-deploy-session)

  AWS_ACCESS_KEY_ID=$(echo ${output} | jq -r .Credentials.AccessKeyId)
  AWS_SECRET_ACCESS_KEY=$(echo ${output} | jq -r .Credentials.SecretAccessKey)
  AWS_SESSION_TOKEN=$(echo ${output} | jq -r .Credentials.SessionToken)

  after=$(aws sts get-caller-identity | jq -r .Arn)
fi

AWS_ACCOUNT=$(aws sts get-caller-identity | jq -r .Account)

if [[ -s ./state_machine/StateMachineWorkFlow.asl.json ]]; then
  sam build \
    --template-file ${sam_file_name}

  sam package \
    --template-file ${sam_file_name} \
    --s3-bucket ${bucket_name} \
    --s3-prefix ${state_machine_name}_${AWS_ACCOUNT} \
    --output-template-file output.yml

  deploy_command=(sam deploy \
      --template-file output.yml \
      --s3-bucket ${bucket_name} \
      --s3-prefix ${state_machine_name}_${AWS_ACCOUNT} \
      --stack-name ${state_machine_name} \
      --capabilities CAPABILITY_IAM \
      --no-fail-on-empty-changeset \
      --parameter-overrides \
        StateMachineName=${state_machine_name} \
        StackUniqueId=${stack_unique_id} \
        Cron1="'${cron_array[0]}'" \
        Cron2="'${cron_array[1]}'" \
        Cron3="'${cron_array[2]}'" \
        Cron4="'${cron_array[3]}'" \
        Cron5="'${cron_array[4]}'" \
        EventPattern1="'${event_pattern_array[0]}'" \
        EventPattern2="'${event_pattern_array[1]}'" \
        EventPattern3="'${event_pattern_array[2]}'" \
        EventPattern4="'${event_pattern_array[3]}'" \
        EventPattern5="'${event_pattern_array[4]}'" \
        EventBusArn1="'${event_bus_arn_array[0]}'" \
        EventBusArn2="'${event_bus_arn_array[1]}'" \
        EventBusArn3="'${event_bus_arn_array[2]}'" \
        EventBusArn4="'${event_bus_arn_array[3]}'" \
        EventBusArn5="'${event_bus_arn_array[4]}'" \
        TargetEventBusArn1="'${target_event_bus_arn_array[0]}'" \
        TargetEventBusArn2="'${target_event_bus_arn_array[1]}'" \
        TargetEventBusArn3="'${target_event_bus_arn_array[2]}'" \
        TargetEventBusArn4="'${target_event_bus_arn_array[3]}'" \
        TargetEventBusArn5="'${target_event_bus_arn_array[4]}'" \
        XRayTracing=${xray_tracing} \
        IamPolicyDocument="'${iam_policy_document}'")

  if [[ -n "${tags_list}" ]]; then
    "${deploy_command[@]}" \
      --tags "'${tags_list}'"
  else
    "${deploy_command[@]}"
  fi
  aws cloudformation describe-stacks --stack-name ${state_machine_name}
else
  # If the ASL file is empty, delete the stack
  yes | \
  sam delete \
    --stack-name ${state_machine_name}
fi

このようなテンプレートファイルとシェルスクリプトだと、各リソースを5つ以上設定できるようにしたい場合にテンプレートファイルもシェルスクリプトも変更する必要があるなどメンテナンスも大変です。

これを次章以降、設定ファイルに入力されたパラメーターの数に応じて、CloudFormationテンプレートファイルを動的に変化するように変更します。

シェルスクリプト魔改造してみた

CloudFormationテンプレートファイル

シェルスクリプトを魔改造して、入力されたパラメーターの数に応じてCloudFormationテンプレートファイルを動的に変化するようにします。

まず、テンプレートファイルから修正します。

テンプレートファイルないには繰り返し定義されているものと、そうでないものとの2種類があります。

これを区別するために、ループ処理の対象にIndexから始まる区切り文字を付与します。
例) 入力されたCron式の数によって動的に変化するもの : CronIndexCron,ScheduledRuleIndexCron

また、yqではYAMLのタグを保持できません。そのため、ループ処理させたい箇所の組み込み関数は!Ref!Subなど短縮記法ではなく、Ref:Fn::Sub:で定義します。

行数は234行とかなり減りました。

実際のテンプレートファイルは以下の通りです。

sam-template.yml

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
  sfn-sam

  Sample SAM Template for sfn-sam

Parameters:
  StateMachineName:
    Description: Please type the Step Functions State Machine Name.
    Type: String
    Default: sfn-sam-state-machine
  StackUniqueId:
    Description: Please type the Stack unique ID.
    Type: String
    Default: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  CronIndexCron:
    Description: Please type the Cron.
    Type: String
    Default: "null"
  EventPatternIndexEventPattern:
    Description: Please type the EventPattern.
    Type: String
    Default: "null"
  EventBusArnIndexEventBusArn:
    Description: Please type the Event Bus ARN.
    Type: String
    Default: "null"
  TargetEventBusArnIndexTargetEventBusArn:
    Description: Please type the target event bus arn after execution.
    Type: String
    Default: "null"
  XRayTracing:
    Description: Please type the AWS X-Ray trace to enable or not.
    Type: String
    Default: false
    AllowedValues:
      - true
      - false
  IamPolicyDocument:
    Description: Please type the IAM Policy Document.
    Type: String

Conditions:
  IsEnabledXRayTracing: !Equals
      - !Sub ${XRayTracing}
      - true
  ExistsIamPolicyDocument: !Not 
    - !Equals
      - !Sub ${IamPolicyDocument}
      - "null"

Resources:
  StateMachine:
    Type: AWS::Serverless::StateMachine
    Properties:
      Name: !Sub ${StateMachineName}
      DefinitionUri: state_machine/StateMachineWorkFlow.asl.json
      Role: !GetAtt StateMachineRole.Arn
      Logging:
        Level: ALL
        IncludeExecutionData: True
        Destinations:
          - CloudWatchLogsLogGroup:
              LogGroupArn: !GetAtt StateMachineLogGroup.Arn
      Tracing: 
        Enabled: !Sub ${XRayTracing}

  StateMachineLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName : !Sub '/aws/vendedlogs/states/${StateMachineName}-${StackUniqueId}-Logs'
      RetentionInDays: 731

  StateMachineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - states.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyDocument: {
              "Version": "2012-10-17",
              "Statement": [
                  {
                      "Effect": "Allow",
                      "Action": [
                          "logs:CreateLogDelivery",
                          "logs:GetLogDelivery",
                          "logs:UpdateLogDelivery",
                          "logs:DeleteLogDelivery",
                          "logs:ListLogDeliveries",
                          "logs:PutResourcePolicy",
                          "logs:DescribeResourcePolicies",
                          "logs:DescribeLogGroups"
                      ],
                      "Resource": "*"
                  }
              ]
          }
          PolicyName: CloudWatchLogsDeliveryFullAccessPolicy
        - !If
          - IsEnabledXRayTracing
          - PolicyDocument: {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Effect": "Allow",
                        "Action": [
                            "xray:PutTraceSegments",
                            "xray:PutTelemetryRecords",
                            "xray:GetSamplingRules",
                            "xray:GetSamplingTargets"
                        ],
                        "Resource": [
                            "*"
                        ]
                    }
                ]
            }
            PolicyName: XRayAccessPolicy
          - !Ref AWS::NoValue
        - !If
          - ExistsIamPolicyDocument
          - PolicyDocument: !Sub ${IamPolicyDocument}
            PolicyName: IamPolicyForExecuteStateMachine
          - !Ref AWS::NoValue

  ScheduledRuleIndexCron: 
    Type: AWS::Events::Rule
    Properties: 
      Description: ScheduledRule
      ScheduleExpression: 
        Fn::Sub: ${CronIndexCron}
      State: ENABLED
      Targets: 
        - Arn: 
            Ref: StateMachine
          Id: 
            Fn::GetAtt: StateMachine.Name
          RoleArn: 
            Fn::GetAtt: ExecuteStateMachineRole.Arn

  EventPatternRuleIndexEventPattern: 
    Type: AWS::Events::Rule
    Properties: 
      Description: EventPatternRule
      EventPattern: 
        Fn::Sub: ${EventPatternIndexEventPattern}
      EventBusName: 
        Fn::Sub: ${EventBusArnIndexEventBusArn}
      State: ENABLED
      Targets: 
        - Arn: 
            Ref: StateMachine
          Id: 
            Fn::GetAtt: StateMachine.Name
          RoleArn: 
            Fn::GetAtt: ExecuteStateMachineRole.Arn

  ExecuteStateMachineRole: 
      Type: AWS::IAM::Role
      Properties:
        AssumeRolePolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - events.amazonaws.com
              Action:
                - sts:AssumeRole
        Path: /
        Policies:
          - PolicyName: states_StartExecution
            PolicyDocument:
              Version: "2012-10-17"
              Statement:
                - Effect: Allow
                  Action: 
                    - states:StartExecution
                  Resource: 
                    - !Ref StateMachine

  TargetEventBusArnRuleIndexTargetEventBusArn: 
    Type: AWS::Events::Rule
    Properties: 
      Description: TargetEventBusArnRule
      EventPattern: {
        "source": ["aws.states"],
        "detail-type": ["Step Functions Execution Status Change"],
        "detail": {
          "status": ["SUCCEEDED"],
          "stateMachineArn": [Ref: StateMachine]
        }
      }
      State: ENABLED
      Targets: 
        - Arn: 
            Ref: TargetEventBusArnIndexTargetEventBusArn
          Id: TargetEventBusArnIndexTargetEventBusArn
          RoleArn: 
            Fn::GetAtt: InvokeEventBusRoleIndexTargetEventBusArn.Arn

  InvokeEventBusRoleIndexTargetEventBusArn: 
      Type: AWS::IAM::Role
      Properties:
        AssumeRolePolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - events.amazonaws.com
              Action:
                - sts:AssumeRole
        Path: /
        Policies:
          - PolicyName: InvokeEventBus
            PolicyDocument:
              Version: "2012-10-17"
              Statement:
                - Effect: Allow
                  Action: 
                    - events:PutEvents
                  Resource: 
                    - Ref: TargetEventBusArnIndexTargetEventBusArn

シェルスクリプト

build_command.sh

CloudFormationのテンプレートファイルの修正が終わったら、次はbuild_command.shの修正です。

build_command.shsam buildsam deployなどAWS SAM CLIを実行しています。

sam deploy時にテンプレートファイルに渡すパラメーターの数が動的に変化する必要があります。

そのため、CloudFormationのテンプレートファイルと同様に、ループ処理の対象にIndexから始まる区切り文字を付与します。

実際のコードは以下の通りです。

build_command.sh

#!/bin/bash

# -x to display the command to be executed
set -x

bucket_name="$1"
sam_file_name="$2"
state_machine_name="$3"
stack_unique_id="$4"

# Check variables
echo deployment_destination_account_iam_role_arn : ${deployment_destination_account_iam_role_arn}

IFS=';'; cron_array=(${tmp_cron_array}); unset IFS
IFS=';'; event_pattern_array=(${tmp_event_pattern_array}); unset IFS
IFS=';'; event_bus_arn_array=(${tmp_event_bus_arn_array}); unset IFS
IFS=';'; target_event_bus_arn_array=(${tmp_target_event_bus_arn_array}); unset IFS

echo "${cron_array[@]}"
echo "${event_pattern_array[@]}"
echo "${event_bus_arn_array[@]}"
echo "${target_event_bus_arn_array[@]}"

echo xray_tracing : ${xray_tracing}
echo iam_policy_document : ${iam_policy_document}
echo tags_list : ${tags_list}

# Change to the directory where AWS SAM CLI is to be executed
cd sam-sfn

# Check that the State Machine workflow exists
ls -l ./state_machine/StateMachineWorkFlow.asl.json

# Assume Role if the deployment destination is a different AWS account
if [[ "${deployment_destination_account_iam_role_arn}" != null ]]; then
  before=$(aws sts get-caller-identity | jq -r .Arn)
  output=$(aws sts assume-role --role-arn ${deployment_destination_account_iam_role_arn} --role-session-name sam-deploy-session)

  AWS_ACCESS_KEY_ID=$(echo ${output} | jq -r .Credentials.AccessKeyId)
  AWS_SECRET_ACCESS_KEY=$(echo ${output} | jq -r .Credentials.SecretAccessKey)
  AWS_SESSION_TOKEN=$(echo ${output} | jq -r .Credentials.SessionToken)

  after=$(aws sts get-caller-identity | jq -r .Arn)
fi

AWS_ACCOUNT=$(aws sts get-caller-identity | jq -r .Account)

# If the "StateMachineWorkFlow.asl.json" is not empty, then Deploying resources with AWS SAM CLI
if [[ -s ./state_machine/StateMachineWorkFlow.asl.json ]]; then
  sam build \
    --template-file ${sam_file_name}

  sam package \
    --template-file ${sam_file_name} \
    --s3-bucket ${bucket_name} \
    --s3-prefix ${state_machine_name}_${AWS_ACCOUNT} \
    --output-template-file output.yml

  deploy_command=(sam deploy \
      --template-file output.yml \
      --s3-bucket ${bucket_name} \
      --s3-prefix ${state_machine_name}_${AWS_ACCOUNT} \
      --stack-name ${state_machine_name} \
      --capabilities CAPABILITY_IAM \
      --no-fail-on-empty-changeset \
      --parameter-overrides \
        StateMachineName=${state_machine_name} \
        StackUniqueId=${stack_unique_id} \
        CronIndexCron="'${cron_array[IndexCron]}'" \
        EventPatternIndexEventPattern="'${event_pattern_array[IndexEventPattern]}'" \
        EventBusArnIndexEventBusArn="'${event_bus_arn_array[IndexEventBusArn]}'" \
        TargetEventBusArnIndexTargetEventBusArn="'${target_event_bus_arn_array[IndexTargetEventBusArn]}'" \
        XRayTracing=${xray_tracing} \
        IamPolicyDocument="'${iam_policy_document}'")

  # If tags are specified, add an option to specify tags
  if [[ -n "${tags_list}" ]]; then
    "${deploy_command[@]}" \
      --tags "'${tags_list}'"
  else
    "${deploy_command[@]}"
  fi

  # After deployment, the stack is displayed
  aws cloudformation describe-stacks --stack-name ${state_machine_name}
else
  # If the "StateMachineWorkFlow.asl.json" is empty, delete the stack
  yes | \
  sam delete \
    --stack-name ${state_machine_name}
fi

pre_build_command.sh

最後にpre_build_command.shを修正します。

pre_build_command.sh内で行なっている、リソース毎の処理の流れは以下の通りです。

  1. テンプレートファイル上で繰り返し定義したい内容を変数に保存する
  2. テンプレートファイル上の繰り返し定義したい内容を削除する
  3. 設定ファイルに入力されたパラメーターの数分、次の処理を繰り返す
    1. build_dcommand.sh上のIndexから始まる区切り文字を含む行をコピーして、次の行に貼り付ける
    2. 変数に保存していた繰り返し定義したい内容をテンプレートファイルに追加する。その際、Indexから始まる区切り文字をループした回数に置換する。
    • 例) 区切り文字がIndexCronでループ回数が0回の場合は、IndexCron0に置換する。
    1. 配列に設定ファイルに入力されたパラメーターを保存する
  4. build_dcommand.sh上のIndexから始まる区切り文字を含む行を削除する

これを各リソース毎におこないます。

ここではsedyqが大活躍します。

sedはパターンスペースとホールドスペースの動きを理解することが必要です。その際、ITmediaさんの以下記事が非常に参考になりました。

実際のコードは以下の通りです。

pre_build_command.sh

#!/bin/bash

# -x to display the command to be executed
set -x

bucket_name="$1"
sam_file_name="$2"
repository_path="$3"

# Download the AWS SAM template file from the S3 bucket
aws s3 cp s3://${bucket_name}/${sam_file_name} ${sam_file_name}

# Display State Machine workflow
cat StateMachineWorkFlow.asl.json

# Move the necessary files to the AWS SAM directory
mkdir -p sam-sfn/state_machine

cp -p ${repository_path}StateMachineWorkFlow.asl.json ./sam-sfn/state_machine/StateMachineWorkFlow.asl.json
cp -p ${sam_file_name} ./sam-sfn/${sam_file_name}


# Get the ARN of the IAM role to be used when deploying
deployment_destination_account_iam_role_arn=$(yq -r ".Settings.deployment_destination_account_iam_role_arn" ${repository_path}StateMachineSettings.yml)
echo deployment_destination_account_iam_role_arn : ${deployment_destination_account_iam_role_arn}

# Get the Cron expression of the EventBridge rule associated with the State Machine
# Template delimiter
delimiter=IndexCron

# Get the template part of Parameters and Resources
template_parameter=$(yq -r ".Parameters.Cron${delimiter}" ./sam-sfn/${sam_file_name})
template_resource=$(yq -r ".Resources.ScheduledRule${delimiter}" ./sam-sfn/${sam_file_name})

# Remove the template parts of Parameters and Resources from the template file
cat ./sam-sfn/${sam_file_name} | \
  yq 'del(.Parameters.Cron'${delimiter}')' -Y | \
  yq 'del(.Resources.ScheduledRule'${delimiter}')' -Y | \
  tee ./sam-sfn/tmp_${sam_file_name}
mv ./sam-sfn/tmp_${sam_file_name} ./sam-sfn/${sam_file_name}

i=0
IFS=$'\n'; for cron in $(yq -rc ".Settings.event_bridge_rule[].cron? | select(.!=null)" ${repository_path}StateMachineSettings.yml); do
  # Copy and paste the line with the delimiter on "build_command.sh" to the next line
  sed -i -e "/${delimiter}/{ h; s/${delimiter}/${i}/g; G; }" build_command.sh

  # Add Parameters and Resources to the template file
  cat ./sam-sfn/${sam_file_name} | \
    yq --argjson object "${template_parameter}" '.Parameters += {Cron'${i}': $object}' -Y | \
    yq --argjson object "${template_resource}" '.Resources += {ScheduledRule'${i}': $object}' -Y | \
    sed -e 's/Cron'${delimiter}'/Cron'${i}'/' | \
    tee ./sam-sfn/tmp_${sam_file_name}
  mv ./sam-sfn/tmp_${sam_file_name} ./sam-sfn/${sam_file_name}

  # Store values entered in the configuration file as an array
  cron_array[$((i++))]=$(echo ${cron})
done

# Delete lines with delimiters on "build_command.sh
sed -i "/${delimiter}/d" build_command.sh

# Check values in an array
echo "${cron_array[@]}"


# Get the ARN of the Event Bus of the EventBridge rule event pattern associated with the State Machine
# Template delimiter
delimiter=IndexEventBusArn

i=0
IFS=$'\n'; for event_bus_arn in $(yq -rc ".Settings.event_bridge_rule[].event_bus_arn? | select(.!=null)" ${repository_path}StateMachineSettings.yml); do
  # Copy and paste the line with the delimiter on "build_command.sh" to the next line
  sed -i -e "/${delimiter}/{ h; s/${delimiter}/${i}/g; G; }" build_command.sh

  # Store values entered in the configuration file as an array
  event_bus_arn_array[$((i++))]=$(echo ${event_bus_arn})
done

# Delete lines with delimiters on "build_command.sh
sed -i "/${delimiter}/d" build_command.sh

# Check values in an array
echo "${event_bus_arn_array[@]}"


# Get the event pattern of the EventBridge rule associated with the State Machine
# Template delimiter
delimiter_event_pattern=IndexEventPattern
delimiter_event_bus_arn=IndexEventBusArn

# Get the template part of Parameters and Resources
template_parameter_event_pattern=$(yq -r ".Parameters.EventPattern${delimiter_event_pattern}" ./sam-sfn/${sam_file_name})
template_parameter_event_bus_arn=$(yq -r ".Parameters.EventBusArn${delimiter_event_bus_arn}" ./sam-sfn/${sam_file_name})
template_resource_event_pattern=$(yq -r ".Resources.EventPatternRule${delimiter_event_pattern}" ./sam-sfn/${sam_file_name})

# Remove the template parts of Parameters and Resources from the template file
cat ./sam-sfn/${sam_file_name} | \
  yq 'del(.Parameters.EventPattern'${delimiter_event_pattern}')' -Y | \
  yq 'del(.Parameters.EventBusArn'${delimiter_event_bus_arn}')' -Y | \
  yq 'del(.Resources.EventPatternRule'${delimiter_event_pattern}')' -Y | \
  tee ./sam-sfn/tmp_${sam_file_name}
mv ./sam-sfn/tmp_${sam_file_name} ./sam-sfn/${sam_file_name}

i=0
IFS=$'\n'; for event_pattern in $(yq -rc ".Settings.event_bridge_rule[].event_pattern? | select(.!=null)" ${repository_path}StateMachineSettings.yml); do
  # Copy and paste the line with the delimiter on "build_command.sh" to the next line
  sed -i -e "/${delimiter_event_pattern}/{ h; s/${delimiter_event_pattern}/${i}/g; G; }" build_command.sh

  # Add Parameters and Resources to the template file
  cat ./sam-sfn/${sam_file_name} | \
    yq --argjson object "${template_parameter_event_pattern}" '.Parameters += {EventPattern'${i}': $object}' -Y | \
    yq --argjson object "${template_parameter_event_bus_arn}" '.Parameters += {EventBusArn'${i}': $object}' -Y | \
    yq --argjson object "${template_resource_event_pattern}" '.Resources += {EventPatternRule'${i}': $object}' -Y | \
    sed -e 's/EventPattern'${delimiter_event_pattern}'/EventPattern'${i}'/' | \
    sed -e 's/EventBusArn'${delimiter_event_bus_arn}'/EventBusArn'${i}'/' | \
    tee ./sam-sfn/tmp_${sam_file_name}
  mv ./sam-sfn/tmp_${sam_file_name} ./sam-sfn/${sam_file_name}

  # Store values entered in the configuration file as an array
  event_pattern_array[$((i++))]=$(echo ${event_pattern})
done

# Delete lines with delimiters on "build_command.sh
sed -i "/${delimiter_event_pattern}/d" build_command.sh

# Check values in an array
echo "${event_pattern_array[@]}"


# Get the ARN of the Event Bus that puts an event when the State Machine finishes successfully
# Template delimiter
delimiter=IndexTargetEventBusArn

# Get the template part of Parameters and Resources
template_parameter_target_event_bus_arn=$(yq -r ".Parameters.TargetEventBusArn${delimiter}" ./sam-sfn/${sam_file_name})
template_resource_target_event_bus_arn_rule=$(yq -r ".Resources.TargetEventBusArnRule${delimiter}" ./sam-sfn/${sam_file_name})
template_resource_target_event_bus_arn_role=$(yq -r ".Resources.InvokeEventBusRole${delimiter}" ./sam-sfn/${sam_file_name})

# Remove the template parts of Parameters and Resources from the template file
cat ./sam-sfn/${sam_file_name} | \
  yq 'del(.Parameters.TargetEventBusArn'${delimiter}')' -Y | \
  yq 'del(.Resources.TargetEventBusArnRule'${delimiter}')' -Y | \
  yq 'del(.Resources.InvokeEventBusRole'${delimiter}')' -Y | \
  tee ./sam-sfn/tmp_${sam_file_name}
mv ./sam-sfn/tmp_${sam_file_name} ./sam-sfn/${sam_file_name}

i=0
IFS=$'\n'; for target_event_bus_arn in $(yq -rc ".Settings.target_event_bus_arn[]" ${repository_path}StateMachineSettings.yml); do
  # Copy and paste the line with the delimiter on "build_command.sh" to the next line
  sed -i -e "/${delimiter}/{ h; s/${delimiter}/${i}/g; G; }" build_command.sh

  # Add Parameters and Resources to the template file
  cat ./sam-sfn/${sam_file_name} | \
    yq --argjson object "${template_parameter_target_event_bus_arn}" '.Parameters += {TargetEventBusArn'${i}': $object}' -Y | \
    yq --argjson object "${template_resource_target_event_bus_arn_rule}" '.Resources += {TargetEventBusArnRule'${i}': $object}' -Y | \
    yq --argjson object "${template_resource_target_event_bus_arn_role}" '.Resources += {InvokeEventBusRole'${i}': $object}' -Y | \
    sed -e 's/TargetEventBusArn'${delimiter}'/TargetEventBusArn'${i}'/g' | \
    sed -e 's/InvokeEventBusRole'${delimiter}'/InvokeEventBusRole'${i}'/g' | \
    tee ./sam-sfn/tmp_${sam_file_name}
  mv ./sam-sfn/tmp_${sam_file_name} ./sam-sfn/${sam_file_name}

  # Store values entered in the configuration file as an array
  target_event_bus_arn_array[$((i++))]=$(echo ${target_event_bus_arn})
done

# Delete lines with delimiters on "build_command.sh
sed -i "/${delimiter}/d" build_command.sh

# Check values in an array
echo "${target_event_bus_arn_array[@]}"


# Get whether State Machine enables X-Ray tracing
xray_tracing=$(yq -r ".Settings.xray_tracing" ${repository_path}StateMachineSettings.yml)
echo xray_tracing : ${xray_tracing}

# Get the IAM Policy document to attach to the IAM Role associated with the State Machine
iam_policy_document=$(yq -r ".Settings.iam_policy_document" ${repository_path}StateMachineSettings.yml)
echo iam_policy_document : ${iam_policy_document}

# Get tags to attach to resources created by CloudFormation
IFS=$'\n'; for tag in $(yq -rc ".Settings.tags[]" ${repository_path}StateMachineSettings.yml); do
  key=$(echo ${tag} | jq -r .Key)
  value=$(echo ${tag} | jq -r .Value)
  tags_list+=$(echo "'${key}'"="'${value}' ")
done
echo tags_list : ${tags_list}


# Change arrays temporarily to strings
tmp_cron_array=$(IFS=';'; echo "${cron_array[*]}")
tmp_event_pattern_array=$(IFS=';'; echo "${event_pattern_array[*]}")
tmp_event_bus_arn_array=$(IFS=';'; echo "${event_bus_arn_array[*]}")
tmp_target_event_bus_arn_array=$(IFS=';'; echo "${target_event_bus_arn_array[*]}")

デプロイしてみた

それでは実際に修正したテンプレートファイル、シェルスクリプトを使ってデプロイしてみます。

設定ファイル(StateMachineSettings.yml)は以下のように入力しました。

Settings:
  deployment_destination_account_iam_role_arn:
  event_bridge_rule:
    - cron: cron(15 11 * * ? *)
    - cron: cron(15 23 * * ? *)
    - event_pattern: {
        "source": ["aws.states"],
        "detail-type": ["Step Functions Execution Status Change"],
        "detail": {
          "status": ["SUCCEEDED"],
          "stateMachineArn": ["arn:aws:states:us-east-1:<AWSアカウントID>:stateMachine:nonpi"]
        }
      }
      event_bus_arn: arn:aws:events:us-east-1:<AWSアカウントID>:event-bus/default
    - event_pattern: {
        "source": ["aws.states"],
        "detail-type": ["Step Functions Execution Status Change"],
        "detail": {
          "status": ["SUCCEEDED"],
          "stateMachineArn": ["arn:aws:states:us-east-1:<AWSアカウントID>:stateMachine:aaaaaaaaaaaaabbbbbccccdddefg"]
        }
      }
      event_bus_arn: arn:aws:events:us-east-1:<AWSアカウントID>:event-bus/default
  target_event_bus_arn: 
    - arn:aws:events:us-east-1:<AWSアカウントID>:event-bus/StateMachineEventBus
    - arn:aws:events:us-east-2:<AWSアカウントID>:event-bus/default
  xray_tracing: true
  iam_policy_document: {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "ec2:stopInstances"
                ],
                "Resource": [
                    "arn:aws:ec2:us-east-1:<AWSアカウントID>:instance/i-0a3b8c7357a46b183",
                    "arn:aws:ec2:us-east-1:<AWSアカウントID>:instance/i-0a3b8c7357a46dddd",
                    "arn:aws:ec2:us-east-1:<AWSアカウントID>:instance/i-0cb6e117c54a102f4"
                ]
            }
        ]
    }
  tags:
    - Key: System Name
      Value: System 2
    - Key: Environment
      Value: production
    - Key: Auther
      Value: non-97
    - Key: Corporate
      Value: Classmethod

コミットしてしばらくすると、スタックが作成されていました。作成されたスタックの情報をマネージメントコンソールから確認すると、設定ファイルに入力した通りのタグが付与されていることが確認できました。

作成したスタックの情報確認

次に、リソースタブをクリックすると、設定ファイルに入力した通りのリソースが作成されていました。

作成したスタックのリソース一覧

続いて、テンプレートタブをクリックして、このスタックのテンプレートファイルを確認します。

Cron式を2つ入力したので、Cron0Cron1ScheduledRule0ScheduledRule1と入力されたCron式の数に応じてテンプレートファイルが変化していることが確認できます。

CloudFormationのテンプレートファイル
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: 'sfn-sam

  Sample SAM Template for sfn-sam

  '
Parameters:
  StateMachineName:
    Description: Please type the Step Functions State Machine Name.
    Type: String
    Default: sfn-sam-state-machine
  StackUniqueId:
    Description: Please type the Stack unique ID.
    Type: String
    Default: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  XRayTracing:
    Description: Please type the AWS X-Ray trace to enable or not.
    Type: String
    Default: false
    AllowedValues:
    - true
    - false
  IamPolicyDocument:
    Description: Please type the IAM Policy Document.
    Type: String
  Cron0:
    Description: Please type the Cron.
    Type: String
    Default: 'null'
  Cron1:
    Description: Please type the Cron.
    Type: String
    Default: 'null'
  EventPattern0:
    Description: Please type the EventPattern.
    Type: String
    Default: 'null'
  EventBusArn0:
    Description: Please type the Event Bus ARN.
    Type: String
    Default: 'null'
  EventPattern1:
    Description: Please type the EventPattern.
    Type: String
    Default: 'null'
  EventBusArn1:
    Description: Please type the Event Bus ARN.
    Type: String
    Default: 'null'
  TargetEventBusArn0:
    Description: Please type the target event bus arn after execution.
    Type: String
    Default: 'null'
  TargetEventBusArn1:
    Description: Please type the target event bus arn after execution.
    Type: String
    Default: 'null'
Conditions:
  IsEnabledXRayTracing:
    Fn::Equals:
    - Fn::Sub: ${XRayTracing}
    - true
  ExistsIamPolicyDocument:
    Fn::Not:
    - Fn::Equals:
      - Fn::Sub: ${IamPolicyDocument}
      - 'null'
Resources:
  StateMachine:
    Type: AWS::Serverless::StateMachine
    Properties:
      Name:
        Fn::Sub: ${StateMachineName}
      DefinitionUri:
        Bucket: artifactbucketstack-artifactbucket7410c9ef-mpyyu6apfoxt
        Key: StateMachineTest002_984900217833/b1d331f56a2e20e252e7d04171aecc16
      Role:
        Fn::GetAtt:
        - StateMachineRole
        - Arn
      Logging:
        Level: ALL
        IncludeExecutionData: true
        Destinations:
        - CloudWatchLogsLogGroup:
            LogGroupArn:
              Fn::GetAtt:
              - StateMachineLogGroup
              - Arn
      Tracing:
        Enabled:
          Fn::Sub: ${XRayTracing}
  StateMachineLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Sub: /aws/vendedlogs/states/${StateMachineName}-${StackUniqueId}-Logs
      RetentionInDays: 731
  StateMachineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - states.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: /
      Policies:
      - PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - logs:CreateLogDelivery
            - logs:GetLogDelivery
            - logs:UpdateLogDelivery
            - logs:DeleteLogDelivery
            - logs:ListLogDeliveries
            - logs:PutResourcePolicy
            - logs:DescribeResourcePolicies
            - logs:DescribeLogGroups
            Resource: '*'
        PolicyName: CloudWatchLogsDeliveryFullAccessPolicy
      - Fn::If:
        - IsEnabledXRayTracing
        - PolicyDocument:
            Version: '2012-10-17'
            Statement:
            - Effect: Allow
              Action:
              - xray:PutTraceSegments
              - xray:PutTelemetryRecords
              - xray:GetSamplingRules
              - xray:GetSamplingTargets
              Resource:
              - '*'
          PolicyName: XRayAccessPolicy
        - Ref: AWS::NoValue
      - Fn::If:
        - ExistsIamPolicyDocument
        - PolicyDocument:
            Fn::Sub: ${IamPolicyDocument}
          PolicyName: IamPolicyForExecuteStateMachine
        - Ref: AWS::NoValue
  ExecuteStateMachineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - events.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: /
      Policies:
      - PolicyName: states_StartExecution
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - states:StartExecution
            Resource:
            - Ref: StateMachine
  ScheduledRule0:
    Type: AWS::Events::Rule
    Properties:
      Description: ScheduledRule
      ScheduleExpression:
        Fn::Sub: ${Cron0}
      State: ENABLED
      Targets:
      - Arn:
          Ref: StateMachine
        Id:
          Fn::GetAtt: StateMachine.Name
        RoleArn:
          Fn::GetAtt: ExecuteStateMachineRole.Arn
  ScheduledRule1:
    Type: AWS::Events::Rule
    Properties:
      Description: ScheduledRule
      ScheduleExpression:
        Fn::Sub: ${Cron1}
      State: ENABLED
      Targets:
      - Arn:
          Ref: StateMachine
        Id:
          Fn::GetAtt: StateMachine.Name
        RoleArn:
          Fn::GetAtt: ExecuteStateMachineRole.Arn
  EventPatternRule0:
    Type: AWS::Events::Rule
    Properties:
      Description: EventPatternRule
      EventPattern:
        Fn::Sub: ${EventPattern0}
      EventBusName:
        Fn::Sub: ${EventBusArn0}
      State: ENABLED
      Targets:
      - Arn:
          Ref: StateMachine
        Id:
          Fn::GetAtt: StateMachine.Name
        RoleArn:
          Fn::GetAtt: ExecuteStateMachineRole.Arn
  EventPatternRule1:
    Type: AWS::Events::Rule
    Properties:
      Description: EventPatternRule
      EventPattern:
        Fn::Sub: ${EventPattern1}
      EventBusName:
        Fn::Sub: ${EventBusArn1}
      State: ENABLED
      Targets:
      - Arn:
          Ref: StateMachine
        Id:
          Fn::GetAtt: StateMachine.Name
        RoleArn:
          Fn::GetAtt: ExecuteStateMachineRole.Arn
  TargetEventBusArnRule0:
    Type: AWS::Events::Rule
    Properties:
      Description: TargetEventBusArnRule
      EventPattern:
        source:
        - aws.states
        detail-type:
        - Step Functions Execution Status Change
        detail:
          status:
          - SUCCEEDED
          stateMachineArn:
          - Ref: StateMachine
      State: ENABLED
      Targets:
      - Arn:
          Ref: TargetEventBusArn0
        Id: TargetEventBusArn0
        RoleArn:
          Fn::GetAtt: InvokeEventBusRole0.Arn
  InvokeEventBusRole0:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - events.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: /
      Policies:
      - PolicyName: InvokeEventBus
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - events:PutEvents
            Resource:
            - Ref: TargetEventBusArn0
  TargetEventBusArnRule1:
    Type: AWS::Events::Rule
    Properties:
      Description: TargetEventBusArnRule
      EventPattern:
        source:
        - aws.states
        detail-type:
        - Step Functions Execution Status Change
        detail:
          status:
          - SUCCEEDED
          stateMachineArn:
          - Ref: StateMachine
      State: ENABLED
      Targets:
      - Arn:
          Ref: TargetEventBusArn1
        Id: TargetEventBusArn1
        RoleArn:
          Fn::GetAtt: InvokeEventBusRole1.Arn
  InvokeEventBusRole1:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - events.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: /
      Policies:
      - PolicyName: InvokeEventBus
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - events:PutEvents
            Resource:
            - Ref: TargetEventBusArn1

設定ファイルを編集して、もう一度デプロイしてみます。

cron(11 20 * * ? *)というCron式のEventBridgeルールを追加で作成するように、設定ファイル(StateMachineSettings.yml)を更新しました。

StateMachineSettings.yml

Settings:
  deployment_destination_account_iam_role_arn:
  event_bridge_rule:
    - cron: cron(15 11 * * ? *)
    - cron: cron(15 23 * * ? *)
    - cron: cron(11 20 * * ? *)
    - event_pattern: {
        "source": ["aws.states"],
        "detail-type": ["Step Functions Execution Status Change"],
        "detail": {
          "status": ["SUCCEEDED"],
          "stateMachineArn": ["arn:aws:states:us-east-1:984900217833:stateMachine:nonpi"]
        }
      }
      event_bus_arn: arn:aws:events:us-east-1:984900217833:event-bus/default
    - event_pattern: {
        "source": ["aws.states"],
        "detail-type": ["Step Functions Execution Status Change"],
        "detail": {
          "status": ["SUCCEEDED"],
          "stateMachineArn": ["arn:aws:states:us-east-1:984900217833:stateMachine:aaaaaaaaaaaaabbbbbccccdddefg"]
        }
      }
      event_bus_arn: arn:aws:events:us-east-1:984900217833:event-bus/default
  target_event_bus_arn: 
    - arn:aws:events:us-east-1:984900217833:event-bus/StateMachineEventBus
    - arn:aws:events:us-east-2:984900217833:event-bus/default
  xray_tracing: true
  iam_policy_document: {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "ec2:stopInstances"
                ],
                "Resource": [
                    "arn:aws:ec2:us-east-1:984900217833:instance/i-0a3b8c7357a46b183",
                    "arn:aws:ec2:us-east-1:984900217833:instance/i-0a3b8c7357a46dddd",
                    "arn:aws:ec2:us-east-1:984900217833:instance/i-0cb6e117c54a102f4"
                ]
            }
        ]
    }
  tags:
    - Key: System Name
      Value: System 2
    - Key: Environment
      Value: production
    - Key: Auther
      Value: non-97
    - Key: Corporate
      Value: Classmethod

コミットしてしばらくすると、スタックが更新されていました。更新されたスタックのリソース一覧をマネージメントコンソールから確認すると、EventBridgeルールが追加されていることが確認できました。

リソース追加後のリソース一覧

次に、パラメータタブをクリックすると、設定ファイルに入力した通りのパラメーターが指定されていました。

リソース追加後のパラメータ一覧

続いて、テンプレートタブをクリックして、このスタックのテンプレートファイルを確認します。

Cron式を3つ入力したので、Cron0Cron1Cron2ScheduledRule0ScheduledRule1ScheduledRule2と入力されたCron式の数に応じてテンプレートファイルが変化していることが確認できます。

リソース追加後のCloudFormationのテンプレートファイル
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: 'sfn-sam

  Sample SAM Template for sfn-sam

  '
Parameters:
  StateMachineName:
    Description: Please type the Step Functions State Machine Name.
    Type: String
    Default: sfn-sam-state-machine
  StackUniqueId:
    Description: Please type the Stack unique ID.
    Type: String
    Default: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  XRayTracing:
    Description: Please type the AWS X-Ray trace to enable or not.
    Type: String
    Default: false
    AllowedValues:
    - true
    - false
  IamPolicyDocument:
    Description: Please type the IAM Policy Document.
    Type: String
  Cron0:
    Description: Please type the Cron.
    Type: String
    Default: 'null'
  Cron1:
    Description: Please type the Cron.
    Type: String
    Default: 'null'
  Cron2:
    Description: Please type the Cron.
    Type: String
    Default: 'null'
  EventPattern0:
    Description: Please type the EventPattern.
    Type: String
    Default: 'null'
  EventBusArn0:
    Description: Please type the Event Bus ARN.
    Type: String
    Default: 'null'
  EventPattern1:
    Description: Please type the EventPattern.
    Type: String
    Default: 'null'
  EventBusArn1:
    Description: Please type the Event Bus ARN.
    Type: String
    Default: 'null'
  TargetEventBusArn0:
    Description: Please type the target event bus arn after execution.
    Type: String
    Default: 'null'
  TargetEventBusArn1:
    Description: Please type the target event bus arn after execution.
    Type: String
    Default: 'null'
Conditions:
  IsEnabledXRayTracing:
    Fn::Equals:
    - Fn::Sub: ${XRayTracing}
    - true
  ExistsIamPolicyDocument:
    Fn::Not:
    - Fn::Equals:
      - Fn::Sub: ${IamPolicyDocument}
      - 'null'
Resources:
  StateMachine:
    Type: AWS::Serverless::StateMachine
    Properties:
      Name:
        Fn::Sub: ${StateMachineName}
      DefinitionUri:
        Bucket: artifactbucketstack-artifactbucket7410c9ef-mpyyu6apfoxt
        Key: StateMachineTest002_984900217833/b1d331f56a2e20e252e7d04171aecc16
      Role:
        Fn::GetAtt:
        - StateMachineRole
        - Arn
      Logging:
        Level: ALL
        IncludeExecutionData: true
        Destinations:
        - CloudWatchLogsLogGroup:
            LogGroupArn:
              Fn::GetAtt:
              - StateMachineLogGroup
              - Arn
      Tracing:
        Enabled:
          Fn::Sub: ${XRayTracing}
  StateMachineLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Sub: /aws/vendedlogs/states/${StateMachineName}-${StackUniqueId}-Logs
      RetentionInDays: 731
  StateMachineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - states.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: /
      Policies:
      - PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - logs:CreateLogDelivery
            - logs:GetLogDelivery
            - logs:UpdateLogDelivery
            - logs:DeleteLogDelivery
            - logs:ListLogDeliveries
            - logs:PutResourcePolicy
            - logs:DescribeResourcePolicies
            - logs:DescribeLogGroups
            Resource: '*'
        PolicyName: CloudWatchLogsDeliveryFullAccessPolicy
      - Fn::If:
        - IsEnabledXRayTracing
        - PolicyDocument:
            Version: '2012-10-17'
            Statement:
            - Effect: Allow
              Action:
              - xray:PutTraceSegments
              - xray:PutTelemetryRecords
              - xray:GetSamplingRules
              - xray:GetSamplingTargets
              Resource:
              - '*'
          PolicyName: XRayAccessPolicy
        - Ref: AWS::NoValue
      - Fn::If:
        - ExistsIamPolicyDocument
        - PolicyDocument:
            Fn::Sub: ${IamPolicyDocument}
          PolicyName: IamPolicyForExecuteStateMachine
        - Ref: AWS::NoValue
  ExecuteStateMachineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - events.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: /
      Policies:
      - PolicyName: states_StartExecution
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - states:StartExecution
            Resource:
            - Ref: StateMachine
  ScheduledRule0:
    Type: AWS::Events::Rule
    Properties:
      Description: ScheduledRule
      ScheduleExpression:
        Fn::Sub: ${Cron0}
      State: ENABLED
      Targets:
      - Arn:
          Ref: StateMachine
        Id:
          Fn::GetAtt: StateMachine.Name
        RoleArn:
          Fn::GetAtt: ExecuteStateMachineRole.Arn
  ScheduledRule1:
    Type: AWS::Events::Rule
    Properties:
      Description: ScheduledRule
      ScheduleExpression:
        Fn::Sub: ${Cron1}
      State: ENABLED
      Targets:
      - Arn:
          Ref: StateMachine
        Id:
          Fn::GetAtt: StateMachine.Name
        RoleArn:
          Fn::GetAtt: ExecuteStateMachineRole.Arn
  ScheduledRule2:
    Type: AWS::Events::Rule
    Properties:
      Description: ScheduledRule
      ScheduleExpression:
        Fn::Sub: ${Cron2}
      State: ENABLED
      Targets:
      - Arn:
          Ref: StateMachine
        Id:
          Fn::GetAtt: StateMachine.Name
        RoleArn:
          Fn::GetAtt: ExecuteStateMachineRole.Arn
  EventPatternRule0:
    Type: AWS::Events::Rule
    Properties:
      Description: EventPatternRule
      EventPattern:
        Fn::Sub: ${EventPattern0}
      EventBusName:
        Fn::Sub: ${EventBusArn0}
      State: ENABLED
      Targets:
      - Arn:
          Ref: StateMachine
        Id:
          Fn::GetAtt: StateMachine.Name
        RoleArn:
          Fn::GetAtt: ExecuteStateMachineRole.Arn
  EventPatternRule1:
    Type: AWS::Events::Rule
    Properties:
      Description: EventPatternRule
      EventPattern:
        Fn::Sub: ${EventPattern1}
      EventBusName:
        Fn::Sub: ${EventBusArn1}
      State: ENABLED
      Targets:
      - Arn:
          Ref: StateMachine
        Id:
          Fn::GetAtt: StateMachine.Name
        RoleArn:
          Fn::GetAtt: ExecuteStateMachineRole.Arn
  TargetEventBusArnRule0:
    Type: AWS::Events::Rule
    Properties:
      Description: TargetEventBusArnRule
      EventPattern:
        source:
        - aws.states
        detail-type:
        - Step Functions Execution Status Change
        detail:
          status:
          - SUCCEEDED
          stateMachineArn:
          - Ref: StateMachine
      State: ENABLED
      Targets:
      - Arn:
          Ref: TargetEventBusArn0
        Id: TargetEventBusArn0
        RoleArn:
          Fn::GetAtt: InvokeEventBusRole0.Arn
  InvokeEventBusRole0:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - events.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: /
      Policies:
      - PolicyName: InvokeEventBus
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - events:PutEvents
            Resource:
            - Ref: TargetEventBusArn0
  TargetEventBusArnRule1:
    Type: AWS::Events::Rule
    Properties:
      Description: TargetEventBusArnRule
      EventPattern:
        source:
        - aws.states
        detail-type:
        - Step Functions Execution Status Change
        detail:
          status:
          - SUCCEEDED
          stateMachineArn:
          - Ref: StateMachine
      State: ENABLED
      Targets:
      - Arn:
          Ref: TargetEventBusArn1
        Id: TargetEventBusArn1
        RoleArn:
          Fn::GetAtt: InvokeEventBusRole1.Arn
  InvokeEventBusRole1:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - events.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: /
      Policies:
      - PolicyName: InvokeEventBus
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - events:PutEvents
            Resource:
            - Ref: TargetEventBusArn1

それシェルスクリプトで出来るかも

シェルスクリプトを使って、入力されたパラメーターの数に応じてCloudFormationテンプレートファイルを動的に変化させてみました。

かなりのパワープレイですが、AWS CDKやマクロを使わずにAWS CloudFomationでループ処理を実装したい場合は、このようにテンプレートファイルを直接編集するスクリプトを作ることになるのかなと思います。

そのような場面に備えてスクリプトを書く力を鍛えておくことが重要そうですね。

今回シェルスクリプトを書くにあたっては、GoogleのShell Style Guideを参考にしました。

この記事が誰かの助けになれば幸いです。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!