Migration zur typesicheren Serialisierung in Durable Functions for Python

Dieser Artikel zeigt Ihnen, wie Sie type-safe (auch type-aware genannt) Payload-Serialisierung in einer bestehenden Durable Functions-App einführen können, die das Python-Programmiermodell verwendet. Typsichere Serialisierung validiert deserialisierte Nutzlasten gegen einen erwarteten Typ und ermöglicht es Ihnen, in einen gehärteten strengen Modus zu wechseln, der ein Deserialisierungsrisiko für unvertrauenswürdige Nutzlasten entfernt.

Die Einführung von typsicherer Serialisierung ist eine empfohlene Best Practice für jede Durable Functions-App, die Python verwendet, einschließlich Apps, die nicht sicherheitssensitiv sind. Es hilft Ihnen, Typfehler frühzeitig zu erkennen, weil das SDK jede Nutzlast gegen den Typ validiert, den Ihr Code erwartet, anstatt stillschweigend den Typ der gespeicherten Daten zu rekonstruieren. Der strenge Modus härtet Ihre App zusätzlich gegen die Deserialisierung nicht vertrauenswürdiger Payloads ab, wodurch Ihr Code sicherer wird. Das azure-functions SDK bewirbt den strengen Modus als Best Practice, und dieser Artikel führt Sie durch die schrittweise Einführung davon, beginnend mit rückwärtskompatiblen Schritten.

Die Funktion wird über zwei Pakete verteilt, die zusammenarbeiten:

  • azure-functions bietet den zentralisierten Serializern (df_dumps / df_loads) optionale Typvalidierung und Unterstützung für strikte Typisierung.
  • azure-functions-durable leitet die gesamte Payload-Serialisierung von Durable Functions über diese Serializer und ergänzt die Orchestrierungs- und Entitäts-APIs um den Parameter expected_type sowie die automatische Typerkennung.

Für Hintergrundinformationen darüber, welche Daten Durable Functions gespeichert werden und wie benutzerdefinierte Typen serialisiert werden, siehe Data Persistence and Serialization in Durable Functions.

Was ändert sich?

Vor der Einführung dieses Features deserialisierte Durable Functions die Nutzdaten benutzerdefinierter Objekte, indem die in dem gespeicherten JSON eingebetteten Felder __module__ und __class__ gelesen und importlib.import_module() aufgerufen wurde, um die Klasse zu ermitteln. Es gab keine Kontrolle, ob die Klasse in der Nutzlast mit dem Typ übereinstimmte, den Ihr Code erwartete.

Typsichere Serialisierung bietet:

  • Ein optionales expected_type Argument bei den Orchestrierungs- und Entitäts-APIs, die eine Payload deserialisieren.
  • Automatische Typermittlung, die die Rückgabetypannotation Ihrer mit v2 dekorierten Aktivitäts- und Suborchestratorfunktionen liest und sie ohne Codeänderung als expected_type verwendet.
  • Ein strenger Modus, der über die Umgebungsvariable AZURE_FUNCTIONS_DURABLE_STRICT_TYPING aktiviert wird, Typinkonsistenzen in schwerwiegende Fehler umwandelt und benutzerdefinierte Objekte deserialisiert, ohne importlib.import_module() aufzurufen.

Das Serialisierungsformat ist unverändert. Eingebaute Typen serialisieren weiterhin zu einfachem JSON, und benutzerdefinierte Objekte verwenden weiterhin die Konvention {"__class__", "__module__", "__data__"} . Das bedeutet, dass der lose Modus vollständig abwärtskompatibel ist: Vorhandene Verläufe und laufende Orchestrierungen werden weiterhin wie bisher deserialisiert.

Voraussetzungen

  • Eine bestehende Durable Functions-App, die das Python-Programmiermodell (v1 oder v2) verwendet.

  • Die folgenden Mindestpaketversionen, die die zentralisierten df_dumps / df_loads Serialisierer ausliefern:

    Python-Version Mindestversion azure-functions
    3.13 und später 2.2.0
    3.10 – 3.12 1.26.0
  • azure-functions-durable 1.6.0 oder später.

Note

Wenn das installierte azure-functions Paket nicht liefertdf_dumps / df_loads, greift Durable Functions auf die Legacy-Serialisierungspipeline zurück. Das bestehende JSON-Format bleibt gleich, aber das Argument und der expected_type strenge Modus haben keine Auswirkung. Aktualisieren Sie auf die Versionen in der vorherigen Tabelle, um eine typvalidierte Serialisierung zu ermöglichen.

Lockerer Modus im Vergleich zum strengen Modus

Typsichere Serialisierung hat zwei Modi.

Behavior Lockerer Modus (Standard) Strenger Modus
Zustimmen Immer aktiviert Setze AZURE_FUNCTIONS_DURABLE_STRICT_TYPING auf 1, true, oder yes
Typ stimmt nicht überein Meldet eine Warnung und greift dann auf den Legacy-Decoder zurück Erhöhungen TypeError
Dekodierung benutzerdefinierter Objekte Verwendet importlib.import_module() (veralteter Pfad) Ruft direkt an expected_type.from_json() ; ruft nie an import_module
to_json / from_json Vertrag Unverändert Muss symmetrisch sein und nativ JSON-serialisierbare Daten erzeugen (siehe Update to_json und from_json).
Abwärtskompatibel Yes No. Erfordert Änderungen am Code

Der Loose-Modus kann bedenkenlos sofort übernommen werden, da er das Verhalten bei korrekt typisierten Nutzdaten niemals ändert. Der strenge Modus ist eine bewusste, sicherheitshärtende Änderung, die die folgenden Migrationsschritte erfordert.

Inkrementelle Migration

Führen Sie eine typsichere Serialisierung in Phasen ein. Die Schritte 1 und 2 sind abwärtskompatibel und können sicher separat verschickt werden. Erledige die Schritte 3 und 4 nur, wenn du bereit bist, den Strict-Mode zu aktivieren.

Schritt 1: Upgrade der Pakete

Aktualisieren Sie die Anforderungen Ihrer App auf die Mindestversionen in den Voraussetzungen. Zum Beispiel in requirements.txt:

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

Nach dem Upgrade läuft deine App weiterhin im Loose-Modus, ohne dass sich das Verhalten ändert. Du musst keine weiteren Änderungen vornehmen, um deine bestehende App am Laufen zu halten.

Schritt 2: Verwenden Sie die Typvalidierung im Loose-Mode

Im lockeren Modus geben Sie den erwarteten Typ an, damit das SDK deserialisierte Nutzlasten validieren und eine Warnung bei Fehlanpassungen protokollieren kann. Man kann den Typ auf drei Arten liefern und nach Bedarf mischen.

Fügen Sie Rückgabe-Anmerkungen zu Aktivitäten und Sub-Orchestratoren hinzu. Im Python v2-Programmiermodell entdeckt das SDK die Rückgabeannotation automatisch und verwendet sie zur Validierung des Ergebnisses. An der Aufrufstelle ist keine Änderung erforderlich.

@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

Übergeben Sie expected_type explizit. Eine explizit angegebene expected_type-Annotation hat Vorrang vor einer erkannten Annotation. Verwende es, wenn der Rückgabetyp keine konkrete Klasse ist. Zum Beispiel können generische Aliase wie list[Order] oder Optional[Order] nicht automatisch entdeckt werden.

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

Das Argument expected_type ist auf diesen Orchestrierungs-APIs verfügbar:

  • call_activity und call_activity_with_retry
  • call_sub_orchestrator und call_sub_orchestrator_with_retry
  • call_entity
  • wait_for_external_event
  • get_input

Und über diese Entitäts-APIs, über DurableEntityContext:

  • get_state
  • get_input

Deklariere den Orchestrierungseingabetyp auf dem Trigger. Verwenden Sie bei orchestration_trigger das Argument input_type, damit context.get_input() die Eingabe validiert. Eine Aufrufstelle expected_type hat Vorrang vor get_input().

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

Nach diesem Schritt führen Sie Ihre App aus und achten Sie in den Protokollen des Loggers azure.functions.DurableFunctions auf Warnungen zu Typinkompatibilitäten. Kläre alle Warnungen, bevor du in den strengen Modus übergehst. Da dieser Schritt nur Warnungen hinzufügt, ist es sicher, ihn eigenständig einzusetzen.

Tip

Automatische Typerkennung löst nur konkrete type Objekte auf. Generische Aliase wie list[Order], dict[str, Order], und Optional[Order] lösen sich auf "keine Typinformation" auf, und die Dekodierung fällt auf die reine modulbezogene Auflösung zurück. Gib explizit an expected_type , wenn du eine Bestätigung für diese Formen brauchst.

Schritt 3: Aktualisieren Sie to_json und from_json für den strengen Modus

Der strenge Modus ändert den Vertrag für benutzerdefinierte Typen. Im strengen Modus muss to_json() einen Wert zurückgeben, den json.dumps nativ serialisieren kann, z. B. Wörterbücher, Listen, Zeichenfolgen, Zahlen, boolesche Werte oder None. Du musst verschachtelte benutzerdefinierte Objekte explizit serialisieren, anstatt sie als Instanzen zurückzugeben, und from_json() symmetrisch rekonstruieren.

Diese Anforderung entfernt __module__ Strings aus gespeicherten Nutzdaten auf allen Verschachtelungsebenen, sodass die Deserialisierung Typnamen nicht mehr aus den Nutzdaten auflösen muss.

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
        )

Verwalten Sie während des Rollouts die In-Flight-Legacy-Nutzlasten. Wenn deine App möglicherweise weiterhin Payloads liest, die vor dem Upgrade im lockeren Modus geschrieben wurden, sollte from_json beide Formate akzeptieren. Ein lose codierter verschachtelter Wert trifft als bereits rekonstruierte Instanz ein (dabei wird das Legacy-object_hook ausgelöst), während ein streng codierter Wert als einfaches Dictionary eintrifft.

    @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)

Schritt 4: Aktiviere den strengen Modus

Setzen Sie die Anwendungseinstellung AZURE_FUNCTIONS_DURABLE_STRICT_TYPING auf 1, true oder yes fest (Groß-/Kleinschreibung wird nicht berücksichtigt).

In Ihrem örtlichen local.settings.json:

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

Oder als Anwendungseinstellung in deiner Funktions-App:

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

Im strengen Modus:

  • Typkonflikte lösen TypeError aus, anstatt eine Warnung zu protokollieren.
  • Benutzerdefinierte Objekte werden durch direkte Aufrufe expected_type.from_json() deserialisiert und import_module werden daher nie verwendet.
  • Jede Aufrufstelle, die ein benutzerdefiniertes Objekt ohne ein expected_type deserialisiert, löst TypeError aus. Stellen Sie sicher, dass jede solche Anrufseite einen Typ über einen der Mechanismen in Schritt 2 bereitstellt, bevor Sie den strengen Modus aktivieren.
  • Aktivitätsfunktionseingaben können keine benutzerdefinierten Objekte sein. Beachten Sie den folgenden Hinweis.

Important

Im strengen Modus kann die Eingabe einer Aktivitätsfunktion kein benutzerdefiniertes Objekt sein. Wenn der Host eine Aktivität aufruft, deserialisiert der azure-functions Aktivitätstriggerkonverter die Eingabe ohne ein expected_type, da der Functions-Worker die Parametertypannotation der Aktivität nicht an den Konverter weiterleitet. Eine benutzerdefinierte Objekteingabe schlägt daher mit einem ValueErrorfehl. Stattdessen werden Aktivitätseingaben als nativ JSON-serialisierbare Werte weitergegeben, wie Wörterbücher, Listen, Zeichenketten, Zahlen, Boolesche Werte oder None. Wenn du ein benutzerdefiniertes Objekt senden musst, konvertiere es vor dem Aufruf mit der Methode to_json() und stelle es innerhalb der Aktivität mit from_json() wieder her. Diese Einschränkung gilt nur für Aktivitätseingaben. Aktivitätsrückgabewerte, Orchestrierung und Entitätseingaben, Entitätszustand und externe Ereignisnutzlasten unterstützen alle benutzerdefinierte Typen im strikten Modus, wenn du einen Typ angibst.

Important

Aktiviere den Strict-Modus erst, nachdem alle App-Instanzen aufgerüstet sind und alle In-Flight-Orchestrierungen mit locker codierten Historien entfernt sind oder deine from_json Methoden beide Formen tolerieren (Schritt 3). Eine Orchestrierung, die vor dem Upgrade begann, spielt ihre ursprüngliche, lose kodierte Geschichte erneut ab. Wenn dein Code diese Geschichte im strengen Modus nicht decodieren kann, schlägt die Wiederholung fehl.

Versionierungsimplikationen für bestehende Orchestrierungen

Ein Update auf typsichere Serialisierung unterbricht laufende Orchestrierungen, wenn sich die Nutzlasttypen von der Altimplementierung ändern. Jedes Mal, wenn eine Orchestrierung fortgesetzt wird, spielt sie ihre gespeicherte Geschichte erneut ab. Wenn eine Dekodierungsstelle jetzt einen Typ erwartet, der nicht zu dem passt, was in einer älteren Payload gespeichert wurde, löst der strikte Modus ein TypeError aus, das nicht vorhanden war, als der Verlauf ursprünglich geschrieben wurde, und dieser neue Fehler bringt die Orchestrierung zum Scheitern. Zwei häufige Migrationsänderungen führen zu dieser Diskrepanz:

  • Ein Weg, der zuvor mehr als einen Typ enthielt. Wenn ein einzelner Deserialisierungspfad, wie ein Aktivitätsergebnis, zuvor verschiedene Objekttypen zurückgeben konnte und man ihn jetzt mit einem einzigen expected_typeannotiert, passt eine gespeicherte Nutzlast, die einen anderen Typ verwendet hat, nicht mehr überein und kann nicht dekodieren.
  • Benutzerdefinierte Typen, die als Aktivitätseingaben verwendet werden. Da Aktivitätseingaben im strikten Modus keine benutzerdefinierten Objekte sein können, erfordert die Einführung des strengen Modus, dass man diese Eingaben in JSON-serialisierbare Werte umwandelt, was die Form der Nutzlast, die laufende Instanzen erhalten haben, verändert.

Allgemeiner gilt: Jede Änderung, die den gespeicherten Typ einer Nutzlast von dem abweicht, den eine Decode-Stelle nun erwartet, verursacht denselben Fehler. Zum Beispiel führt das Umbenennen oder Verschieben einer benutzerdefinierten Klasse, nachdem ihre Instanzen persistiert wurden, zur gleichen Diskrepanz.

Um sicher zu migrieren, verwenden Sie eine dieser Ansätze:

  • Empfohlen: Aufteilung des Rollouts mit Orchestrierungsversionierung. Verwende die Versionierung von Orchestrierungen mit der Strict Strategie für den Versionsabgleich, sodass deine neuen Worker im strikten Modus nur die Orchestrierungen verarbeiten, die mit der neuen Version gestartet wurden. Diese Best Practice ermöglicht, dass beide Versionen während eines Rolling Upgrades koexistieren, und vermeidet Replay-Fehler.
  • Alternative: Zuerst ablassen. Lass alle Orchestrierungen während des Flugs fertig sein und aktiviere dann den strengen Modus.

Bevor Sie den strikten Modus im Produktivbetrieb aktivieren, stellen Sie sicher, dass jede Decodierungsstelle für benutzerdefinierte Objekte einen Typ angibt und dass Ihre benutzerdefinierten Klassen denselben Namen und dasselbe Modul beibehalten, den bzw. das sie zu dem Zeitpunkt hatten, als laufende Instanzen ihre Nutzdaten persistierten.

Weiterführende Hinweise zur sicheren Bereitstellung von Änderungen, die sich auf laufende Orchestrierungen auswirken, finden Sie unter Versioning in Durable Functions.

Sicherheitshärtung

Der strenge Modus stärkt, wie benutzerdefinierte Objektnutzlasten deserialisiert werden. Anstatt darauf zu vertrauen, dass die in einer gespeicherten oder eingehenden Payload eingebetteten Modul- und Klassennamen zur Bestimmung eines Typs verwendet werden, rekonstruiert der Strict Mode benutzerdefinierte Objekte mithilfe von expected_type, das dein Code bereitstellt, und die to_json()-Ausgabe im Strict Mode speichert auf keiner Verschachtelungsebene Modulnamen. Diese Änderung macht es überflüssig, bei der Deserialisierung beliebige Typnamen aus den Nutzdaten aufzulösen, und stellt damit gegenüber der Abhängigkeit von den in den Nutzdaten enthaltenen Typinformationen eine zusätzliche Absicherung im Sinne von Defense in Depth dar.

Wenn Ihre Nutzlasten sensible Daten enthalten können, überprüfen Sie auch "Arbeiten mit sensiblen Daten".