チャットからLambda関数の実行を予約する

#はじめに
LINEからLambda関数の指定時刻実行を予約したかったのでLambda関数を作成しました。

#つくるもの
###イメージ
![訓練時.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2457109/4e3230bb-7a9b-559f-befb-7b9a711ebe3d.png)

* LINEから受け取ったメッセージを基にLambdaでboto3を実行し、指定のLambda関数を実行予約するEventBridgeルールを作成します。
* 定期実行ではないため実行予約１つにつき１つのEventBridgeルールを作成します。
* ルール名は＜Lambda関数名-実行時間＞とし、この関数から作成された事を示すためのTagと実行時刻（UTC）Tagを付与します。
* Tagを参照しこの関数から作成されたEventBridgeルールの中で、実行済みのEventBridgeルールをチャットから一括削除出来るようにします。
* 実行対象に取れる関数のリージョンは１つのみです（メッセージでリージョンを指定させる事も可能ですが、基本東京リージョンでしかLambda関数を使用していないため）。

###使用技術
* Python3.8
* AWS SDK for Python (Boto3)
* AWS Lambda
* Amazon EventBridge

#実装
###AWS Lambda関数の作成
AWSマネジメントコンソールにて関数を作成します。
関数名「Make_LambdaEvent」、ランタイム「Python 3.8」、その他はデフォルト設定です。

###トリガーの追加
作成したLambda関数にトリガーを追加します。
作成した関数の画面を開き、「設定」タブの「トリガー」を開き、トリガーを追加します。
API Gatewayを選択、チャットツールのWebhookに生成されたAPI エンドポイントを設定してください。

###IAMロールのポリシー編集
Lambda関数にアタッチされているロールにEventBridgeルール操作に必要な権限を追加します。
「Make_LambdaEvent」関数の画面を開き、「設定」タブの「アクセス権限」から実行ロールを押下し、関数にアタッチされているIAMロールを開きます。
![Lambda_Role.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2457109/e7f65e34-0a7a-f9d1-7d56-3f4eda5f06f2.png)
開いた画面でポリシー編集ボタンを押下します。
![ポリシー編集ボタン.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2457109/5d264315-7f55-6c24-ac2b-2519fd588b53.png)
「さらにアクセス許可を追加する」を選択、サービス「EventBridge」を選択し以下の権限を追加し、変更を保存します。

* ListRules: ルールのリスト表示
* PutRule: ルール作成
* PutTargets: ルールのターゲット設定
* TagResource: ルールのTag追加
* ListTargetsByRule: ルールのターゲットのリスト表示
* RemoveTargets: ルールのターゲット削除（ターゲットがあるルールは削除出来ない）
* DeleteRule: ルール削除
![target_list.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2457109/3a8cf52c-f57c-f00c-2da7-6213796f1bc1.png)


Tag操作に必要な権限を追加します。「さらにアクセス許可を追加する」を選択、サービス「Resource Group Tagging」を選択し以下の権限を追加し、変更を保存します。

* GetResources: Tagからのリソース検索
* TagResources: Tagの追加
![tagu---.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2457109/decd64a0-c974-210f-bd6b-15f960e9be6f.png)

###環境変数設定
EventBridgeルールのターゲット指定時にLambda関数のarnが必要なため、環境変数にリージョン及びアカウントIDを設定します。
「Make_LambdaEvent」関数の画面を開き、「設定」タブの「環境変数」から編集ボタンを押下します。
![環境変数１.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2457109/96793686-26d2-080c-11eb-4f5825fafa65.png)
「環境変数の追加」ボタンを押下し、以下の変数を追加し保存します。
![環境変数2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2457109/3c952d9f-80fe-55b6-580b-c821233865dc.png)

* region: 実効予約をする関数のリージョン（東京の場合はap-northeast-1）
* accountid: 実効予約をする関数があるAWSアカウントのID（12桁の数字）

###パーミッション付与
実効対象のLambda関数にて「設定」タブ「アクセス権限」の「ポリシーステートメント」にて、EventBridgeルールからの関数実行を許可します。
![パーミッション.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2457109/0fe57fc1-1583-4361-8d9a-f264da6a29df.png)


###コード
「Make_LambdaEvent」関数の「コード」画面にて以下を貼り付けてDeployボタンを押下します。

```python:lambda_function.py
import json
import boto3
import os
import datetime

#Lambda関数の実効を予約
def make_rule(lambda_name,exec_date):
    
    #日付をdatetimeに変換、JSTをUTCに、cron式に変換
    dt_exec_date = datetime.datetime.strptime(exec_date, '%Y/%m/%d %H:%M')
    dt_exec_date = dt_exec_date - datetime.timedelta(hours=9)
    cron_exec_date = f"cron({dt_exec_date.minute} {dt_exec_date.hour} {dt_exec_date.day} {dt_exec_date.month} ? {dt_exec_date.year})"
    
    #EventBridgeルール名<Lambda関数名-実行時間>
    rule_name = lambda_name + '-' + exec_date.replace('/', '').replace(' ', '').replace(':', '')
    
    #EventBridgeルール作成
    event_client = boto3.client('events')
    response = event_client.put_rule(
                    Name = rule_name,
                    ScheduleExpression = cron_exec_date,
                    EventPattern = '',
                    State = 'ENABLED',
                    Description = 'Made by Make_LambdaEventFunction.'
                )
    
    rule_arn = response['RuleArn']
    
    #環境変数からTargetに指定するLambda関数のarnを作成
    lambda_arn = 'arn:aws:lambda:' + os.environ['region'] + ':' + os.environ['accountid'] + ':function:' + lambda_name
    
    #EventBridgeルールのTargetを指定
    response_t = event_client.put_targets(
                    Rule = rule_name,
                    Targets = [{'Id': lambda_name,'Arn': lambda_arn}]
                )
    
    #作成したEventBridgeルールにTagを追加
    tag_client = boto3.client('resourcegroupstaggingapi')
    response = tag_client.tag_resources(
                    ResourceARNList = [rule_arn],
                    Tags = {
                            'DELETE_RULE': 'ON',
                            'EXEC_DATE': exec_date
                    }
                )

#実効済みEventBridgeルールを削除
def delete_rule():
    
    #EventBridgeルールの中でDELETE_RULEキーがONのルールの中で予約時間から1日以上経過しているルールを配列に格納
    deleteon_resource_arr = []
    
    tag_client = boto3.client('resourcegroupstaggingapi')
    response = tag_client.get_resources(
                    TagFilters = [{'Key': 'DELETE_RULE','Values': ['ON']}],
                    ResourceTypeFilters = ['events']
                )
    
    for deleteon_resource in response['ResourceTagMappingList']:
        for tag in deleteon_resource['Tags']:
            if tag['Key'] == 'EXEC_DATE':
                if datetime.datetime.now() > (datetime.datetime.strptime(tag['Value'], '%Y/%m/%d %H:%M') - datetime.timedelta(hours=9)):
                    deleteon_resource_arr.append(deleteon_resource['ResourceARN'])
    
    while response['PaginationToken'] != '':
        response = tag_client.get_resources(
                    PaginationToken = response['PaginationToken'],
                    TagFilters = [{'Key': 'DELETE_RULE','Values': ['ON']}],
                    ResourceTypeFilters = ['events']
                )
        for deleteon_resource in response['ResourceTagMappingList']:
            for tag in deleteon_resource['Tags']:
                if tag['Key'] == 'EXEC_DATE':
                    if datetime.datetime.now() > (datetime.datetime.strptime(tag['Value'], '%Y/%m/%d %H:%M') - datetime.timedelta(hours=9)):
                        deleteon_resource_arr.append(deleteon_resource['ResourceARN'])

    #配列に格納されたEventBridgeルールを削除
    event_client = boto3.client('events')
    for deleteon_resource in deleteon_resource_arr:
        deleteon_rule_name = (deleteon_resource.split('/'))[-1]
        #EventBridgeルールのTargetを削除
        response = event_client.list_targets_by_rule(
                        Rule=deleteon_rule_name
                    )
        for target in response['Targets']:
            response = event_client.remove_targets(
                Rule = deleteon_rule_name,
                Ids = [target['Id']]
            )
        #EventBridgeルールを削除
        response = event_client.delete_rule(
                        Name = (deleteon_resource.split('/'))[-1]
                    )
    print(deleteon_resource_arr)
    
def lambda_handler(event, context):
    #LINE以外のメッセージAPIを使用する場合は下の１行=以降を変更してください。
    received_text = json.loads(event['body'])['events'][0]['message']['text']
    received_text_arr = received_text.split('\n')

    if received_text_arr[0] == 'set event':
        make_rule(received_text_arr[1],received_text_arr[2])
    elif received_text_arr[0] == 'delete event':
        delete_rule()

    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
```
#使用方法
###Lambda関数の実行予約作成

チャットツールで以下形式のメッセージを送信する事でLambda関数をターゲットに取るEventBridgeルールが作成されます。
`set event
<実行するLambda関数名>
<YYYY/MM/DD hh:mm(実行する時間)>`
![set.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2457109/e9d51e1f-0e20-81cc-1458-c37eb695da8d.png)
作成されたEventBridgeルール
![set_result.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2457109/fd4fbb8b-f743-feae-2df8-9f8d4004c54f.png)

指定した時間に実行されます。
![実効.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2457109/9d5e27f5-33bf-2aa4-dbdc-e62b9a28c4ba.png)
###実行済みのEventBridgeルール削除

チャットツールで以下形式のメッセージを送信する事で、本機能で作成され、かつ実行済みのEventBridgeルールが削除されます。
`delete event`
![before.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2457109/ff2bc91c-1753-b89a-bc17-677c218bf4aa.png)
![delete.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2457109/9057402a-5ffd-4e1a-212d-ec62ce9cc23c.png)
![after.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2457109/2beadaf3-3936-892d-62d1-018e53dcad02.png)

#参考
[CloudWatchEvents - Boto 3 Docs 1.9.42 documentaion](https://boto3.amazonaws.com/v1/documentation/api/1.9.42/reference/services/events.html#CloudWatchEvents.Client.put_rule)
[ResourceGroupsTaggingAPI - Boto 3 Docs 1.20.46 documentaion](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/resourcegroupstaggingapi.html#ResourceGroupsTaggingAPI.Client.tag_resources)