タケユー・ウェブ日報

Webシステム受託会社の業務の中での気づきや調べごとのメモ。

cron の代わり CloudWatch Events + Lambda + RunCommand でタスク実行を冗長化する

Webサーバーを運用していると、定期的にコマンドを実行させたいことはよくあります。 たとえば、このブログだと予約投稿で決められた日時を過ぎたら公開処理をする、みたいなのですね。

そういったものを簡単に実現する手法として、古くから cron が用いられてきました。

ja.wikipedia.org

しかしながら、負荷分散のために EC2 AutoScaling や OpsWorks など、同じ構成のインスタンスを複数並列起動する構成では、グループのインスタンスすべてで同じジョブが実行されてしまう問題があります。

1台をグループから外し、その1台だけでジョブを実行するようにする方法もありますが、cron実行が単一障害点となり、複数台構成の恩恵を十分に受けられません。

複数台構成に対応するジョブスケジューラ製品もいろいろありますが、ここではAWSのサービスだけで実現する方法を考えてみます。

CloudWatch Events + Lambda + AWS SSM RunCommand

f:id:uzuki05:20200813221135p:plain
AWSによるCRON代替構成

構成の特徴

冗長化可能

EC2インスタンスが複数あるとき、そのうちのどれかで実行できるので、構成の冗長化が可能です。

サーバーレス

ジョブ管理サーバーを用意する必要が無いため、管理の手間がかからないこと、余分なコストが発生しないことが特徴です。

CloudFormationで構成可能

AWSのサービスのみで構成されていて、CloudFormationで構成を追加できます。 構成をコードで管理可能で、同じものを作り直すのも簡単です。

手順

マネジメントコンソールを使って構成する場合の手順を示します。 通常はCloudFormationを使って構成する方が管理上好ましいです。

0. EC2インスタンスの準備

EC2インスタンスAWS Systems Manager のための設定が必要です。

blog.takeyuweb.co.jp

1. Step Functions でワークフローを作成

RunCommand をLambdaから実行すれば良いのですが、RunCommand はコマンドを送信するだけで実行結果はとれないので、次の複数の処理からなるワークフローを作成します。

  • EC2インスタンスにコマンドを送信
  • コマンド実行完了を待機
  • 実行結果を通知(コマンド実行成功 or 失敗)

ワークフローの実現には、AWS Step Functions のステートマシンを使うのが簡単です。

aws.amazon.com

Lambda 関数の準備
# Lambda 関数の実行ロールを作成

まず、Lambda関数を実行するときの権限を指定するためのロールを作成しておきます。

今回 Lambda でやりたいことは次の2つです。

なので、次のようなIAM Role を作成すれば良いです。

  • 管理ポリシー
    • AWSLambdaBasicExecutionRole (Lambda関数を実行するのに必要)
  • 次のインラインポリシーを含む
    • ssm:SendCommand
    • ssm:ListCommandInvocations
    • ssm:GetCommandInvocation
  • サービスプリンシパル
    • lambda.amazonaws.com

docs.aws.amazon.com

f:id:uzuki05:20200813181414p:plain
AWSLambdaBasicExecutionRole + インラインポリシー

f:id:uzuki05:20200813172918p:plain
lambda.amazonaws.com で引き受け。ロール作成時にユースケースとして Lambda を選んでいれば自動で設定されている。

# Lambda 関数を作成

次の2つのLambda関数を作成します。

f:id:uzuki05:20200813173202p:plain
実行ロールには作成したロールを選択します。

作成したLambda関数のARNを控えておきます。(ワークフローの定義する際に使用します)

## SendCommand: コマンドを送信

入力 event に、実行インスタンスを特定する情報と、実行したいコマンドを受け取って、 sendCommand で送ります。

const AWS = require('aws-sdk');
const ssm = new AWS.SSM({apiVersion: '2014-11-06'});

const debug = (key, object) => { console.log(`DEBUG: ${key}\n`, JSON.stringify(object)); }

exports.handler = async (event, context) => {
  console.log("INFO: request Recieved.\nEvent:\n", JSON.stringify(event));
  const { TagKey, TagValue, Command } = event;

  const sendCommandParams = {
    DocumentName: 'AWS-RunShellScript',
    Targets: [
      {
        Key: `tag:${TagKey}`,
        Values: [TagValue]
      }
    ],
    Parameters: {
      commands: [Command],
      executionTimeout: ['3600']
    },
    MaxConcurrency: '1',
    MaxErrors: '1',
    TimeoutSeconds: 3600,
  };
  debug("sendCommandParams", sendCommandParams);
  const sendCommandResult = await ssm.sendCommand(sendCommandParams).promise();
  debug("sendCommandResult", sendCommandResult);

  const results = {
    sendCommandParams: sendCommandParams,
    sendCommandResult: sendCommandResult
  };
  debug("results", results);
  return results;
};
## WaitForCommandExecutions: コマンド実行完了を待機

getCommandInvocation でコマンドの実行状態を確認します。 実行状態として "Success" または "Cancelled" "TimedOut" "Failed" のいずれかが得られない場合は CommandNotYetCompleteError 例外を送出し、呼び出し元にコマンド実行が終わっていないことを通知します。

const AWS = require('aws-sdk');
const ssm = new AWS.SSM({apiVersion: '2014-11-06'});

const debug = (key, object) => { console.log(`DEBUG: ${key}\n`, JSON.stringify(object)); }

class CommandNotYetCompleteError extends Error {
  constructor(message) {
    super(message);
    this.name = 'CommandNotYetCompleteError';
  }
}

exports.handler = async (event, context) => {
  console.log("INFO: request Recieved.\nEvent:\n", JSON.stringify(event));
  const { sendCommandParams, sendCommandResult } = event;

  let commandStatus;
  const listCommandInvocationsParams = {
    CommandId: sendCommandResult.Command.CommandId
  };
  debug("listCommandInvocationsParams", listCommandInvocationsParams);
  const listCommandInvocationsResult = await ssm.listCommandInvocations(listCommandInvocationsParams).promise().catch(e => console.error("CommandInvocations", e));
  debug("listCommandInvocationsResult", listCommandInvocationsResult);

  const getCommandInvocationParams = {
    CommandId: sendCommandResult.Command.CommandId,
    InstanceId: listCommandInvocationsResult.CommandInvocations[0].InstanceId,
  };
  debug("getCommandInvocationParams", getCommandInvocationParams);
  const getCommandInvocationResult = await ssm.getCommandInvocation(getCommandInvocationParams).promise().catch(e => console.error("getCommandInvocation", e));
  debug("getCommandInvocationResult", getCommandInvocationResult);
  if (getCommandInvocationResult) {
    commandStatus = getCommandInvocationResult.Status;
  }

  if (commandStatus !== "Success" && commandStatus !== "Cancelled" && commandStatus !== "TimedOut" && commandStatus !== "Failed") {
    throw new CommandNotYetCompleteError("Command is not yet complete. Retry");
  }

  const results = {
    sendCommandParams: sendCommandParams,
    sendCommandResult: sendCommandResult,
    getCommandInvocationParams: getCommandInvocationParams,
    getCommandInvocationResult: getCommandInvocationResult,
    commandStatus: commandStatus
  };
  debug("results", results);
  return results;
};
SNSトピックを作成

コマンドの成功/失敗を通知するSNSトピックを作成しておきます。 成功時と失敗時で通知先を変えたい場合はそれぞれ作成します。 作成したSNSトピックのARNを控えておきます。(ワークフローの定義する際に使用します)

f:id:uzuki05:20200813182932p:plain
今回は CommandExecutionSucceeded CommandExecutionFailed という2つのトピックを作成した。

Step Functions でステート マシン ワークフローの作成
ステートマシンの実行ロールの作成

ステートマシンに必要なアクセス権限を与えるための、実行ロールが必要です。

今回ステートマシン ワークフローでは、次の2つのことを行います。

なので、次のようなIAM Role を作成します。

  • 次のインラインポリシーを含む
    • lambda:InvokeFunction
    • sns:Publish
  • サービスプリンシパル
    • states.amazonaws.com

IAM > ロール > ロールの作成へと進み、ユースケースとして Step Functions を選択します。

docs.aws.amazon.com

docs.aws.amazon.com

f:id:uzuki05:20200813185549p:plain
states.amazonaws.com を信頼。これはロール作成時にユースケースとしてSNSを選択すれば自動設定される。

f:id:uzuki05:20200813185515p:plain
lambda:InvokeFunction と sns:Publish

ステートマシンの作成

Step Functions > ステートマシン > ステートマシンの作成を選びます。

f:id:uzuki05:20200813183220p:plain

f:id:uzuki05:20200813183114p:plain
コードスニペットで作成、タイプ:標準を選びます。

定義に次のJSONを使います。 ARNを指定する箇所は、それぞれ作成したもので置き換えてください。

{
  "Comment": "ExecuteScheduleTask",
  "StartAt": "SendCommand",
  "States": {
    "SendCommand": {
      "Type": "Task",
      "Resource": "コマンドを送信するLambda関数のARN(arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxxxx:function:WaitForCommandExecutions)",
      "Retry": [
        {
          "ErrorEquals": [
            "States.TaskFailed",
            "States.Timeout"
          ],
          "IntervalSeconds": 10,
          "MaxAttempts": 6,
          "BackoffRate": 1.0
        }
      ],
      "Next": "WaitForCommandExecutions"
    },
    "WaitForCommandExecutions": {
      "Type": "Task",
      "Resource": "コマンド実行完了を待機するLambda関数のARN(arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxxxx:function:WaitForCommandExecutions)",
      "Retry": [
        {
          "ErrorEquals": [
            "CommandNotYetCompleteError"
          ],
          "IntervalSeconds": 10,
          "MaxAttempts": 360,
          "BackoffRate": 1.0
        },
        {
          "ErrorEquals": [
            "States.TaskFailed",
            "States.Timeout"
          ],
          "IntervalSeconds": 10,
          "MaxAttempts": 6,
          "BackoffRate": 1.0
        }
      ],
      "Next": "ChoiceCommandStatus"
    },
    "ChoiceCommandStatus": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.commandStatus",
          "StringEquals": "Success",
          "Next": "NotifySuccess"
        }
      ],
      "Default": "NotifyFail"
    },
    "NotifySuccess": {
      "Type": "Task",
      "Resource": "arn:aws:states:::sns:publish",
      "Parameters": {
        "Subject": "Step Functions succeeded",
        "Message.$":"$",
        "TopicArn": "成功時に通知するSNSトピックのARN(arn:aws:sns:ap-northeast-1:xxxxxxxxxxxxxx:CommandExecutionSucceeded)"
      },
      "End": true
    },
    "NotifyFail": {
      "Type": "Task",
      "Resource": "arn:aws:states:::sns:publish",
      "Parameters": {
        "Subject": "Step Functions failed",
        "Message.$":"$",
        "TopicArn": "失敗時に通知するSNSトピックのARN(arn:aws:sns:ap-northeast-1:xxxxxxxxxxxxxx:CommandExecutionSucceeded)"
      },
      "Next": "Fail"
    },
    "Fail": {
      "Type": "Fail"
    }
  }
}

f:id:uzuki05:20200813191123p:plain
定義に合わせてワークフロー表示してくれます。

アクセス許可は、作成した実行ロールを選択します。

f:id:uzuki05:20200814001032p:plain
既存のロールを選択するを選ぶと、サービスプリンシパルからロールが絞り込まれ、選べるようになります。

ステートマシンの実行

インスタンスtest に対して適当なコマンドを送信して試してみます。 実行時の入力として次のJSONを与えます。 この内容が最初のステップのLambdaの event に入ります。

{
  "TagKey": "Name",
  "TagValue": "test",
  "Command": "date"
}

f:id:uzuki05:20200813191006p:plain
ステートマシンの詳細から実行の開始

実行すると進捗状況が表示されます。

f:id:uzuki05:20200813191401g:plain
ステートマシンワークフロー実行の様子

SNSに通知され、メールが届きました。

f:id:uzuki05:20200813191546p:plain
通知メール

2. CloudWatch Events でワークフローを開始

ワークフローができたので、これを cron のように定期実行できるようにします。

これには次の操作が必要です。

  • ルールのターゲットのロールの作成
  • ルールの作成
ルールのターゲットのロールの作成

スケジュールがトリガーされたとき、ターゲットを呼び出そうとします。

ターゲットの種類はいろいろ設定できますが、今回は Step Functions の ステートマシンを実行したいので、そのために必要なアクセス権限を許可するためのロールを作成します。

ロールの内容は次の通りです。

  • 次のインラインポリシーを含む
    • states:StartExecution
  • サービスプリンシパル
    • events.amazonaws.com

IAM > ロール > ロールの作成へと進み、ユースケースとして CloudWatch Events を選択します。

f:id:uzuki05:20200813205649p:plain
CloudWatch Events

f:id:uzuki05:20200813210050p:plain
CloudWatch Events のためのAWS管理ポリシーに加えて、 states:StartExecution を追加

f:id:uzuki05:20200813210140p:plain
CloudWatch Events で使うので信頼関係に events.amazonaws.com

ルールの作成

いよいよスケジュールを登録します。

これには CloudWatch > ルール > ルールの作成 に進みます。

イベントソースは、スケジュールを選択します。 一定時間おきか、Cron式での指定が可能です。 Cron式についてはこちらをご覧下さい。

ターゲットは、Step Functions ステートマシン を選択し、先ほど作成したステートマシンにします。 入力の設定は、JSONテキストを選択し、ステートマシン実行の入力を指定します。ここでは {"TagKey": "Name","TagValue": "test","Command": "date > /tmp/date.txt"} としました。 既存のロールを使用を選び、作成したロールを指定します。

f:id:uzuki05:20200813211125p:plain
スケジュールと、スケジュールがトリガーされたときにやりたいことを指定します。

f:id:uzuki05:20200813211334p:plain
作成できました

ルールが作成でき、スケジュールがトリガーされると、CloudWatchのメトリクスで確認できるほか、Step Functions の実行ログに追加されていきます。

f:id:uzuki05:20200813212740p:plain
5分おきに実行されたことを確認できる

無事に動きました!

CloudFormationテンプレート

ここまでAWSマネジメントコンソール上での操作方法を説明しましたが、いちいちこれを手作業で行うのは非常に辛いので、CloudFormationテンプレートを作成しての運用をおすすめします。

使用方法

2つのテンプレートで構成しています。

  1. コマンド実行に必要な各種リソース作成
  2. 実行スケジュールと実行コマンドを持つ CloudWatch Events Rule リソース作成

この構成では、複数のコマンドを登録したい場合も、1つめのスタックは1つだけで、2つめのスタックで追加していく形になります。

1. コマンド実行に必要な各種リソース作成

まず コマンド実行に必要な各種リソースの作成のテンプレートを使ってスタックを作成します。

f:id:uzuki05:20200813230122p:plain
コマンド実行に必要な各種リソースの作成

f:id:uzuki05:20200813230720p:plain
IAMロールを作成するので、 `AWS CloudFormation によって IAM リソースが作成される場合があることを承認します。` へのチェックが必要です。

スタックの作成が終わったら、出力の StateMachineArn の値を控えておきます。

2. 実行スケジュールと実行コマンドを持つ CloudWatch Events Rule リソース作成

続いて、実際に定期的にコマンドを実行するルールを追加します。

StateMachineArn には先ほど控えておいた値を入力します。 ScheduleExpression の書き方はこちらをご覧下さい。

f:id:uzuki05:20200813233636p:plain
スタックパラメータとして実行したいコマンドなどを入力します

スタック作成が終わると、CloudWatch Events のルールからSSMを叩くLambdaまで一式が作成されます。

f:id:uzuki05:20200813234212p:plain
スタックの作成が無事完了した

f:id:uzuki05:20200813234359p:plain
無事実行に成功していることを確認できた

コード

コマンド実行に必要な各種リソースの作成

Resources:
  StateMachineExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - states.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: run-statemachine
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action: lambda:InvokeFunction
                Resource:
                  - !GetAtt LambdaSendCommand.Arn
                  - !GetAtt LambdaWaitForCommandExecutions.Arn
              - Effect: Allow
                Action: sns:Publish
                Resource:
                  - !Ref TopicCommandExecutionSucceeded
                  - !Ref TopicCommandExecutionFailed
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      Policies:
        - PolicyName: run-command
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - ssm:SendCommand
                  - ssm:ListCommandInvocations
                  - ssm:GetCommandInvocation
                Resource: "*"

  LambdaSendCommand:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |+
          const AWS = require('aws-sdk');
          const ssm = new AWS.SSM({apiVersion: '2014-11-06'});

          const debug = (key, object) => { console.log(`DEBUG: ${key}\n`, JSON.stringify(object)); }

          exports.handler = async (event, context) => {
            console.log("INFO: request Recieved.\nEvent:\n", JSON.stringify(event));
            const { TagKey, TagValue, Command } = event;

            const sendCommandParams = {
              DocumentName: 'AWS-RunShellScript',
              Targets: [
                {
                  Key: `tag:${TagKey}`,
                  Values: [TagValue]
                }
              ],
              Parameters: {
                commands: [Command],
                executionTimeout: ['3600']
              },
              MaxConcurrency: '1',
              MaxErrors: '1',
              TimeoutSeconds: 3600,
            };
            debug("sendCommandParams", sendCommandParams);
            const sendCommandResult = await ssm.sendCommand(sendCommandParams).promise();
            debug("sendCommandResult", sendCommandResult);

            const results = {
              sendCommandParams: sendCommandParams,
              sendCommandResult: sendCommandResult
            };
            debug("results", results);
            return results;
          };
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Runtime: "nodejs12.x"
      MemorySize: 128
      Timeout: 60
  LambdaPermissionLambdaSendCommand:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt LambdaSendCommand.Arn
      Principal: states.amazonaws.com
      SourceArn: !Ref StateMachine
  LambdaWaitForCommandExecutions:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |+
          const AWS = require('aws-sdk');
          const ssm = new AWS.SSM({apiVersion: '2014-11-06'});

          const debug = (key, object) => { console.log(`DEBUG: ${key}\n`, JSON.stringify(object)); }

          class CommandNotYetCompleteError extends Error {
            constructor(message) {
              super(message);
              this.name = 'CommandNotYetCompleteError';
            }
          }

          exports.handler = async (event, context) => {
            console.log("INFO: request Recieved.\nEvent:\n", JSON.stringify(event));
            const { sendCommandParams, sendCommandResult } = event;

            let commandStatus;
            const listCommandInvocationsParams = {
              CommandId: sendCommandResult.Command.CommandId
            };
            debug("listCommandInvocationsParams", listCommandInvocationsParams);
            const listCommandInvocationsResult = await ssm.listCommandInvocations(listCommandInvocationsParams).promise().catch(e => console.error("CommandInvocations", e));
            debug("listCommandInvocationsResult", listCommandInvocationsResult);

            const getCommandInvocationParams = {
              CommandId: sendCommandResult.Command.CommandId,
              InstanceId: listCommandInvocationsResult.CommandInvocations[0].InstanceId,
            };
            debug("getCommandInvocationParams", getCommandInvocationParams);
            const getCommandInvocationResult = await ssm.getCommandInvocation(getCommandInvocationParams).promise().catch(e => console.error("getCommandInvocation", e));
            debug("getCommandInvocationResult", getCommandInvocationResult);
            if (getCommandInvocationResult) {
              commandStatus = getCommandInvocationResult.Status;
            }

            if (commandStatus !== "Success" && commandStatus !== "Cancelled" && commandStatus !== "TimedOut" && commandStatus !== "Failed") {
              throw new CommandNotYetCompleteError("Command is not yet complete. Retry");
            }

            const results = {
              sendCommandParams: sendCommandParams,
              sendCommandResult: sendCommandResult,
              getCommandInvocationParams: getCommandInvocationParams,
              getCommandInvocationResult: getCommandInvocationResult,
              commandStatus: commandStatus
            };
            debug("results", results);
            return results;
          };
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Runtime: "nodejs12.x"
      MemorySize: 128
      Timeout: 60
  LambdaPermissionWaitForCommandExecutions:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt LambdaWaitForCommandExecutions.Arn
      Principal: states.amazonaws.com
      SourceArn: !Ref StateMachine

  TopicCommandExecutionSucceeded:
    Type: AWS::SNS::Topic

  TopicCommandExecutionFailed:
    Type: AWS::SNS::Topic

  StateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      DefinitionString:
        !Sub
          - |+
              {
                "Comment": "ExecuteScheduleTask",
                "StartAt": "SendCommand",
                "States": {
                  "SendCommand": {
                    "Type": "Task",
                    "Resource": "${lambdaSendCommandArn}",
                    "Retry": [
                      {
                        "ErrorEquals": [
                          "States.TaskFailed",
                          "States.Timeout"
                        ],
                        "IntervalSeconds": 10,
                        "MaxAttempts": 6,
                        "BackoffRate": 1.0
                      }
                    ],
                    "Next": "WaitForCommandExecutions"
                  },
                  "WaitForCommandExecutions": {
                    "Type": "Task",
                    "Resource": "${lambdaWaitForCommandExecutionsArn}",
                    "Retry": [
                      {
                        "ErrorEquals": [
                          "CommandNotYetCompleteError"
                        ],
                        "IntervalSeconds": 10,
                        "MaxAttempts": 360,
                        "BackoffRate": 1.0
                      },
                      {
                        "ErrorEquals": [
                          "States.TaskFailed",
                          "States.Timeout"
                        ],
                        "IntervalSeconds": 10,
                        "MaxAttempts": 6,
                        "BackoffRate": 1.0
                      }
                    ],
                    "Next": "ChoiceCommandStatus"
                  },
                  "ChoiceCommandStatus": {
                    "Type": "Choice",
                    "Choices": [
                      {
                        "Variable": "$.commandStatus",
                        "StringEquals": "Success",
                        "Next": "NotifySuccess"
                      }
                    ],
                    "Default": "NotifyFail"
                  },
                  "NotifySuccess": {
                    "Type": "Task",
                    "Resource": "arn:aws:states:::sns:publish",
                    "Parameters": {
                      "Subject": "Step Functions succeeded",
                      "Message.$":"$",
                      "TopicArn": "${topicCommandExecutionSucceededArn}"
                    },
                    "End": true
                  },
                  "NotifyFail": {
                    "Type": "Task",
                    "Resource": "arn:aws:states:::sns:publish",
                    "Parameters": {
                      "Subject": "Step Functions failed",
                      "Message.$":"$",
                      "TopicArn": "${topicCommandExecutionFailedArn}"
                    },
                    "Next": "Fail"
                  },
                  "Fail": {
                    "Type": "Fail"
                  }
                }
              }
          - lambdaSendCommandArn: !GetAtt LambdaSendCommand.Arn
            lambdaWaitForCommandExecutionsArn: !GetAtt LambdaWaitForCommandExecutions.Arn
            topicCommandExecutionSucceededArn: !Ref TopicCommandExecutionSucceeded
            topicCommandExecutionFailedArn: !Ref TopicCommandExecutionFailed
      RoleArn: !GetAtt StateMachineExecutionRole.Arn

Outputs:
  StateMachineArn:
    Value: !Ref StateMachine

実行スケジュールと実行コマンドを持つ CloudWatch Events Rule リソース作成

Parameters:
  RuleName:
    Type: String
  ScheduleExpression:
    Type: String
  EC2InstanceTagName:
    Type: String
    Default: Name
  EC2InstanceTagValue:
    Type: String
  Command:
    Type: String
    Default: sar
  Enabled:
    Type: String
    Default: "true"
    AllowedValues:
      - "true"
      - "false"
  StateMachineArn:
    Type: String
Conditions:
  isEnabled: !Equals [ !Ref Enabled, "true" ]
Resources:
  StartStateMachineExecution:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - events.amazonaws.com
            Action:
              - "sts:AssumeRole"
      Path: /
      Policies:
        - PolicyName: run-statemachine
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action: states:StartExecution
                Resource: !Ref StateMachineArn
  Rule:
    Type: AWS::Events::Rule
    Properties:
      Description: !Sub ${AWS::StackName}-${RuleName}
      ScheduleExpression: !Ref ScheduleExpression
      State: !If [isEnabled, "ENABLED", "DISABLED"]
      Targets:
        - Id: StateMachine
          Input: !Sub '{"TagKey": "${EC2InstanceTagName}", "TagValue": "${EC2InstanceTagValue}", "Command": "${Command}"}'
          Arn: !Ref StateMachineArn
          RoleArn: !GetAtt StartStateMachineExecution.Arn