Migrace na typově bezpečné serializaci v Durable Functions for Python

Tento článek vám ukáže, jak zavést typově bezpečné (také nazývané typově nevědomé) serializaci payload v existující aplikaci Durable Functions, která využívá programovací model Python. Typově bezpečná serializace ověřuje deserializované datové zátěže proti očekávanému typu a umožňuje aktivovat zesílený režim strict mode, který odstraňuje riziko deserializace nedůvěryhodné datové zátěže.

Přijetí typově bezpečné serializace je doporučenou nejlepší praxí pro každou aplikaci Durable Functions, která používá Python, včetně aplikací, které nejsou citlivé na bezpečnost. Pomáhá vám to včas odhalit chyby způsobené nesouladem typů, protože SDK ověřuje každou datovou položku podle typu, který váš kód očekává, místo aby tiše rekonstruovalo typ, který je u uložených dat uveden. Přísný režim navíc odolní vaší aplikaci proti deserializaci nedůvěryhodného payloadu, což činí váš kód bezpečnějším. SDK propaguje azure-functions přísný režim jako nejlepší postup a tento článek vás provede postupným zaváděním, začínaje zpětně kompatibilními kroky.

Funkce je poskytována ve dvou balíčcích, které spolupracují:

  • azure-functions poskytuje centralizované serializátory (df_dumps / df_loads) s volitelnou validací typů a podporou přísného typování.
  • azure-functions-durable směruje veškerou serializaci datové části v Durable Functions prostřednictvím těchto serializátorů a přidává parametr expected_type a automatické zjišťování typů do rozhraní API pro orchestraci a entity.

Informace o tom, jaká data Durable Functions uchovává a jak se serializují vlastní typy, najdete v článku Uchovávání dat a serializace v Durable Functions.

Co se změní

Před zavedením této funkce Durable Functions deserializovaly payloady vlastních objektů čtením polí __module__ a __class__ vložených do uloženého formátu JSON a voláním importlib.import_module() za účelem nalezení třídy. Nebyla žádná kontrola, zda třída v payloadu odpovídá typu, který váš kód očekával.

Typově bezpečná serializace přidává:

  • Volitelný argument expected_type v rozhraních API pro orchestraci a entity, která deserializují datovou část.
  • Automatické rozpoznání typu, které čte anotaci návratového typu vašich funkcí aktivit a suborchestrátorových funkcí dekorovaných pomocí v2 a používá ji jako expected_type bez jakékoli změny kódu.
  • Přísný režim, který se aktivuje pomocí proměnné prostředí AZURE_FUNCTIONS_DURABLE_STRICT_TYPING, považuje nesoulad typů za závažné chyby a deserializuje uživatelsky definované objekty bez volání importlib.import_module().

Formát serializace zůstává nezměněn. Vestavěné typy se stále serializují jako prostý JSON a vlastní objekty stále používají konvenci {"__class__", "__module__", "__data__"}. To znamená, že volný režim je plně zpětně kompatibilní: existující historie a orchestrace během letu pokračují v deserializaci jako dříve.

Předpoklady

  • Existující aplikace Durable Functions, která používá programovací model Python (v1 nebo v2).

  • Následující minimální verze balíčků, které obsahují centralizované serializátory df_dumps / df_loads:

    verze Pythonu Minimální azure-functions verze
    3.13 a později 2.2.0
    3.10 – 3.12 1.26.0
  • azure-functions-durable 1.6.0 nebo později.

Poznámka

Pokud nainstalovaný balíček azure-functions neposkytuje df_dumps / df_loads, Durable Functions použije starší proces serializace. Přetrvávající formát JSON zůstává stejný, ale expected_type argument a přísný režim nemají žádný vliv. Upgradujte na verze uvedené v předchozí tabulce pro umožnění typově validované serializace.

Volný režim oproti přísnému režimu

Typově bezpečná serializace má dva režimy.

Chování Volný režim (výchozí) Striktní režim
Přihlásit se Vždy zapnuto Nastavte AZURE_FUNCTIONS_DURABLE_STRICT_TYPING na 1, true, nebo yes
Neshoda typu Zapíše varování do protokolu a poté přejde na starší dekodér Zvýšení platu TypeError
Dekódování pomocí vlastních objektů Používá importlib.import_module() (zastaralá cesta) Volá expected_type.from_json() přímo; nikdy nevolá import_module
to_json / from_json smlouva Nezměněný Musí být symetrický a generovat nativně JSON-serializovatelná data (viz Update to_json a from_json)
Zpětná kompatibilita Ano Ne. Vyžaduje změny kódu

Volný režim lze bezpečně okamžitě zavést, protože nikdy nemění chování správně typovaných datových částí. Přísný režim je záměrná, bezpečnostní změna, která vyžaduje následující migrační kroky.

Migrujte postupně

Přijměte typově bezpečné serializaci ve fázích. Kroky 1 a 2 jsou zpětně kompatibilní a lze je bezpečně nasadit samostatně. Kroky 3 a 4 dokončujte jen tehdy, když budete připraveni zapnout přísný režim.

Krok 1: Upgradujte balíčky

Aktualizujte požadavky aplikace na minimální verze uvedené v části Požadavky. Například v requirements.txt:

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

Po upgradu aplikace pokračuje v volném režimu bez změny chování. Nemusíte dělat žádné další změny, abyste udrželi stávající aplikaci funkční.

Krok 2: Přijměte validaci typů ve volném režimu

V volném režimu uveďte očekávaný typ, aby SDK mohlo ověřit deserializované payloady a zaznamenat varování při případném nesouladu. Typ můžete zadat třemi způsoby a podle potřeby je kombinovat.

Přidejte anotace návratového typu k aktivitám a suborchestrátorům. V programovacím modelu Python v2 SDK automaticky objeví návratovou anotaci a použije ji k ověření výsledku. Není potřeba měnit místo hovoru.

@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

Předejte expected_type explicitně. Explicitní expected_type má přednost před objevenou anotací. Použijte to, když návratový typ není konkrétní třídou. Například obecné aliasy jako list[Order] nebo Optional[Order] nelze automaticky objevit.

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

Argument expected_type je dostupný na těchto orchestračních API:

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

A v rámci těchto API entit, prostřednictvím DurableEntityContext:

  • get_state
  • get_input

Na spouštěči označte typ orchestrace vstupu. Použijte u input_type argument orchestration_trigger, aby context.get_input() ověřil vstup. Místo volání expected_type na get_input() má přednost.

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

Po tomto kroku spusťte aplikaci a sledujte logy, zda se u protokolovače azure.functions.DurableFunctions neobjeví varování na nesoulad typů. Odstraňte všechna varování, než přejdete do přísného režimu. Protože tento krok přidává jen varování, je bezpečné ho nasadit samostatně.

Tip

Automatické objevování typů řeší pouze konkrétní type objekty. Obecné aliasy, jako jsou list[Order], dict[str, Order] a Optional[Order], se vyhodnotí jako „bez informací o typu“ a dekódování se vrátí k rozlišení pouze podle modulu. Dodajte explicitně expected_type , když potřebujete ověření těchto tvarů.

Krok 3: Aktualizuj to_json a from_json pro přísný režim

Přísný režim mění smlouvu pro vlastní typy. Ve striktním režimu musí to_json() vracet hodnotu, kterou lze v json.dumps nativně serializovat, například slovníky, seznamy, řetězce, čísla, booleovské hodnoty nebo None. Musíte explicitně serializovat vnořené vlastní objekty místo jejich vracení jako instance a from_json() musíte je rekonstruovat symetricky.

Tento požadavek odstraňuje řetězce __module__ z uložených datových částí na všech úrovních vnoření, takže deserializace už nemusí vyhodnocovat názvy typů z datové části.

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
        )

Zpracovat probíhající starší datové zátěže během nasazení. Pokud vaše aplikace stále čte payloady, které byly před upgradem napsány volně, nechte from_json tolerovat oba tvary. Volně kódovaná vnořená hodnota přichází jako už rekonstruovaná instance (aktivuje se starší mechanismus object_hook), zatímco přísně kódovaná hodnota přichází jako prostý slovník.

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

Krok 4: Zapněte přísný režim

Nastavte nastavení aplikace AZURE_FUNCTIONS_DURABLE_STRICT_TYPING na 1, true nebo yes (nerozlišují se malá a velká písmena).

V místním souboru local.settings.json:

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

Nebo jako nastavení aplikace ve vaší aplikaci funkcí:

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

V přísném režimu:

  • Nesoulad typů vyvolá TypeError místo protokolování varování.
  • Vlastní objekty jsou deserializovány přímým voláním expected_type.from_json() , takže import_module se nikdy nepoužívají.
  • Jakékoli místo volání, které deserializuje vlastní objekt bezexpected_type, vyvolá TypeError. Ujistěte se, že každé takové místo volání specifikuje typ pomocí jednoho z mechanismů v Step 2, než zapnete přísný režim.
  • Vstupy funkcí aktivity nemohou být vlastní objekty. Podívejte se na následující poznámku.

Important

V přísném režimu nemůže být vstup funkce aktivity uživatelsky definovaný objekt. Když hostitel spustí aktivitu, převodník triggeru azure-functions deserializuje vstup bez expected_type, protože worker procesu Functions nepředává tomuto převodníku anotaci typu parametru aktivity. Vstup vlastního objektu tedy selže s ValueError. Místo toho předáváte vstupy aktivit jako nativně JSON-serializovatelné hodnoty, jako jsou slovníky, seznamy, řetězce, čísla, booleany nebo None. Pokud potřebujete odeslat vlastní objekt, převeďte jej před voláním pomocí metody to_json() a uvnitř aktivity jej znovu vytvořte pomocí from_json(). Toto omezení platí pouze pro vstupy aktivity. Návratové hodnoty aktivit, vstupy orchestrace a entit, stav entity i datové části externích událostí podporují v přísném režimu vlastní typy, když zadáte typ.

Important

Striktní režim zapněte až poté, co jsou všechny instance aplikací upgradovány a všechny orchestrace během letu s volně kódovanou historií jsou vyčerpány, nebo jakmile vaše from_json metody tolerují oba tvary (Krok 3). Orchestrace, která začala před upgradem, znovu přehrává svou původní, volně kódovanou historii. Pokud váš kód nedokáže tuto historii dekódovat v přísném režimu, opakování selže.

Dopady verzování na stávající orchestrace

Aktualizace na typově bezpečnou serializaci přeruší běžící orchestrace, pokud se typy payloadů změní oproti starší implementaci. Pokaždé, když orchestrace pokračuje, přehrává svou uloženou historii. Pokud místo dekódování nyní očekává typ, který neodpovídá tomu, co bylo uloženo ve starší datové zátěži, přísný režim vyvolá chybu TypeError, která se neobjevovala, když byla historie poprvé zapsána, a tato nová chyba rozbije orchestraci. Dvě běžné migrační změny přinášejí tento nesoulad:

  • Cesta, která dříve obsahovala více než jeden typ. Pokud jedna cesta deserializace, například výsledek aktivity, mohla dříve vrátit různé typy objektů a nyní ji anotujete jedním expected_type, uložený payload, který používal jiný typ, už neodpovídá a nedokáže dekódovat.
  • Vlastní typy používané jako vstupy pro aktivity. Protože vstupy aktivit nemohou být ve striktním režimu vlastní objekty, přijetí přísného režimu vyžaduje změnu těchto vstupů na hodnoty serializovatelné v JSON, což mění tvar payloadu, který běžící instance zachovávaly.

Obecněji jakákoli změna, která způsobí, že uložený typ payloadu se liší od typu, který nyní dekódovací site očekává, způsobuje stejnou chybu. Například přejmenování nebo přesun vlastní třídy po zachování jejích instancí zavede stejný nesoulad.

Pro bezpečnou migraci použijte jeden z těchto přístupů:

  • Doporučeno: rozdělte zavádění pomocí verzování orchestrace. Použijte verzování orchestrací se strategií Strict porovnávání verzí, aby vaši noví workeři ve striktním režimu zpracovávali pouze orchestrace, které byly spuštěny v nové verzi. Tato osvědčená praxe umožňuje oběma verzím koexistovat během plynulého upgradu a zabraňuje neúspěchům při přehrávání.
  • Alternativa: nejdřív vylijte. Nechte dokončit všechny orchestrace během letu, poté zapněte přísný režim.

Než v produkci povolíte striktní režim, ověřte, že každé místo dekódování vlastních objektů uvádí typ a že vaše vlastní třídy si zachovávají stejný název a modul, jaké měly v době, kdy běžící instance ukládaly své payloady.

Pro širší doporučení k bezpečnému nasazování změn, které ovlivňují běžící orchestrace, viz Versioning in Durable Functions.

Posílení zabezpečení

Přísný režim zpřísňuje způsob deserializace datových částí uživatelských objektů. Místo toho, aby se důvěřovalo jménům modulů a tříd vloženým do uloženého nebo příchozího payloadu, že najdou typ, přísný režim rekonstruuje vlastní objekty pomocí toho expected_type , co váš kód poskytuje, a výstup v přísném režimu to_json() neuchovává názvy modulů na žádné úrovni vnoření. Tato změna odstraňuje nutnost během deserializace určovat libovolné názvy typů z dat datové části, což představuje dodatečné posílení obrany oproti spoléhání se na informace o typu obsažené v datové části.

Pokud vaše payloady mohou obsahovat citlivá data, zkontrolujte také Work with sensitive data.