Wir alle lieben es, glänzende neue Tools zu haben, hassen aber die lästige Pflicht, sie ständig zu aktualisieren. Dies gilt für alles: Betriebssysteme, Apps, APIs, Linux-Pakete. Es ist schmerzhaft, wenn unser Code aufgrund eines Updates nicht mehr funktioniert, und es ist doppelt so schmerzhaft, wenn das Update nicht einmal von uns initiiert wurde.
Bei der Web-API-Entwicklung besteht ständig das Risiko, dass der Code Ihrer Benutzer bei jedem neuen Update beschädigt wird. Wenn es sich bei Ihrem Produkt um eine API handelt, werden diese Updates jedes Mal erschreckend sein. Die Hauptprodukte von Monite sind unsere API und unser White-Label-SDK. Da wir ein API-First-Unternehmen sind, legen wir großen Wert darauf, dass unsere API stabil und benutzerfreundlich bleibt. Daher steht das Problem der Breaking Changes ganz oben auf unserer Prioritätenliste.
Eine gängige Lösung besteht darin, Ihren Kunden veraltete Warnungen zukommen zu lassen und Breaking Changes selten zu veröffentlichen. Plötzlich können Ihre Veröffentlichungen Monate dauern und einige Funktionen müssen bis zur nächsten Veröffentlichung verborgen bleiben oder sogar nicht zusammengeführt werden. Dies verlangsamt Ihre Entwicklung und zwingt Ihre Benutzer, ihre Integration alle paar Monate zu aktualisieren.
Wenn Sie Releases schneller machen, müssen Ihre Benutzer ihre Integration zu oft aktualisieren. Wenn Sie die Zeit zwischen den Veröffentlichungen verlängern, werden Sie als Unternehmen langsamer vorankommen. Je unbequemer Sie es für die Benutzer machen, desto bequemer wird es für Sie sein und umgekehrt. Dies ist sicherlich kein optimales Szenario. Wir wollten uns in unserem eigenen Tempo weiterentwickeln, ohne bestehende Kunden zu beeinträchtigen, was mit einem regulären Einstellungsansatz unmöglich wäre. Aus diesem Grund haben wir uns für eine alternative Lösung entschieden: API-Versionierung.
Die Idee ist ganz einfach: Geben Sie alle wichtigen Änderungen jederzeit frei, verstecken Sie sie jedoch unter einer neuen API-Version. Es bietet Ihnen das Beste aus beiden Welten: Die Integrationen der Benutzer werden nicht routinemäßig unterbrochen und Sie können sich mit jeder gewünschten Geschwindigkeit bewegen. Die Benutzer migrieren, wann immer sie wollen – ganz ohne Druck.
In Anbetracht der Einfachheit der Idee scheint sie für jedes Unternehmen perfekt zu sein. Das ist es, was Sie in einem typischen Ingenieurblog erwarten würden. Leider ist es nicht so einfach.
API-Versionierung ist schwierig, sehr schwierig. Seine illusorische Einfachheit verschwindet schnell, sobald man mit der Umsetzung beginnt. Leider warnt Sie das Internet nie wirklich, da es überraschend wenige Ressourcen zu diesem Thema gibt. Die absolute Mehrheit von ihnen streitet darüber, wo die API-Version platziert werden soll, aber nur wenige wenige Artikel versuchen zu antworten: „Wie implementieren sie?“. Die häufigsten sind:
Getrennte Bereitstellungen können sehr teuer und schwer zu unterstützen sein, das Kopieren einzelner Routen lässt sich nicht sehr gut auf große Änderungen skalieren und das Kopieren der gesamten Anwendung erzeugt so viel zusätzlichen Code, dass Sie bereits nach wenigen Versionen darin ertrinken werden.
Selbst wenn Sie versuchen, das günstigste zu wählen, wird die Belastung durch die Versionierung bald nachholen. Auf den ersten Blick wird es sich einfach anfühlen: Fügen Sie hier ein weiteres Schema, dort einen weiteren Zweig in der Geschäftslogik hinzu und duplizieren Sie am Ende ein paar Routen. Wenn jedoch genügend Versionen vorhanden sind, wird Ihre Geschäftslogik schnell unüberschaubar, viele Ihrer Entwickler werden Anwendungsversionen und API-Versionen verwechseln und beginnen, die Daten in Ihrer Datenbank zu versionieren, und Ihre Anwendung wird nicht mehr zu warten sein.
Sie hoffen vielleicht, dass Sie nie mehr als zwei oder drei API-Versionen gleichzeitig haben; dass Sie alte Versionen alle paar Monate löschen können. Dies gilt, wenn Sie nur eine kleine Anzahl interner Verbraucher unterstützen. Kunden außerhalb Ihres Unternehmens werden jedoch nicht die Erfahrung machen müssen, alle paar Monate ein Upgrade durchführen zu müssen.
Die API-Versionierung kann schnell zu einem der teuersten Teile Ihrer Infrastruktur werden, daher ist es wichtig, im Vorfeld sorgfältige Recherchen durchzuführen. Wenn Sie nur interne Verbraucher unterstützen, haben Sie es vielleicht einfacher mit etwas wie GraphQL, aber es kann schnell genauso teuer werden wie die Versionierung.
Wenn Sie ein Startup sind, wäre es ratsam, die API-Versionierung auf die späteren Phasen Ihrer Entwicklung zu verschieben, wenn Sie über die Ressourcen verfügen, es richtig zu machen. Bis dahin könnten Abwertungen und additive Änderungsstrategien ausreichen. Ihre API wird nicht immer großartig aussehen, aber Sie sparen zumindest eine Menge Geld, indem Sie die explizite Versionierung vermeiden.
Nach ein paar Versuchen und vielen Fehlern standen wir am Scheideweg: Unsere oben erwähnten früheren Versionierungsansätze waren zu teuer in der Wartung. Als Ergebnis unserer Schwierigkeiten habe ich die folgende Liste von Anforderungen erstellt, die an ein perfektes Versionierungs-Framework gestellt werden:
Leider gab es kaum oder gar keine Alternativen zu unseren bestehenden Ansätzen. Da kam mir eine verrückte Idee: Was wäre, wenn wir versuchen würden, etwas Anspruchsvolles, etwas Perfektes für den Job zu entwickeln – so etwas wie die API-Versionierung von Stripe?
Als Ergebnis unzähliger Experimente haben wir jetzt Cadwyn: ein Open-Source-API-Versionierungs-Framework, das nicht nur den Ansatz von Stripe implementiert, sondern maßgeblich darauf aufbaut. Wir werden über die Fastapi- und Pydantic-Implementierung sprechen, aber die Kernprinzipien sind sprach- und rahmenunabhängig.
Das Problem aller anderen Versionierungsansätze besteht darin, dass wir zu viel duplizieren. Warum sollten wir die gesamte Route, den Controller oder sogar die Anwendung duplizieren, wenn nur ein winziger Teil unseres Vertrags gebrochen wurde?
Wenn API-Betreuer mit Cadwyn eine neue Version erstellen müssen, wenden sie die bahnbrechenden Änderungen auf ihre neuesten Schemata, Modelle und Geschäftslogiken an. Dann erstellen sie eine Versionsänderung – eine Klasse, die alle Unterschiede zwischen der neuen Version und einer früheren Version kapselt.
Nehmen wir zum Beispiel an, dass unsere Kunden früher einen Benutzer mit einer Adresse erstellen konnten, jetzt möchten wir ihnen jedoch erlauben, mehrere Adressen anstelle einer einzigen anzugeben. Die Versionsänderung würde so aussehen:
class ChangeUserAddressToAList(VersionChange): description = ( "Renamed `User.address` to `User.addresses` and " "changed its type to an array of strings" ) instructions_to_migrate_to_previous_version = ( schema(User).field("addresses").didnt_exist, schema(User).field("address").existed_as(type=str), ) @convert_request_to_next_version_for(UserCreateRequest) def change_address_to_multiple_items(request): request.body["addresses"] = [request.body.pop("address")] @convert_response_to_previous_version_for(UserResource) def change_addresses_to_single_item(response): response.body["address"] = response.body.pop("addresses")[0]
instructions_to_migrate_to_ previous_version werden von Cadwyn verwendet, um Code für ältere API-Versionen von Schemas zu generieren, und die beiden Konverterfunktionen sind der Trick, der es uns ermöglicht, so viele Versionen zu verwalten, wie wir möchten. Der Vorgang sieht wie folgt aus:
Nachdem unsere API-Betreuer die Versionsänderung erstellt haben, müssen sie sie zu unserem VersionBundle hinzufügen, um Cadwyn mitzuteilen, dass diese Versionsänderung in einer Version enthalten sein wird:
VersionBundle( Version( date(2023, 4, 27), ChangeUserAddressToAList ), Version( date(2023, 4, 12), CollapseUserAvatarInfoIntoAnID, MakeUserSurnameRequired, ), Version(date(2023, 3, 15)), )
Das ist es: Wir haben eine bahnbrechende Änderung hinzugefügt, aber unsere Geschäftslogik verarbeitet nur eine einzige Version – die neueste. Selbst nachdem wir Dutzende von API-Versionen hinzugefügt haben, ist unsere Geschäftslogik immer noch frei von Versionierungslogik, ständigem Umbenennen, Wenns und Datenkonvertern.
Versionsänderungen hängen von der öffentlichen Schnittstelle der API ab und wir fügen fast nie Breaking Changes in bestehende API-Versionen ein. Das bedeutet, dass die Version, sobald wir sie veröffentlicht haben, nicht beschädigt wird.
Da Versionsänderungen Breaking Changes innerhalb von Versionen beschreiben und es keine Breaking Changes in alten Versionen gibt, können wir sicher sein, dass unsere Versionsänderungen völlig unveränderlich sind – es wird nie einen Grund für Änderungen geben. Unveränderliche Entitäten sind viel einfacher zu warten, als wenn sie Teil der Geschäftslogik wären, da sie sich ständig weiterentwickelt. Versionsänderungen werden auch nacheinander angewendet – sie bilden eine Kette von Transformatoren zwischen Versionen, die jede Anfrage auf jede neuere Version und jede Antwort auf jede ältere Version migrieren kann.
API-Verträge sind viel komplexer als nur Schemata und Felder. Sie bestehen aus allen Endpunkten, Statuscodes, Fehlern, Fehlermeldungen und sogar Verhaltensweisen der Geschäftslogik. Cadwyn verwendet dasselbe DSL, das wir oben beschrieben haben, um Endpunkte und Statuscodes zu verwalten, aber Fehler und Verhaltensweisen der Geschäftslogik sind eine andere Geschichte: Sie können nicht mit einem DSL beschrieben werden, sie müssen in die Geschäftslogik eingebettet werden.
Dies macht die Wartung solcher Versionsänderungen viel teurer als alle anderen, da sie sich auf die Geschäftslogik auswirken. Wir nennen diese Eigenschaft einen „Nebeneffekt“ und versuchen sie wegen ihres Wartungsaufwands unbedingt zu vermeiden. Alle Versionsänderungen, die die Geschäftslogik ändern möchten, müssen als Nebenwirkungen gekennzeichnet werden. Dies dient dazu, herauszufinden, welche Versionsänderungen „gefährlich“ sind:
class RequireCompanyAttachedForPayment(VersionChangeWithSideEffects): description = ( "User must now have a company_id in their account " "if they want to make new payments" )Außerdem können API-Betreuer überprüfen, ob die Client-Anfrage eine API-Version verwendet, die diesen Nebeneffekt enthält:
class RequireCompanyAttachedForPayment(VersionChangeWithSideEffects): description = ( "User must now have a company_id in their account " "if they want to make new payments" )Keine Silberkugeln
Die Last der Versionierung besteht jedoch immer noch und selbst ein ausgefeiltes Framework ist kein Allheilmittel. Wir tun unser Bestes, die API-Versionierung nur dann zu verwenden, wenn dies unbedingt erforderlich ist. Wir versuchen auch, unsere API beim ersten Versuch korrekt zu machen, indem wir einen speziellen „API Council“ haben. Alle wesentlichen API-Änderungen werden dort von unseren besten Entwicklern, Testern und technischen Autoren überprüft, bevor mit der Implementierung begonnen wird.
Besonderer Dank geht an Brandur Leach für seinen Artikel zur API-Versionierung bei Stripe und für die Hilfe, die er mir bei der Implementierung von Cadwyn gewährt hat: Ohne seine Hilfe wäre das nicht möglich.
Haftungsausschluss: Alle bereitgestellten Ressourcen stammen teilweise aus dem Internet. Wenn eine Verletzung Ihres Urheberrechts oder anderer Rechte und Interessen vorliegt, erläutern Sie bitte die detaillierten Gründe und legen Sie einen Nachweis des Urheberrechts oder Ihrer Rechte und Interessen vor und senden Sie ihn dann an die E-Mail-Adresse: [email protected] Wir werden die Angelegenheit so schnell wie möglich für Sie erledigen.
Copyright© 2022 湘ICP备2022001581号-3