OCI Resource schedulerの実行エラーを検知

# 概要
[Oracle Cloud Infrastructure(OCI)](https://www.oracle.com/jp/cloud/)の[Resource Scheduler](https://docs.oracle.com/ja-jp/iaas/Content/resource-scheduler/concepts/resourcescheduler-overview.htm)で登録したリソース起動スケジュールが失敗した時にアラート通知します。

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730992/776b3fa3-219d-0f94-b824-4d7cf9abf0cc.png">

# 背景
Resource Schedulerはスケジュール定期実行できるサービスですが、2024年10月時点では実行がエラーになった場合の検出サービスが見当たらなかったため検出できる仕組みを作成しました。

# 構成
- VMからResource schedulerの最終実行結果を定期取得
    -  Resource schedulerの最終実行結果を取得
    -  最終実行=SUCCEEDED以外があればメトリックを1にしてOCI Monitoringに送信
    -  上記を定期実行
- OCI Monitoringで監視
    - メトリック1の場合Notificationsで設定した送付先に通知
    - VM自体の障害を考慮して、メトリックが存在しない場合Notificationsで設定した送付先に通知
- (オプション)失敗したSchedule名を抽出するFunctionを実行
    - Resource schedulerの最終実行結果を取得
    - 最終実行=SUCCEEDED以外のSchedule名を抽出
    - Notificationsで設定した送付先に通知

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730992/14b61739-60e5-a9fa-9e91-972841d5ffa7.png">


# VMからResource schedulerの最終実行結果を定期取得
## 準備
Oracle Cloud Infrastructure (OCI)内に以下を準備します
- Linux VMおよびPython実行環境
- [OCIの監視設定例-Webサーバ Disk使用率](https://qiita.com/tktk2712/items/f1c9002ff5fb20040e0a#web%E3%82%B5%E3%83%BC%E3%83%90-disk%E4%BD%BF%E7%94%A8%E7%8E%87)を参考にポリシー設定
  今回はociユーザー資格情報を使用

## Pythonコード
```rscschcheck.py
import json
import oci
import datetime
from pytz import timezone

# User Principal
config = oci.config.from_file()

# Check resource schedule
resource_scheduler_client = oci.resource_scheduler.ScheduleClient(config,service_endpoint="https://resource-scheduler.ap-tokyo-1.oci.oraclecloud.com")

list_schedules_response = resource_scheduler_client.list_schedules(
    compartment_id="【COMPARTMENT OCID】",
    lifecycle_state="ACTIVE"
    )

with open('/tmp/jsonout.json', 'w') as f:
    print(list_schedules_response.data, file=f)

with open('/tmp/jsonout.json', 'r') as f:
    di = json.load(f)

di2=(di["items"])

runstatus=0
for item in di2:
    if item["last_run_status"] != "SUCCEEDED":
        runstatus=1

# Post value to OCI Monitor
monitoring_client = oci.monitoring.MonitoringClient(config,service_endpoint="https://telemetry-ingestion.ap-tokyo-1.oraclecloud.com")

times_stamp = datetime.datetime.now(timezone('UTC'))

post_metric_data_response = monitoring_client.post_metric_data(
post_metric_data_details=oci.monitoring.models.PostMetricDataDetails(
metric_data=[
    oci.monitoring.models.MetricDataDetails(
        namespace="custom_metrics",
        compartment_id="【COMPARTMENT OCID】",
        name="resource_scheduler",
        dimensions={'result': "scheduler2"},
        datapoints=[
            oci.monitoring.models.Datapoint(
                timestamp=datetime.datetime.strftime(
                    times_stamp,"%Y-%m-%dT%H:%M:%S.%fZ"),
                value=runstatus)]
        )]
    )
)
```
5分おきに実行するようcron設定します
```
$ crontab -l
*/5 * * * * python /path/to/rscschcheck.py
```

# OCI Monitoringで監視
## 準備
- [Notifications](https://oracle-japan.github.io/ocitutorials/intermediates/monitoring-resources/#4-%E3%82%A2%E3%83%A9%E3%83%BC%E3%83%A0%E3%81%AE%E9%80%9A%E7%9F%A5%E5%85%88%E3%81%AE%E4%BD%9C%E6%88%90)を利用して通知先設定

## 監視設定
- OCIコンソール ナビゲーションメニュー>>監視および管理>>モニタリング>>メトリック・エクスプローラにてメトリックが受信できていることを確認
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730992/82dd7c4c-dd45-dc28-3bea-c8e8330cc6c6.png">

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730992/189b51c7-cbdd-1303-ca0c-6d8b7d277010.png">

- このメトリックを利用して2つのアラート設定
    
メトリック値が1を検出
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730992/3bd31933-3fc1-2fa0-65a2-22f10a5ec6aa.png">

メトリックが存在しない(Absent)を検出
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730992/9bd0baef-f75d-6d2d-a6a2-7c2bf6031b5c.png">

# (オプション)失敗したSchedule名を抽出するFunctionを実行
## 準備
Resource schedulerはテナンシー単位の操作となります
私の環境はコンパートメント単位で操作許可が付与されており、テナンシーレベルでリソースプリンシパルができないため、FunctionsにResource schedulerの操作権限を付与することができません。
そのため、Policyは以下の方針としました。
- Policy
     - Functionsにリソースプリンシパルを使用してOCI Vault情報を読み取る
     - FunctionsにOCI Vaultから取得したociユーザー資格情報を使用してResource schedulerの操作を行う
     - Functionsにリソースプリンシパルを使用してNotificationsで設定した送付先に通知

(FunctionsにResource schedulerの操作を行う権限が付与できればOCI Vaultは不要です)

- OCI Valut
   - ociユーザー資格情報を登録
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730992/49041a86-d63f-ee9e-0295-b462f6011400.png">

- [OCI Functions](https://oracle-japan.github.io/ocitutorials/cloud-native/functions-for-beginners/#2-2-oracle-functions%E3%81%AE%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E4%BD%9C%E6%88%90) アプリケーション作成

## Functionsコード
- func.py
オプション1:FunctionsにOCI Vaultから取得したociユーザー資格情報を使用してResource schedulerの操作を行う場合です
```func.py
import io
import json
import logging
import oci
import base64
import datetime
from pytz import timezone

def handler(ctx, data: io.BytesIO = None):
    try:
        # Resource principal
        signer = oci.auth.signers.get_resource_principals_signer()

        # User principal from OCI Vault  -- from here --
        secrets_client = oci.secrets.SecretsClient(config={},signer=signer,service_endpoint="https://secrets.vaults.ap-tokyo-1.oci.oraclecloud.com")

        get_secret_bundle_response = secrets_client.get_secret_bundle(secret_id="【config_userのVAULTSECRET OCID】")
        config_user=base64.b64decode(get_secret_bundle_response.data.secret_bundle_content.content).decode()

        get_secret_bundle_response = secrets_client.get_secret_bundle(secret_id="【config_tenancyのVAULTSECRET OCID】")
        config_tenancy=base64.b64decode(get_secret_bundle_response.data.secret_bundle_content.content).decode()

        get_secret_bundle_response = secrets_client.get_secret_bundle(secret_id="【config_fingerprintのVAULTSECRET OCID】")
        config_fingerprint=base64.b64decode(get_secret_bundle_response.data.secret_bundle_content.content).decode()

        get_secret_bundle_response = secrets_client.get_secret_bundle(secret_id="【config_key_contentのVAULTSECRET OCID】")
        config_key_content=base64.b64decode(get_secret_bundle_response.data.secret_bundle_content.content).decode()

        config = {
            "user": config_user,
            "key_content": config_key_content,
            "fingerprint": config_fingerprint,
            "tenancy": config_tenancy,
            "region": "ap-tokyo-1"
            }

        oci.config.validate_config(config)
        # User principal from OCI Vault  -- to here --

        # resource_scheduler using User principal
        resource_scheduler_client = oci.resource_scheduler.ScheduleClient(config,service_endpoint="https://resource-scheduler.ap-tokyo-1.oci.oraclecloud.com")
        
        # Extract resource shceduler error
        list_schedules_response = resource_scheduler_client.list_schedules(
            compartment_id="【TENANCY OCID】",
            lifecycle_state="ACTIVE"
            )

        with open('/tmp/jsonout.json', 'w') as f:
            print(list_schedules_response.data, file=f)

        with open('/tmp/jsonout.json', 'r') as f:
            di = json.load(f)

        di2=(di["items"])

        with open('/tmp/displayname.json', 'w') as f2:
            print("display name", file=f2)

        for item in di2:
            if item["last_run_status"] != "SUCCEEDED":
                with open('/tmp/displayname.json', 'a') as f2:
                    print(item["display_name"], file=f2)

        with open('/tmp/displayname.json', 'r') as f2:
            displayname = f2.read()

        # Publish Message
        ons_client = oci.ons.NotificationDataPlaneClient(config={},signer=signer,service_endpoint="https://notification.ap-tokyo-1.oci.oraclecloud.com")

        publish_message_response = ons_client.publish_message(
            topic_id="【TOPIC OCID】",
            message_details=oci.ons.models.MessageDetails(
                body=displayname,
                title="Resource Schedule Error")
            )

    except Exception as e:
        print('ERROR: bad Event!', flush=True)
        raise
```
オプション2:Resource principalsを使用したFunctions実行案です(未検証)
<details><summary>Resource principalsを使用したFunctions実行</summary><div>

```func.py
import io
import json
import logging
import oci
import base64
import datetime
from pytz import timezone

def handler(ctx, data: io.BytesIO = None):
    try:
        # Resource principal
        signer = oci.auth.signers.get_resource_principals_signer()

        # resource_scheduler using Resource principal (untested)
        resource_scheduler_client = oci.resource_scheduler.ScheduleClient(config={},signer=signer,service_endpoint="https://resource-scheduler.ap-tokyo-1.oci.oraclecloud.com")
        
        # Extract resource shceduler error
        list_schedules_response = resource_scheduler_client.list_schedules(
            compartment_id="【TENANCY OCID】",
            lifecycle_state="ACTIVE"
            )

        with open('/tmp/jsonout.json', 'w') as f:
            print(list_schedules_response.data, file=f)

        with open('/tmp/jsonout.json', 'r') as f:
            di = json.load(f)

        di2=(di["items"])

        with open('/tmp/displayname.json', 'w') as f2:
            print("display name", file=f2)

        for item in di2:
            if item["last_run_status"] != "SUCCEEDED":
                with open('/tmp/displayname.json', 'a') as f2:
                    print(item["display_name"], file=f2)

        with open('/tmp/displayname.json', 'r') as f2:
            displayname = f2.read()

        # Publish Message
        ons_client = oci.ons.NotificationDataPlaneClient(config={},signer=signer,service_endpoint="https://notification.ap-tokyo-1.oci.oraclecloud.com")

        publish_message_response = ons_client.publish_message(
            topic_id="【TOPIC OCID】",
            message_details=oci.ons.models.MessageDetails(
                body=displayname,
                title="Resource Schedule Error")
            )

    except Exception as e:
        print('ERROR: bad Event!', flush=True)
        raise
```
</div></details>

- func.yaml
```func.yaml
schema_version: 20180708
name: rscscherror
version: 0.0.1
runtime: python
build_image: fnproject/python:3.11-dev
run_image: fnproject/python:3.11
entrypoint: /python/bin/fdk /function/func.py handler
memory: 256

```
- requirements.txt
```requirements.txt
fdk>=0.1.83
oci
```
デプロイして実行します
```
$ fn -v deploy --app fnapp
$ fn invoke fnapp rscscherror
```
成功すると以下のように失敗したスケジュールが通知されます

*題名:Resource Schedule Error
本文:display name
     SCH-EXAMPLE*　←失敗したSchedule名

この作成したFunctionsを"OCI Monitoringで監視"で指定するNotifucationsに追加することによりアラート送付時にFunctionsを実行可能です。