LAC WATCH

セキュリティとITの最新情報

RSS

株式会社ラック

メールマガジン

サイバーセキュリティや
ラックに関する情報をお届けします。

Facebook X Instagram
サービス・製品 | 

OCI Functionsサービスでイベント通知メールを成型してみる(4)

クラウドサービス部の石田です。

OCI Functionsサービス(以下、Functions)で、イベント通知のメール成型をご紹介する連載の最終回です。前回の記事では、当初目的としてきたイベント通知を成型してメール送信するところまで説明しました。ただ、そこで作成したコードは環境によって変動しそうな項目がハードコーディングされており、メンテナンス性や再利用性に課題が残されていました。

そこで、最終回となる今回は、このような環境依存の部分を外部に切り出して再利用性を高める方法の一例を紹介していきます。

再利用性向上のための機能

連載第3回にて作成したコードで変更が発生しそうな部分として、通知サービスのOCIDとメール文章のテンプレートが挙げられます。この2点について、OCIのObject StorageとFunctionsのカスタム構成パラメータという機能を使い、外部に切り出しファンクション自体の再利用性を確保していきます。

Object Storageの使用

まずは、メールの本文および表題のテンプレートを準備します。これをコード内に直接書くのではなく、外部ファイルとして管理することで、メンテナンス性や作業効率を向上させることができます。ファイルの保存場所としては、Functionsでは基本的にObject Storage以外のストレージにはアクセスできないため、Object Storageに配置するのが最適です。

FunctionsからObject Storageを操作するには、通知サービスを使用した際と同様にOCI SDKを使います。そのため、連載第3回と同様のポリシー設定が必要です。動的グループはすでにリソースプリンシパルを設定した際に作っているので、その動的グループに対して以下のポリシーを設定します。今回用意するコードの挙動ではread権限以上が必要です。他にも必要に応じて制限を追加してください。

Allow dynamic-group <dynamic_group_name> to read object-family in compartment <name_of_compartment_where_functions_run>

次はObject Storageに、今回テンプレートを格納するバケットを作ります。ここでは「eventmail_template_vm_powop」という名前にします。そのバケットに、「mailbody.txt」と「subject.txt」の2つのテキスト形式のテンプレートファイルをアップロードします。前者がメール本文の、後者がメール表題のテンプレートになります。テキストのエンコーディングはUTF-8です。

  • mailbody.txt
    インスタンスの操作を検知しました。
    コンパートメント: {body[data][compartmentName]}
    対象サーバ: {body[data][resourceName]}
    時刻: {body[eventTime]}
    操作内容:{body[data][additionalDetails][instanceActionType]}
  • subject.txt
    {body[data][resourceName]}の操作検知

このテンプレートの書式はPythonのstr.format()の形式です。JSONの内容をdictに変換して「body」という名前で渡しているので、bodyの添字として目的の値に対応するJSONのキーを角括弧内に記載し、JSONの階層構造に沿って並べて、置換したいところに波括弧で囲んで記載します。上記テンプレートと、連載第2回で例示したサンプルJSONと比較するとわかりやすいかと思います。

string -- Common string operations -- Python 3.13.3 documentation

このテンプレートを使用するコードは、他の改善も含めて最後に提示します。

カスタム構成パラメータの使用

現状のコードでは、リソース毎に一意に割り当てられるOCIDがハードコーディングされているため、異なる環境で使用する際はコードの修正が発生します。また、先ほどの修正でメールテンプレートが格納されるObject Storageのバケットという参照先が増えてしまったので、ここも同様に別のメールテンプレートを参照したい時に修正が発生します。

このような環境依存の設定値をハードコーディングせず柔軟に管理するため、Functionsにはカスタム構成パラメータという機能があります。機能の概要としては、Functionsのアプリケーションやファンクションの設定にキーと値のセットで設定値を用意し、コード側でキーを指定して値を取得できるようにします。これで実際の設定値はOCI上のアプリケーションやファンクション側で管理できるため、コード側の変更なしで設定値を変更できます。

さらに、この機能はセキュリティに貢献する側面もあります。例えば認証キーなど、コードに直接記述するには不安な情報を管理する必要がある場合、カスタム構成パラメータを利用することで、ソースコード内に機密情報を含めることなく安全に管理できます。

今回は、ファンクションの設定としてカスタム構成パラメータを設定しましょう。Functionsのアプリケーションの左下のメニューから「機能」(これはファンクションのことです......)を選択すると、作成したファンクションの一覧が表示されるので、今回の対象の「event-test」をクリックします。

メニューから「機能」を選択すると、作成したファンクションの一覧が表示される

ファンクションの画面が表示されたら左下のメニューから「構成」をクリックすると、ファンクションのカスタム構成パラメータの設定画面になります。キーと値を入力して「+」ボタンで追加していく形ですね。

ファンクションのカスタム構成パラメータの設定画面

今回は以下の3つのパラメータを、このカスタム構成パラメータで指定したいと思います。

キー 説明
TOPIC_OCID 使用する通知トピックのOCID。連載第3回で作成したトピックのOCIDをコピペする。
BUCKET_NAME メールテンプレートが格納されているObject Storageバケット名。
今回は「eventmail_template_vm_powop」。
LOGLEVEL 出力するログのレベル。今回用意するサンプルコードではDEBUG/INFO/ERRORの三段階を用意しているので適宜設定する。

この画面で設定したパラメータは、コード上ではhandler()に引き渡される第一引数であるリクエストコンテクストの、Config()の戻り値(dict)として参照できます。他にも環境変数としても参照できますが、OCIが提供しているサンプルコードではリクエストコンテクストから参照している例の方が多いようです。

サンプルコード

Object Storageのメールテンプレート読み込みとカスタム構成パラメータ参照の機能を組み込んだコードは、以下のようになります。

import io
import json
import logging
from datetime import timedelta, datetime, timezone
 
from fdk import response
import oci
 
def handler(ctx, data: io.BytesIO = None):
    # カスタム構成パラメータ取得
    cnf = ctx.Config()
    topic_ocid = cnf['TOPIC_OCID']
    bucket_name = cnf['BUCKET_NAME']
    logging.getLogger().setLevel(getattr(logging, cnf['LOGLEVEL']))
 
    mailbody_object_name = "mailbody.txt"
    subject_object_name = "subject.txt"
 
    # イベント内容取得
    try:
        body = json.loads(data.getvalue())
        # 時刻を日本時間に
        body['eventTime'] = datetime.fromisoformat(body['eventTime']).astimezone(
            timezone(timedelta(hours=9))).isoformat(' ')
    except (Exception) as ex:
        logging.getLogger().error('error parsing json payload: ' + str(ex))
        # 最小限のresponseを返して終了
        return response.Response(ctx)
 
    # リソースプリンシパルによる署名情報を取得
    try:
        signer = oci.auth.signers.get_resource_principals_signer()
    except (Exception) as ex:
        logging.getLogger().error('error getting signer: ' + str(ex))
        # 最小限のresponseを返して終了
        return response.Response(ctx)
 
    # オブジェクトストレージからメールテンプレート取得
    try:
        # オブジェクトストレージクライアント生成、ネームスペース取得
        object_storage_client = oci.object_storage.ObjectStorageClient(
            config={}, signer=signer)
        namespace = object_storage_client.get_namespace().data
 
        # オブジェクトストレージからファイル内容取得
        mailbody_obj = object_storage_client.get_object(
            namespace, bucket_name, mailbody_object_name)
        subject_obj = object_storage_client.get_object(
            namespace, bucket_name, subject_object_name)
        mailbody_template = mailbody_obj.data.content.decode(encoding='utf-8')
        subject_template = subject_obj.data.content.decode(encoding='utf-8')
 
        logging.getLogger().debug('mailbody template: ' + mailbody_template)
        logging.getLogger().debug('subject template: ' + subject_template)
    except (Exception) as ex:
        logging.getLogger().error('error reading object storage: ' + str(ex))
        # 最小限のresponseを返して終了
        return response.Response(ctx)
 
    # メールテンプレートレンダリング
    try:
        mailbody = mailbody_template.format(body=body)
        subject = subject_template.format(body=body)
 
        logging.getLogger().debug('mailbody : ' + mailbody)
        logging.getLogger().debug('subject : ' + subject)
    except (KeyError)as ex:
        logging.getLogger().error('error rendering template: ' + str(ex))
        # 最小限のresponseを返して終了
        return response.Response(ctx)
 
    # 通知へのメッセージ発行
    try:
        # 通知クライアントを生成
        ons_client = oci.ons.NotificationDataPlaneClient(
            config={}, signer=signer)
        # メッセージ発行
        publish_message_response = ons_client.publish_message(
            topic_id=topic_ocid,
            message_details=oci.ons.models.MessageDetails(
                body=mailbody,
                title=subject
            )
        )
        logging.getLogger().info('publishing result: ' + str(publish_message_response.data))
 
    except (Exception) as ex:
        logging.getLogger().error('error publishing notification: ' + str(ex))
        # 最小限のresponseを返して終了
        return response.Response(ctx)
 
    # 特に出力はないので最小限のresponseを返す
    return response.Response(ctx)

これを連載第3回で作成した「event-test」のfunc.pyの内容に差し替えて、デプロイ・テストしてみてください。同内容のメールが送信されることがわかります。Object Storageに格納したメールテンプレートを修正すると、それに合わせて表題や本文も変わるので色々試してみてください。

ファンクションの再利用

別のイベントについて通知メールを送る場合は、今回作成したDockerイメージを再利用して別のファンクションを作成し、別のメールテンプレートや通知を参照させるようにカスタム構成パラメータを設定します。

実施手順としては以下のようになります。

  • 1.(必要に応じて)新しいメールテンプレート配置用のObject Storageバケットを作成する
  • 2.(必要に応じて)新しい通知トピックとサブスクリプションを作成する
  • 3.OCIコンソール上で、作成済みのDockerイメージから新しく別のファンクションを作る
  • 4.新しいファンクションにカスタム構成パラメータを設定する
  • 5.イベントで対象のイベントにマッチする条件を作成し、アクションに新しく作成したファンクションを指定する
  • 6.(必要に応じて)サンプル・イベントJSONを元に、新しいメールテンプレートを作成し、今回作成したバケットにアップロードする

3以外は既出の手順になるので、3の手順のみ順を追って説明します。

アプリケーションの機能(ファンクション)の一覧の画面に「ファンクションの作成」ボタンがあります。これを押すと「Create from existing image」と「Create in Code Editor」の2つの選択肢が現れます。後者は今までやってきたコード・エディタを使って開発する方法です。今回は既存のイメージの再利用なので、前者の「Create from existing image」を選択します。

機能(ファンクション)の一覧の画面にある「ファンクションの作成」のドロップダウンメニューから「Create from existing image」を選択する

すると、以下のように新規関数(これは新規ファンクションのことです......)の設定画面が出てきます。

新規関数の設定画面

今回は「event-test-2」と名付けましょう。リポジトリは連載第1回で設定したリポジトリになります。その際に設定したコンパートメントに切り替えて参照してください。イメージの指定は、ファンクションの一覧に表示されている再利用したい元のファンクションの「イメージ」欄にあるものと、バージョン含め同じものを設定します。

イメージの指定は、ファンクションの一覧に表示されている再利用したい元のファンクションの「イメージ」欄にあるものと、バージョン含め同じものを設定する。

その他はひとまずデフォルトで良いです。ここまで設定できれば、「作成」ボタンを押すと新しいファンクションが作成され、以下のようにファンクションの一覧に今作成したものが表示されます。以下の画面例では隠していますが、「イメージ」や「Image digest」の項目が再利用元の「event-test」と同じものになっているのを確認してください。

機能(ファンクション)の一覧の画面。「イメージ」や「Image digest」の項目が再利用元の「event-test」と同じものになっているのを確認する。

あとは同様にメールテンプレートや通知を設定し、カスタム構成パラメータに反映させていきます。ここの設定を別なものにするので、同じイメージを使っても異なる動作をするようになります。それができたら、クラウドシェルでこれまで同様の手順でイベントのサンプルJSONでテストします。

cat test.json | fn invoke event-test event-test-2

これで想定通り動けば、連載第3回で説明した手順に沿って新たにイベントのルールを設定し、アクションに新しい「event-test-2」を指定すれば動作するようになります。

アクションに新しい「event-test-2」を指定する

最後に、設定したイベントを発生させて、作成したテンプレート通りのメールが送信されることを確認してください。

このようにカスタム構成パラメータやObject Storageをうまく活用することで、再利用性が大幅に向上します。一度ファンクションのイメージを作成すれば、以降はコードの追加修正の必要なしに監視対象を増やしたりメールの文面を修正したりできるのです。

おわりに

連載第1回でも触れましたが、DevOps的な体制を推進している企業では、Functionsを運用場面で有効活用している場合も多いでしょう。しかし、実際にはすべての現場がそのレベルに達しているわけではなく、自動化の導入にはまだ障壁があることも多いのではないでしょうか。そのような中でも、メール成型などの自動化タスクをOCI Functionsを使って実現する方法は、規模や予算に関わらず、価値のあるアプローチとなり得ます。

その際、ちょっとした修正の度にコードを修正する必要があると、コストがかかり運用施策の継続性に影響が出てきます。そこで、今回紹介したように、最初からFunctionsの機能を使って再利用性を確保しておけば、修正対応も低コストで行えるようになります。施策の継続性も確保しやすくなり、長期的に安定した運用をしやすくなるでしょう。ぜひ本稿を参考に、様々な場面でOCI Functionsを活用していただければ幸いです。

ラックではOCIだけではなく、AWS、Azure、Google Cloudも取り扱っています。企業のシステム環境、課題に合わせ、適切なソリューション・サービスを提案いたしますので、マルチクラウドやハイブリッドクラウドも含めたシステム構成に関するお悩みがありましたら、ぜひラックまでお問い合わせください。

プロフィール

石田 翼

石田 翼
ハードウェア側からインフラ業務に入ったのに今や1mmもハードウェアを使わないクラウドのお仕事が中心になって困惑しているインフラエンジニア。LinuxやOracle DBの設計・構築などに従事しつつスキルをクラウドシフト中。

「クラウドインテグレーション」に関するお問い合わせ

この記事は役に立ちましたか?

はい いいえ

page top