Amazon EventBridge SchedulerをAWS SDKを使って操作

# はじめに
アプリ開発において、定期的にもそうですが、時間差（数十分後等）で実行したい処理もあると思います。
プロセスを継続するという選択肢もありますが、AWSの勉強をしているので、せっかくなので何か活用できそうなサービスがないか探した結果、
「Amazon EventBridge Scheduler」というものを見つけました。
# Amazon EventBridge Scheduler　とは

https://aws.amazon.com/jp/blogs/compute/introducing-amazon-eventbridge-scheduler/

もともとAmazon EventBridgeというサービスがあって、そのうちの一つの機能に、今回の主役である「EventBridge Scheduler」があるという位置づけのようです。
先行して、「EventBridge Rule」という機能がローンチされていてこちらがよく使われていたようです。

下記記事にこの2つの違いをまとめてくれています

https://dev.classmethod.jp/articles/eventbridge-scheduler-and-event-bridge-rule-difference/

印象としては、「EventBridge Rule」がよりパワフルになったのが、「EventBridge Scheduler」と見られるような気がします。
少なくとも、1回限りのスケジュールの作成の容易さ、作成しておくことのできるスケジュール数、タイムゾーンを鑑みると「EventBridge Scheduler」のほうが、私の求めている機能としてはふさわしいかなと思いこちらを検証することにしました。

# 今回の目的

AWS-SDKを使ったEventBridge Schedulerの操作に関する記事があまりなかったので今回投稿することにしました。

こんなイメージのアプリ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3507211/36be96af-fb8b-c2a0-e2aa-d4df8c70be35.png)


※WEBアプリの構築手順などは省きます。
### 検証環境構成
EC2にDockerをインストールして、Docker上でnode.jsベースの
WEBアプリを展開しています。
そのnode.jsベースのバックエンドからAWS-SDKを通じてEventBridge SchedulerにあるLambda関数を指定の時刻になったら実行するようなスケジュールの作成、及び削除をします。

# Iamロールの設計
まだ完璧にはこのIamロールの理解ができていないですが、
AWSリソース間の操作においてこの概念はかなり重要だと感じています。

https://qiita.com/yas99en/items/0d85c486bad9f868c08f

残念ながらよくわかりませんが、
今回の目標においては最低2点理解できればいいかなと思いました。
#### 信頼ポリシー
〇誰が
#### 許可ポリシー
〇何をできるか

この2つでIamロールは構成されると思ってます。（本当はもっとあると思います。）
今回のアプリでは、少なくとも矢印の向きは2点あるのでIamロールも2つ用意が必要です。

### ①EC2 -> EventBridge Scheduler
EC2にアタッチされるIamロールとなります。


##### 信頼ポリシー
```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "ec2.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
```

##### 許可ポリシー

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "scheduler:*",
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": "iam:PassRole",
            "Resource": "arn:aws:iam::*:role/*",
            "Condition": {
                "StringLike": {
                    "iam:PassedToService": "scheduler.amazonaws.com"
                }
            }
        }
    ]
}
```
※許可ポリシーは「AmazonEventBridgeschedulerFullAccess」というポリシーがパッケージで存在しているのでそれをGUI上でロールにアタッチしました。

EC2サービスからAmazonEventBridgeschedulerを操作できる全権限が与えられてるんだなっていうのがなんとなく読み取れると思います。

### ②EventBridge Scheduler -> Lambda
スケジュールされるタスクに対してアタッチされるIamロールになります。
#### 信頼ポリシー
```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "scheduler.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
```
※同じサービスとはいえ、EventBridge ruleとエンティティが異なるようで
schedulerとeventsを結構間違えるようです。

https://qiita.com/taskforce_Hisui/items/4c72b910c37b09da52ca
#### 許可ポリシー
```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "lambda:InvokeFunction",
            "Resource": "arn:your-arn:function:aAAAA"
        }
    ]
}
```
aAAAAという名前のLambda関数を作りました。
"Resource"にはLambda関数を作ったときに生成されるリソースネームが入ります。

スケジュールされたタスクがLambdaサービス内のaAAAAという関数にアクセスできるということになります。
このロールをAWS SDKでスケジュールを作成する際に指定します。

# AWS SDK
aws-sdkライブラリをインストール
今回はnode.jsでの検証になるので、
package.json内に依存関係として含める。

```{
  "name": "myapp",
  "version": "1.0.0",
  "main": "app.js",
  "dependencies": {
    "express": "^4.19.2",
    "http": "0.0.1-security",
    "@aws-sdk/client-scheduler": "^3.44.0"

  },
  "scripts": {
    "start": "node app.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": ""
}
```


下記がAWS SDKを通じてスケジュールの作成・削除を実施するためのコード全文
```
const { SchedulerClient, CreateScheduleCommand, DeleteScheduleCommand } = require("@aws-sdk/client-scheduler");
const config = {
  region: 'Your Region',
};
const client = new SchedulerClient(config);

const express = require('express');
const path = require('path');

const app = express();
const port = 3000;

app.use(express.static('public'));

app.get('/create', async (req, res) => {
  try {
    const currentDate = new Date();
    const offset = 9 * 60 * 60 * 1000;
    const japanTime = new Date(currentDate.getTime() + offset);
    const futureDate = new Date(currentDate.getTime() + 5 * 60000);
    const iso8601String = futureDate.toISOString().slice(0, -5);

    const scheduleExpression_string = `at(${iso8601String})`;
    const scheduleParams = {
      Name: 'test',
      ScheduleExpression: scheduleExpression_string,
      Target: {
        Arn: 'Lambda function resource',
        RoleArn: 'iam role',
      },
      FlexibleTimeWindow: {
        Mode: 'OFF',
      },
      ActionAfterCompletion: "DELETE"
    };
    const command = new CreateScheduleCommand(scheduleParams);
    const response = await client.send(command);

    console.log(`Response: ${response}`);
    console.log('Schedule created successfully!');
    res.send('Schedule created successfully!');

  } catch (error) {
    console.error('Error creating schedule:', error);
    res.status(500).send('Error creating schedule');
  }
});

app.get('/delete', async (req, res) => {
  try {
    const input = {
      Name: "test",
    };
    const command = new DeleteScheduleCommand(input);
    const response = await client.send(command);
    
    res.send('Schedule deleted successfully!');

  } catch (error) {
    console.error('Error deleting schedule:', error);
    res.status(500).send('Error deleting schedule');
  }
});

app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

app.listen(port, () => {
  console.log(`Server is running`);
});
```

### 抜粋して解説
## インポート
```
const { SchedulerClient, CreateScheduleCommand, DeleteScheduleCommand } = require("@aws-sdk/client-scheduler");
const config = {
  region: 'Your Region',
};
const client = new SchedulerClient(config);
```
今回は「EventBridge Schedulerとの通信」と「スケジュールの作成」と「スケジュールの削除」に必要なモジュールをインポートします。
configには使用しているリージョン（私は東京リージョン）を指定しています。
※（クレデンシャル情報としてアクセスキーやシークレットキーをここで格納するようですが、今回はIamロールを使用しているため、不要です）

const client = new SchedulerClient(config);
でインスタンスを作成して、clientを通じて通信します

## スケジュール作成
```
const currentDate = new Date();
    const offset = 9 * 60 * 60 * 1000;
    const japanTime = new Date(currentDate.getTime() + offset);
    const futureDate = new Date(currentDate.getTime() + 5 * 60000);
    const iso8601String = futureDate.toISOString().slice(0, -5);

    const scheduleExpression_string = `at(${iso8601String})`;
    const scheduleParams = {
      Name: 'test',
      ScheduleExpression: scheduleExpression_string,
      Target: {
        Arn: 'Lambda function resource',
        RoleArn: 'iam role',
      },
      FlexibleTimeWindow: {
        Mode: 'OFF',
      },
      ActionAfterCompletion: "DELETE"
    };
    const command = new CreateScheduleCommand(scheduleParams);
    const response = await client.send(command);
```

https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/scheduler/command/CreateScheduleCommand/

本当はパラメーターはもっと多いですが、
必須条件と一部必要だと感じたものをピックアップしました。

・Name：
　スケジュールの名前記載
　これがキー情報になるようなので割と重要です。

・ScheduleExpression：
　タスクの実行時刻を指定 
　rate式 cron式 at式で指定できますが今回は一度きりのスケジュールのため、at式を埋め込む。
　at(2024-06-01T17:31:52)の形式で項目を入力する必要があるので前段でいろいろ頑張ってます。

・Target: 
　ここで実行したいLambda関数のリソースと、Iamロールを指定します。

・FlexibleTimeWindow: 
　実行時刻を厳密にするか、ある程度の近似でいいかの設定です。
  今回はOFFにしてるのでこれだけですが、Flexibleにした際はseedみたいな項目を設定する必要があります。

・ActionAfterCompletion:
　スケジュールを実行したあと、そのスケジュールリソースを削除するか、残すかの設定が可能です。

 あとはコマンドインスタンスを作成して、スケジュール作成の実行リクエストを出して、レスポンスしてます。

## スケジュール作成
 ```
 const input = {
      Name: "test",
    };
    const command = new DeleteScheduleCommand(input);
    const response = await client.send(command);
    
    res.send('Schedule deleted successfully!');
```
こちらは割とシンプルです。
削除したいスケジュールの名前を指定します。
コマンドインスタンスを作成して、スケジュール削除の実行リクエストを出して、レスポンスして終わりです。

# 結論
スケジュールの作成・削除は確認できたが
果たして、Lambda関数は実行されてるのか？