はじめに
こんにちは、クラウド事業部の吉本です。
Oracle Cloud Infrastructure (OCI)上でFunctionsとリソース・スケジューラ機能を利用して、定期的にGitHubのNotificationの内容をLLMで要約してSlackに通知するアプリケーションを個人開発したので、開発の流れを紹介します。
本記事では、GitHubやSlackとの接続やプログラムの内容については軽く触れるに留め、OCIでの実装部分を中心に解説します。
アプリ開発の経緯
私は業務でOSSを組み合わせた先端システムの研究を行っており、最新のOSS情報をいち早く集める必要があります。
そのため、GitHubのWatch機能で、各種OSSのReleaseをGitHub Notificationに通知するようにしているのですが、Release Noteはすべて英語で書かれており、OSSやReleaseによっては更新内容が多いため、トレースするのが大変です。
そのため、Release Noteの内容を日本語で要約して知らせてくれるアプリが欲しいと思っていました。

↑ このようなリリースノートを毎回解読するのはつらいので、日本語で要約したい!
システム概要
本システムはOCI Functionsが機能の中心で、ここでPythonアプリを動作させます。Pythonアプリでは、GitHubの1日分のNotificationを取得し、OpenAI APIで要約して、社内のSlackに通知します。
FunctionはOCIのリソース・スケジューラ機能で1日1回起動するようにします。
また、アプリケーションのログをOCIのログ監視機能によって取得できるようにします。
以下にシステム概要図を示します。

OCI Functionsの開発
OCI Functionsは、コンテナイメージベースで動作するのが大きな特徴です。AWS LambdaやAzure Functionsと違い、OCI Registry (OCIR) にコンテナイメージをPushし、そのコンテナを起動するという仕組みです。
ややこしく感じるかもしれませんが、コンテナイメージのビルドやPush, Pullなどは自動で実行されるため、ユーザの操作は最小限で実現できます。
Functionの開発は、専用の fdk というパッケージで行います。
私はWSL上で開発を行いましたので、以下はLinux環境での手順となります。
fdkによるPythonアプリ開発
fdk init を実行すると、func.py, func.yaml, requirements.txt というファイルが作成されます。
fn init --runtime python
func.yamlは以下のようになっています。バージョンを上げた際などは修正を行います。
schema_version: 20180708 name: github-release-notifier 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: 1024 timeout: 300
func.pyを編集し、Pythonプログラムを書いていきます。ソースコードの内容の解説は割愛します。
ビルドとデプロイ
プログラムが完成したら、コンテナイメージのビルドとデプロイを行います。
事前にOCI CLIがインストール済みで、oci setup configを済ませている前提で解説します。
OCI Functionは、Applicationの中に、Functionが1つ以上作成されるという構造になっています。
以下にApplicationとFunctionの関係図を示します。
図ではFunctionを2つにした場合を描いていますが、今回はFunctionは1つです。

まずは、器となるOCI Functions Applicationを作成します。今回はパブリック・サブネットに作成するため、パブリック・サブネットのIDを指定します。
fn create app github-release-notify --annotation oracle.com/oci/subnetIds="[\"ocid1.subnet.oc1.ap-tokyo-1.aaaaaaaaxxx...xxx\"]"
コンテナレジストリを指定します。xxxxxxxxxはnamespaceを指定します。
fn update context registry ap-tokyo-1.ocir.io/xxxxxxxxx/github-release
コンテキスト(接続先情報)が正しく設定されているか確認します。
fn inspect context oci
以下のように表示されればよいです。私はルートコンパートメントを使ったので、コンパートメントIDはテナンシーIDと同じです。
Current context: oci api-url: https://functions.ap-tokyo-1.oraclecloud.com oracle.compartment-id: ocid1.tenancy.oc1..aaaaaaaaxxx...xxx provider: oracle registry: ap-tokyo-1.ocir.io/xxxxxxxxx/github-release
コンテナレジストリにログインします。
docker login ap-tokyo-1.ocir.io -u <namespace>/<your_email@example.com> -p <your_auth_token>
[補足] パスワードに入力する認証トークンの作成方法
OCIコンソールの右上の[ユーザー設定]から、[トークンおよびキー]タブを選択し、[トークンの生成]を選択すると作成できます。
Functionをデプロイします。
fn deployコマンドで、コンテナイメージのビルドとコンテナレジストリへのPushを同時に行ってくれます。
--verboseをつけると詳細が表示されるので、コンテナイメージのビルドエラーの際のトラブルシュートが容易になります。
fn deploy --app github-release-notify --verbose
確認と手動実行
アプリケーションの画面を見ると、github-release-appという名前のアプリケーションが作成されていることが分かります。

github-release-appをクリックし、ファンクションというタブを開くと、github-release-notifierという名前のファンクションが作成されていることが分かります。

構成のタブから環境変数を追加することが可能です。環境変数はApplication共通でもFunction単位でも設定可能ですが、今回はApplicationを対象に設定しました。
ここでは画面からではなく、コマンドで設定する方法を紹介します。
fn config app github-release-app GITHUB_TOKEN "ghp_x...X" fn config app github-release-app OPENAI_API_KEY "sk-proj-X...X" fn config app github-release-app SLACK_WEBHOOK_URL "https://hooks.slack.com/services/XXXXX/XXXXX/XXXXXXX"
コンソールからも設定されたことが分かります。

ついでに、OCI Registryの状態も見ておきましょう。
OCIコンソールのメニューから、[開発者サービス] → [コンテナとアーティファクト] → [コンテナ・レジストリ]を選択します。
[リポジトリおよびイメージ]からgithub-release/github-release-notifierを選択すると、リポジトリが作成されていることが分かります。

準備ができたので、試しにFunctionを起動してみます。Functionを手動で実行する場合は fn invokeコマンドを実行します。
fn invoke github-release-app github-release-notifier
運用機能の設定
ログ監視設定
上記でFunctionを実行しても、画面からは成功したか失敗したかくらいしか分かりません。
アプリの標準出力をログとして取得するために、ロギングの設定を行います。
ログを作成する前に、ログ・グループを作成する必要があります。
OCIコンソールのメニューから、[監視および管理] → [ロギング] → [ログ・グループ]を選択します。
[ログ・グループの作成]をクリックしてログ・グループを作成します。

ログ・グループを作り終えたら、次に左メニューから[ログ]を選択します。
[アクション] → [サービス・ログの有効化]を選択し、サービスにFunctionsを選択します。ログはFunction単位ではなくApplication単位で取得されます。
その他の項目も入力して[有効化]をクリックします。

Functionを実行した後、上記で作成したログの画面で、[ログの探索]を確認するとログが出力されているはずです。
ログが表示されていないときは、[ログ検索で探索]をクリックし、時間によるフィルタから適切な期間を指定してください。ただし、指定はUTCなので、日本時間と9時間ずれていることに注意してください。

定期実行設定
スケジュールの設定
GitHubの情報を毎日通知したいので、Functionを1日に1回実行できるように設定を行います。
OCIコンソールのメニューから、[ガバナンスと管理] → [リソース・スケジューラ] → [スケジュール]を選択します。
[スケジュールの作成]からウィザードに従って設定を行います。リソースの選択方法では静的を選択します。

Apply parameters (Optional)は少しややこしいです。これはPythonのコマンドライン引数のことではなく、RESTのリクエストBodyに変数を追加したいときに使うものです。私は特別なパラメタは不要なので、設定は行わずに[次へ]に進みました。

[スケジュール]の画面では[フォーム・インタフェース]と[Cron式]を選択できます。いずれも1時間未満の定期起動はできません。私は[フォーム・インタフェース]を使いました。
[時間]を設定する際は、指定時刻がUTCなので、日本時間からは9時間引き算して入力する必要があります。私は毎日朝の5時に起動するように設定しました。

ポリシーの追加
スケジュールを設定できたら、次はリソーススケジューラがファンクションにアクセスするためのポリシーを設定する必要があります。
OCIコンソールのメニューから、[アイデンティティとセキュリティ] → [アイデンティティ] → [ポリシー]を選択します。
[ポリシーの作成]を選択し、名前と説明を入力した後、[手動エディタの表示]を選択します。
以下の定義を入力し、[作成]を選択すれば完了です。
※ 実際の商用環境では、より厳密なポリシー付与を行ってください。
Allow any-user to manage functions-family in tenancy where all { request.principal.type = 'resourceschedule' }

課題対応
開発にあたり、いくつか課題に直面しましたが、OCIに関係する課題を2点紹介します。
LangChainが使えなかった
PythonアプリはGitHubのRelease Noteを日本語で要約するためにOpenAI APIを利用するのですが、当初はLangChain 1.0を利用するつもりでした。
ローカルでの動作は成功したものの、OCI上にデプロイ後にfn invokeすると、以下のエラーで動作しませんでした。
'NoneType' object is not callable
どうもlangchain_openai → langchain_core → langsmith の依存関係でエラーになっているようでした。
LangChainのバージョンを 0.3 や 0.2 にダウングレードしても事象は解消しませんでした。
Webで同様の事例を調査したところ、同じ現象になっている人が少なくなかったです。
調査に時間がかかりそうだったので、LangChainはあきらめて、OpenAI SDKで実装を進めました。
Functionの時間超過
OCI Functionの実行制限時間は300秒です。
GitHubの1日のNotificationが15個程度あると、LLMでの要約に時間がかかって、300秒を超えてしまい、すべてのRelease情報をSlackに通知できないという課題がありました。
そこで、OpenAI APIへのリクエストを並列で行う実装を行い、ローカル環境での動作に成功しました。
しかし、OCI上にデプロイ後にfn invokeすると、エラーで動作しませんでした。
① OCI Functions/fdk環境では既にイベントループが実行中であるため、asyncio.run()を呼び出せない。
asyncio.run() cannot be called from a running event loop
② 新しいループも実行できない
Cannot run the event loop while another loop is running
最終的に、asyncioをThreadPoolExecutorに置き換えたことで動作に成功しました。
並列処理実装時は、ローカル環境で成功しても、OCI Functions環境だとエラーになる可能性を考慮したほうがよさそうです。
最終実行結果
すべての設定を行い、課題対応を行った翌日、Slackを確認してみると、想定通りにGitHub Notificationの1日分のRelease Noteの日本語要約が朝5時に通知されていることを確認できました。

おわりに
ここまでのOCI機能はすべて無料で実装できています。
お手軽にアプリを動作させる環境としてOCI Functionsは良い選択肢になりそうです。