平日に起動するLambda関数を作成してみた

## 概要

平日の営業時間は処理を実行したいが、土日祝日は処理を動かしたくないケースがあるかと思います。
そこで平日のみLambdaを実行し、土日祝日はLambdaを実行しないようにする構成をCloudFormationで作成します。

## 今回の構成を作成する前に

まずは、月～金に処理を実行し、土日は処理を実行しない場合の構成をCloudFormationで作成してみます。

### 構成
構成としては以下のようなシンプルな形になります。

![構成図-1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/78cd9af3-d899-dd43-3133-902f7c0122a3.png)

### リソースの作成に使用するファイル
リソースの作成には以下のテンプレートを使用します。

```template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: "Periodic Execution Function Template"
Transform: AWS::Serverless-2016-10-31
Resources:
  EventBridgeRule:
    Type: AWS::Events::Rule
    Properties:
      Description: "lambda invoke every 10 minutes Monday through Friday rule."
      Name: lambda-invoke-every-10minutes-rule
      ScheduleExpression: 'cron(0/10 * ? * MON-FRI *)'
      State: ENABLED
      Targets:
        - Arn: !GetAtt EventBridgeTriggerFunction.Arn
          Id: lambda
  EventBridgeTriggerFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: "output-current-time-function"
      Description: "output current time"
      Runtime: nodejs14.x
      CodeUri: app/src
      Handler: index.handler
      MemorySize: 256
      Timeout: 10
      Role: !GetAtt EventBridgeTriggerFunctionRole.Arn
  EventBridgeTriggerFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "output-current-time-function-role"
      Description: "Role For Lambda"
      Path: "/service/"
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: "log-output-policy"
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                Resource:
                  - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*"
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/output-current-time-function:*"
  LambdaInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt EventBridgeTriggerFunction.Arn
      Principal: events.amazonaws.com
      SourceArn: !GetAtt EventBridgeRule.Arn
```

Lambdaの処理では、現在日時を出力します。

```app/src/index.js
exports.handler = async function (event, context) {

  // 現在時刻を日本時間で出力
  console.log(new Date(Date.now() + ((new Date().getTimezoneOffset() + (9 * 60)) * 60 * 1000)));

  return;
}
```


### リソースの作成
今回はローカル環境からAWS SAM CLIコマンドを使用して、スタックの作成を行います。

AWS SAM CLIコマンドのインストール方法は、[こちら](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html
)をご参照ください。

```
sam deploy \
  --stack-name event-bridge-trigger-function-stack \
  --capabilities CAPABILITY_NAMED_IAM \
  --s3-bucket ${テンプレートアップロード先バケット名} \
  --s3-prefix event-bridge-trigger-function-stack \
  --no-fail-on-empty-changeset
```

作成したリソースは以下になります。

![スタック2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/bb982d23-8498-f782-e469-c70a6dc5b17f.png)

### 実行結果の確認

CloudWatchLogsを確認するとLambdaが10分毎に起動し、現在日時を出力していることがわかります。
![Lambdaログ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/e3469816-aab9-5b1f-6984-ceee89096131.png)
![Lambdaログ2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/1ef8ba4c-98fd-3629-5fe9-257a47229ef5.png)


## 土日祝日に実行されないようにしたい場合

先ほどの構成ですと、指定した日付や指定した曜日にLambdaを実行することは出来ますが、祝日に実行されたくない場合などに対応できません。
そこで、**Systems Manager Change Calendar**を使用し、祝日にLambdaが起動しないようにしていきます。

### 構成
構成としては以下になります。

![構成図-2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/249687ab-c336-2182-e2f4-ddf2054d3fe4.png)

### Systems Manager Change Calendarとは

オリジナルのカレンダーイベントを作成できるサービスで、イベントの変化や現在の状態確認を通じて以下のようなことを実現できます。

・09:00にオープン、18:00にクローズするようなカレンダーを作成し、09:00になったらDBを動かし、18:00になったらDBを止めるようなLambdaを実行
・APIでカレンダーイベントの状態を取得して、オープンであれば**営業時間内**、クローズであれば**営業時間外**をレスポンス

カレンダーの設定はコンソール画面で自由に設定したり、GoogleカレンダーなどのサードパーティーのiCalendar (.ics) ファイルをインポートして、カレンダーに設定できます。

※詳しい内容を知りたい方は、[AWS公式サイト](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-change-calendar.html)などをご参照ください。


### カレンダーを作成してみる

Googleカレンダーからエクスポートしたicsファイルを利用して、カレンダーを作成してみます。
今回は日本時間のカレンダーを作成するため、icsファイルのタイムゾーンを以下のように変更します。
```
X-WR-TIMEZONE:UTC
↓
X-WR-TIMEZONE:Asia/Tokyo
```

**カレンダーを作成**ボタンをクリックします。
![SSMカレンダー作成-1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/781e56c3-5a11-48f9-1dc1-1943936ef923.png)

**カレンダー名**を入力し、icsファイルをインポートします。
**カレンダータイプ**は、祝日のみ実行しないようにするため**デフォルトで開く**を選択します。
![SSMカレンダー作成-2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/d1804aff-baf6-3e66-9ae2-629b96398add.png)

作成したカレンダーは以下になります。
![SSMカレンダー-1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/8d68359e-d3f1-5a44-89ec-8f458355e36b.png)


### リソースの作成に使用するファイル

リソースの作成には以下のテンプレートを使用します。

```template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: "Periodic Execution Function Template"
Transform: AWS::Serverless-2016-10-31
Resources:
  OutputCurrentTimeFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "output-current-time-function-role"
      Description: "Role For Lambda"
      Path: "/service/"
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: "log-output-policy"
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                Resource:
                  - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*"
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/output-current-time-function:*"
  OutputCurrentTimeFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: "output-current-time-function"
      Description: "output current time"
      Runtime: nodejs14.x
      CodeUri: app/src
      Handler: index.handler
      MemorySize: 256
      Timeout: 10
      Role: !GetAtt OutputCurrentTimeFunctionRole.Arn
  EventBridgeRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "state-machine-exec-every-10minutes-rule-role"
      Description: "Role For Lambda"
      Path: "/service/"
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - events.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: "state-machine-exec-policy"
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - states:StartExecution
                Resource:
                  - !Ref StateMachine
  EventBridgeRule:
    Type: AWS::Events::Rule
    Properties:
      Description: "state machine exec every 10 minutes Monday through Friday rule."
      Name: state-machine-exec-every-10minutes-rule
      ScheduleExpression: 'cron(0/10 * ? * MON-FRI *)'
      State: ENABLED
      Targets:
        - Arn: !Ref StateMachine
          Id: lambda
          Input: '{}'
          RoleArn: !GetAtt EventBridgeRole.Arn
  StateMachineLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: /sfn/lambda-invoke-excluding-holiday-state-machine
  StateMachineRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "lambda-invoke-excluding-holiday-state-machine-role"
      Description: "Role For StateMachine"
      Path: "/service/"
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - states.ap-northeast-1.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: "ssm-read-policy"
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ssm:GetCalendarState
                Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:document/*"
        - PolicyName: "lambda-invoke-policy"
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - lambda:InvokeFunction
                Resource: !GetAtt OutputCurrentTimeFunction.Arn
        - PolicyName: "log-policy"
          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: "*"
  StateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      StateMachineName: lambda-invoke-excluding-holiday-state-machine
      RoleArn: !GetAtt StateMachineRole.Arn
      DefinitionString:
        !Sub
          - |-
            {
              "Comment": "Lambda Invoke Excluding Holiday State Machine",
              "StartAt": "GetCalendarState",
              "States": {
                "GetCalendarState": {
                  "Type": "Task",
                  "Parameters": {
                    "CalendarNames": [
                      "${ssmCalendarArn}"
                    ]
                  },
                  "Resource": "arn:aws:states:::aws-sdk:ssm:getCalendarState",
                  "Next": "CalendarStateChoice"
                },
                "CalendarStateChoice": {
                  "Type": "Choice",
                  "Choices": [
                    {
                      "Variable": "$.State",
                      "StringEquals": "OPEN",
                      "Next": "LambdaInvoke"
                    },
                    {
                      "Variable": "$.State",
                      "StringEquals": "CLOSED",
                      "Next": "LambdaInvokeSkip"
                    }
                  ],
                  "Default": "LambdaInvokeSkip"
                },
                "LambdaInvoke": {
                  "Type": "Task",
                  "Resource": "${lambdaArn}",
                  "Next": "LambdaInvokeSuccess"
                },
                "LambdaInvokeSuccess": {
                  "Type": "Succeed"
                },
                "LambdaInvokeSkip": {
                  "Type": "Pass",
                  "End": true
                }
              }
            }
          - {
              ssmCalendarArn: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:document/JapaneseHoliday",
              lambdaArn: !GetAtt OutputCurrentTimeFunction.Arn
            }
      LoggingConfiguration:
        Destinations:
          - CloudWatchLogsLogGroup:
              LogGroupArn: !GetAtt StateMachineLogGroup.Arn
        IncludeExecutionData: true
        Level: ALL
```

### リソースの作成

スタックの作成を行います。
```
sam deploy \
  --stack-name invoke-lambda-excluding-holidays-function-stack \
  --capabilities CAPABILITY_NAMED_IAM \
  --s3-bucket ${テンプレートアップロード先バケット名} \
  --s3-prefix invoke-lambda-excluding-holidays-function-stack \
  --no-fail-on-empty-changeset
```

作成したリソースは以下になります。

・スタック
![スタック1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/96625184-e3cf-ec1b-6f76-71fdf3fb05fa.png)

・EventBridge
![EventBridge1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/63fb2cbb-69ec-f68a-b2e2-bc3f3444cc27.png)


・ステートマシン

ステートマシンでは、カレンダーの状態を取得(**GetCalendarState**)し、カレンダーの状態を確認(**CalendarStateChoice**)します。
状態がOPENであればLambdaを実行し、CLOSEDであればステートマシンを終了します。
![ステートマシン-1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/1ff848e6-8222-b9c5-1354-b1e2b641a6b9.png)

・Lambda
![Lambda1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/646de5ed-676b-6d79-7cbd-79dfc6273619.png)


### 実行結果を確認してみた

2022/06/03に実行されたステートマシンの結果を確認すると、カレンダーはOPEN状態のため、Lambdaが実行されていることがわかります。

![ステートマシン-2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/5c4e03b5-afbd-09f1-d802-858358f9e0f9.png)


次に、CLOSEDの動作確認のため、icsファイルに**テスト祝日の日**を追加し、再インポートします。
```
BEGIN:VEVENT
DTSTART;VALUE=DATE:20220603
DTEND;VALUE=DATE:20220604
DTSTAMP:20220603T082139Z
UID:20231009_irbln7n3car55hoi8mu58b019s@google.com
CLASS:PUBLIC
CREATED:20220126T031610Z
DESCRIPTION:祝日
LAST-MODIFIED:20220126T031610Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:テスト祝日の日
TRANSP:TRANSPARENT
```
![SSMカレンダー-2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/d52656c3-edc8-878f-a0b0-2d91b2001bfc.png)

ステートマシンの結果を確認すると、カレンダーはCLOSED状態のため、Lambda実行がスキップされていることがわかります。
![ステートマシン3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/48c83a2c-ddac-ff07-07b6-294f4d7cf0b3.png)


## おわりに
今回はSystems Manager Change Calendarを使用して、平日のみ起動するLambdaを作成しました。
EventBridgeのcron設定による定期実行だけでは実現できないパターンに対応できるので、実行制御の幅が広がりそうです。