Python 向け Durable Functions で型安全なシリアル化に移行

この記事では、Pythonプログラミングモデルを使用する既存のDurable Functionsアプリで型安全(タイプ認識とも呼ばれる)ペイロードのシリアライゼーションを導入する方法を紹介します。 型安全なシリアライゼーションは、デシリアライズされたペイロードを期待される型に対して検証し、信頼できないペイロードのデシリアライズリスクを除去するハードド ストリアモード へのオプトインを可能にします。

型安全なシリアライズの採用は、Pythonを使用するすべてのDurable Functionsアプリ(セキュリティに敏感でないアプリも含む)で推奨されるベストプラクティスです。 SDK は各ペイロードを、保存データで指定されている型を黙って再構築するのではなく、コードが想定する型に照らして検証するため、型不一致のバグを早い段階で発見するのに役立ちます。 さらに、厳密モードはアプリを信頼できないペイロードのデシリアライズに対して強化し、コードの安全性を高めます。 azure-functions SDKは厳格モードをベストプラクティスとして宣伝しており、この記事では後方互換性のあるステップから段階的に導入する方法を解説します。

この機能は、連携して動作する2つのパッケージで提供されます。

  • azure-functions 中央集約型シリアライザー(df_dumps / df_loads)にオプションの型検証および厳格型付けサポートを提供します。
  • azure-functions-durable は、Durable Functions のすべてのペイロードのシリアル化をこれらのシリアライザーを介して行い、オーケストレーション API とエンティティ API に expected_type パラメーターと型の自動検出を追加します。

Durable Functionsがどのデータを永続化し、カスタム型がどのようにシリアライズされるかの背景については、「Durable Functionsにおけるデータ永続性とシリアライズ」を参照してください。

変更内容

この機能以前は、保存されたJSONに埋め込まれた__module__および__class__フィールドを読み取り、クラスを特定するためにDurable Functionsimportlib.import_module()を呼び出してカスタムオブジェクトのペイロードをデシリアライズしていました。 ペイロードのクラスがコードの期待するタイプと一致しているかどうかのチェックはありませんでした。

型安全なシリアル化では、次が追加されます:

  • ペイロードをデシリアライズするオーケストレーションおよびエンティティAPIに関するオプションの expected_type 引数。
  • v2で装飾されたアクティビティやサブオーケストレーター関数のリターン型注釈を読み取り、コードを変更せずにそれをとして使うexpected_type機能です。
  • 環境変数でオプトインしたAZURE_FUNCTIONS_DURABLE_STRICT_TYPING、型の不一致をハードエラーに変換し、importlib.import_module()を呼び出さずにカスタムオブジェクトをデシリアライズします。

シリアライズ形式は変更されていません。 組み込み型は依然として単純なJSONにシリアライズされ、カスタムオブジェクトは引き続き {"__class__", "__module__", "__data__"} の慣例を使います。 つまり、ルーズ モードは完全な下位互換性を備えており、既存の履歴や実行中のオーケストレーションは、これまでと同様にデシリアライズされ続けます。

前提条件

  • 既存のDurable Functionsアプリで、Pythonプログラミングモデル(v1またはv2)を使用しています。

  • 一元化された df_dumps / df_loads シリアライザーを含む、以下の最小パッケージ バージョン:

    Python バージョン 最低 azure-functions バージョン
    3.13以降 2.2.0
    3.10 – 3.12 1.26.0
  • azure-functions-durable 1.6.0以降のバージョンです。

Note

インストール済みのazure-functionsパッケージがdf_dumps / df_loadsを提供していない場合、Durable Functionsレガシーのシリアライゼーションパイプラインにフォールバックします。 永続的なJSON形式は変わりませんが、 expected_type 引数や厳密モードは影響しません。 前の表のバージョンにアップグレードして型検証シリアライズを有効にしてください。

ルーズモードと厳格モードの比較

型式安全シリアライゼーションには2つのモードがあります。

Behavior ルースモード(デフォルト) 厳格モード
オプトイン 常時オン AZURE_FUNCTIONS_DURABLE_STRICT_TYPING1true、または yes に設定します
型が一致しません。 警告を記録し、その後レガシーデコーダーに戻します 昇給 TypeError
カスタムオブジェクトデコード importlib.import_module() を使用する(レガシー パス) expected_type.from_json() を直接呼び出します。import_module は決して呼び出しません。
to_json / from_json 契約 変更なし 対称的で、ネイティブにJSONシリアライズ可能なデータを生成する必要があります( Update to_jsonおよび from_json を参照)
後方互換性 はい No. 必要なコード変更

ルースモードは正しくタイプされたペイロードの挙動が変わらないため、すぐに採用しても安全です。 厳格モードは意図的でセキュリティ強化のための変更であり、その後の移行ステップが必要です。

段階的に移行してください

型安全なシリアル化を段階的に採用しましょう。 ステップ1と2は後方互換性があり、単体で安全に発送できます。 厳格モードを有効にする準備ができたときに、ステップ3と4を完了してください。

ステップ1:パッケージのアップグレード

アプリの要件を Prerequisitesの最低バージョンに更新してください。 例えば、 requirements.txtでは:

azure-functions>=2.2.0
azure-functions-durable>=1.6.0

アップグレード後もアプリはルーズモードで動作し続け、挙動の変化はありません。 既存のアプリを動かし続けるために他に変更を加える必要はありません。

ステップ2:ルーズモード型検証を採用する

ルースモードでは、SDKがデシリアライズされたペイロードを検証し、不一致があれば警告を記録できるように、期待される型を提供してください。 タイプは3つの方法で供給でき、必要に応じて混ぜることができます。

アクティビティおよびサブ オーケストレーターに戻り値型のアノテーションを追加します。 Python v2プログラミングモデルでは、SDKがリターン注釈を自動的に検出し、それを使って結果を検証します。 コールサイトの変更は必要ありません。

@myApp.activity_trigger(input_name="city")
def get_weather(city: str) -> WeatherReport:
    return WeatherReport(city=city, temperature_c=21)


@myApp.orchestration_trigger(context_name="context")
def orchestrator(context: df.DurableOrchestrationContext):
    # The WeatherReport return annotation on get_weather is discovered
    # automatically and used to validate the result.
    report = yield context.call_activity("get_weather", "Seattle")
    return report.temperature_c

expected_type を明示的に渡します。 明示的な expected_type が発見された注釈よりも優先されます。 リターン型が具体的なクラスでない場合に使うと良いです。 例えば、 list[Order]Optional[Order] のような一般的な別名は自動的に発見できません。

orders = yield context.call_activity("get_orders", customer_id, expected_type=list)

expected_typeの議論は以下のオーケストレーションAPIで利用可能です:

  • call_activity および call_activity_with_retry
  • call_sub_orchestrator および call_sub_orchestrator_with_retry
  • call_entity
  • wait_for_external_event
  • get_input

そしてこれらのエンティティAPIに関しては、 DurableEntityContextを通じて:

  • get_state
  • get_input

トリガーにオーケストレーション入力タイプを宣言します。 input_typeorchestration_triggerの引数を使い、context.get_input()入力を検証します。 expected_type 上の呼び出し元 get_input() が優先されます。

@myApp.orchestration_trigger(context_name="context", input_type=OrderRequest)
def orchestrator(context: df.DurableOrchestrationContext):
    request = context.get_input()  # validated against OrderRequest
    ...

このステップの後、アプリを実行し、 azure.functions.DurableFunctions ロガーのログでタイプミスマッチ警告をチェックしてください。 厳格モードに進む前に警告を解決してください。 このステップは警告を追加するだけなので、単独で展開しても安全です。

Tip

自動型検出は具体的な type オブジェクトのみを解決します。 list[Order]dict[str, Order]Optional[Order]などの一般的な別名は「型情報なし」に解決され、復号はモジュールのみの解決に戻されます。 これらの形状の検証が必要な場合は、明確に expected_type を提示してください。

ステップ 3: 厳格モード向けに to_json と from_json を更新する

厳格モードはカスタムタイプの契約を変更します。 厳密モードでは、to_json() は、json.dumps がネイティブにシリアライズできる値 (辞書、リスト、文字列、数値、ブール値、または None など) を返す必要があります。 ネストされたカスタムオブジェクトはインスタンスとして返すのではなく明示的にシリアライズし、対称的に再構築 from_json() なければなりません。

この要件により、各ネスティングレベルで保存されたペイロードから __module__ 文字列が削除されるため、デシリアライゼーションはもはやペイロードデータから型名を解決する必要がなくなりました。

class Order:
    def __init__(self, item, hat):
        self.item = item
        self.hat = hat

    @staticmethod
    def to_json(obj):
        return {
            "item": obj.item,
            "hat": Hat.to_json(obj.hat),   # explicit, not obj.hat
        }

    @staticmethod
    def from_json(data):
        return Order(
            item=data["item"],
            hat=Hat.from_json(data["hat"]),  # symmetric
        )

展開時にフライト化されたレガシー ペイロードを扱う。 もしアプリがアップグレード前にルーズモードで書かれたペイロードをまだ読み取る可能性があるなら、両方の形状を許容 from_json してください。 緩やかにエンコードされたネスト値はすでに再構築されたインスタンスとして届きます(レガシー object_hook が発動します)、厳密に符号化された値はプレーンdictとして表示されます。

    @staticmethod
    def from_json(data):
        hat_data = data["hat"]
        if isinstance(hat_data, Hat):
            hat = hat_data                 # loose-encoded: object already built
        else:
            hat = Hat.from_json(hat_data)  # strict-encoded: plain dict
        return Order(item=data["item"], hat=hat)

ステップ4:厳格モードを有効にする

AZURE_FUNCTIONS_DURABLE_STRICT_TYPINGアプリケーション設定を1true、またはyes(大文字を区別しません)に設定してください。

ローカルのlocal.settings.jsonでは:

{
  "Values": {
    "AZURE_FUNCTIONS_DURABLE_STRICT_TYPING": "true"
  }
}

または、関数アプリのアプリケーション設定として:

az functionapp config appsettings set --name <APP_NAME> --resource-group <RESOURCE_GROUP> --settings AZURE_FUNCTIONS_DURABLE_STRICT_TYPING=true

厳密モードの場合:

  • 型の不一致が発生した場合、警告をログに記録するのではなく、TypeError を発生させます。
  • カスタムオブジェクトは expected_type.from_json() を直接呼び出すことでデシリアライズされるため、 import_module は決して使用されません。
  • なしでカスタム オブジェクトをデシリアライズする呼び出し箇所では、expected_type が発生します。 厳格モードを有効にする前に、すべての通話サイトが ステップ2 のメカニズムのいずれかでタイプを提供していることを確認してください。
  • アクティビティ関数 の入力 はカスタムオブジェクトにはできません。 以下の注意を参照してください。

Important

厳密モードでは、アクティビティ関数の 入力 はカスタムオブジェクトにはなりません。 ホストがアクティビティを呼び出すと、 azure-functions アクティビティトリガーコンバーターは expected_typeなしで入力をデシリアライズします。これは、Functionsワーカーがアクティビティのパラメータタイプの注釈をコンバーターに転送しないためです。 したがって、カスタムオブジェクト入力は ValueErrorで失敗します。 代わりに、アクティビティ入力は辞書、リスト、文字列、数字、ブール値、 Noneなど、ネイティブにJSONシリアライズ可能な値として渡します。 カスタムオブジェクトを送る必要がある場合は、呼び出し前に to_json() メソッドで変換し、アクティビティ内の from_json() で再構築してください。 この制限は活動入力にのみ適用されます。 アクティビティのリターン値、オーケストレーションやエンティティ入力、エンティティの状態、外部イベントペイロードはすべて、タイプを指定すると厳密モードでカスタム型をサポートしています。

Important

すべてのアプリインスタンスがアップグレードされ、緩やかにエンコードされた履歴を持つオンフライトオーケストレーションがすべてクリアされたり、 from_json メソッドが両方の形状を許容する(ステップ3)後にのみ厳格モードを有効にしてください。 アップグレード前に開始されたオーケストレーションは、元の緩い形式でエンコードされた履歴を再生します。 もし厳格モードでその履歴をデコードできなければ、リプレイは失敗します。

既存のオーケストレーションに対するバージョン管理の影響

タイプセーフなシリアライゼーションへのアップデートは、ペイロードタイプがレガシー実装から変わると実行中のオーケストレーションが壊れます。 オーケストレーションが続くたびに、その保存された歴史が再生されます。 もしデコードサイトが古いペイロードが保存していた型と一致しないことを期待している場合、厳密モードは履歴が最初に書かれたときに存在しなかった TypeError を上げ、その新たなエラーがオーケストレーションを壊します。 このミスマッチを引き起こす一般的な移動変更は2つあります。

  • 以前は複数の型を持っていた経路。 もし単一のデシリアライズパス(例えばアクティビティ結果)が以前は異なるオブジェクトタイプを返すことがあり、現在は単一の expected_typeで注釈を付けた場合、異なるタイプの保存されたペイロードはもはやマッチせずデコードに失敗します。
  • アクティビティ入力として使われるカスタムタイプ。 厳密モードではアクティビティ入力がカスタムオブジェクトではないため、厳密モードを採用するにはそれらの入力をJSONシリアライズ可能な値に変更し、実行中のインスタンスが保持していたペイロード形状が変わります。

より一般的には、ペイロードの保存型がデコードサイトが現在期待する型と異なる変更も同じエラーを引き起こします。 例えば、インスタンスが永続化された後にカスタムクラスの名前を変更したり移動したりすると、同じ不一致が生じます。

安全に移行するには、以下のいずれかの方法を用いてください。

  • 推奨: オーケストレーションのバージョン管理を利用して、ロールアウトを分割してください。 バージョンマッチング戦略でStrictを使い、新しい厳格モードのワーカーが新バージョンで始まったオーケストレーションのみを処理するようにしましょう。 このベストプラクティスにより、 ローリングアップグレード 中に両バージョンが共存し、リプレイ失敗を避けられます。
  • 代替案としては、まず排水してください。 すべての機内オーケストレーションが終了するまで待ってから、厳格モードを有効にしてください。

本番環境で厳密モードを有効にする前に、すべてのカスタムオブジェクトデコードサイトが型を提供していること、そしてカスタムクラスがインスタンスがペイロードを永続化していた時と同じ名前とモジュールを維持しているかを確認してください。

実行中のオーケストレーションに影響を与える変更を安全にデプロイするためのより広範なガイダンスについては、「Versioning in Durable Functions」を参照してください。

セキュリティ強化

厳密モードはカスタムオブジェクトペイロードのデシリアライズ方法を強化します。 保存されたペイロードやインバウンドに埋め込まれたモジュール名やクラス名を信頼して型を特定する代わりに、厳密モードはコードが提供する expected_type を使ってカスタムオブジェクトを再構築し、厳格モードの to_json() 出力はどのネスティングレベルでもモジュール名を永続化しません。 この変更により、デシリアライズ時にペイロードデータから任意の型名を解決する必要がなくなり、ペイロードに搭載された型情報に依存するよりも防御面での改善が実現しました。

もしペイロードに機密データが含まれている可能性があるなら、『 機密データ扱い』もレビューしてください。