Leser meiner Veröffentlichungen sind wahrscheinlich mit der Idee vertraut, einen API-First-Ansatz zur Entwicklung von Microservices zu verwenden. Unzählige Male habe ich die Vorteile erkannt, die sich daraus ergeben, die erwarteten URIs und zugrunde liegenden Objektmodelle zu beschreiben, bevor mit der Entwicklung begonnen wird.
In meinen über 30 Jahren Erfahrung in der Technologiebranche bin ich jedoch mit der Realität alternativer Abläufe vertraut geworden. Mit anderen Worten: Ich gehe fest davon aus, dass es Situationen geben wird, in denen API First einfach nicht möglich ist.
In diesem Artikel wollte ich anhand eines Beispiels zeigen, wie es Teams, die Microservices erstellen, dennoch gelingen kann, eine OpenAPI-Spezifikation für andere bereitzustellen, ohne manuell eine openapi.json-Datei zu definieren.
Außerdem wollte ich aus meiner Komfortzone heraustreten und dies ohne Java, .NET oder sogar JavaScript tun.
Am Ende der meisten meiner Artikel erwähne ich oft mein persönliches Leitbild:
„Konzentrieren Sie sich auf die Bereitstellung von Features/Funktionalitäten, die den Wert Ihres geistigen Eigentums steigern. Nutzen Sie Frameworks, Produkte und Services für alles andere.“ – J. Vester
Mit diesem Leitbild möchte ich mich dafür einsetzen, meine Zeit optimal zu nutzen, wenn ich versuche, auf höherer Ebene gesetzte Ziele zu erreichen. Wenn unser Fokus darauf liegt, mehr Widgets zu verkaufen, sollte ich meine Zeit grundsätzlich darauf verwenden, Wege zu finden, dies zu erreichen – und Herausforderungen zu vermeiden, die bereits durch vorhandene Frameworks, Produkte oder Services gelöst wurden.
Ich habe Python als Programmiersprache für meinen neuen Microservice ausgewählt. Bis heute waren 99 % des Python-Codes, den ich für meine vorherigen Artikel geschrieben habe, entweder das Ergebnis von Stack Overflow Driven Development (SODD) oder von ChatGPT-gesteuerten Antworten. Python liegt eindeutig außerhalb meiner Komfortzone.
Nachdem ich nun den aktuellen Stand der Dinge geklärt habe, wollte ich mit minimaler Erfahrung in der Quellsprache einen neuen Python-basierten RESTful-Mikroservice erstellen, der meinem persönlichen Leitbild entspricht.
Da habe ich FastAPI gefunden.
FastAPI gibt es seit 2018 und ist ein Framework, das sich auf die Bereitstellung von RESTful-APIs mit Python-ähnlichen Hinweisen konzentriert. Das Beste an FastAPI ist die Möglichkeit, OpenAPI 3-Spezifikationen automatisch zu generieren, ohne dass aus Entwicklersicht zusätzlicher Aufwand entsteht.
Für diesen Artikel kam mir die Idee einer Artikel-API in den Sinn, die eine RESTful-API bereitstellt, mit der Verbraucher eine Liste meiner kürzlich veröffentlichten Artikel abrufen können.
Der Einfachheit halber nehmen wir an, dass ein bestimmter Article
die folgenden Eigenschaften besitzt:
id
– einfache, eindeutige Identifikationseigenschaft (Nummer)title
– der Titel des Artikels (Zeichenfolge)url
– die vollständige URL zum Artikel (Zeichenfolge)year
– das Jahr, in dem der Artikel veröffentlicht wurde (Nummer)
Die Artikel-API umfasst die folgenden URIs:
/articles
– ruft eine Liste von Artikeln ab/articles/{article_id}
– ruft einen einzelnen Artikel anhand der ID-Eigenschaft ab/articles
– fügt einen neuen Artikel hinzuIn meinem Terminal habe ich ein neues Python-Projekt namens fast-api-demo erstellt und dann die folgenden Befehle ausgeführt:
$ pip install --upgrade pip $ pip install fastapi $ pip install uvicorn
Ich habe eine neue Python-Datei namens api.py
erstellt und einige Importe hinzugefügt sowie eine app
Variable eingerichtet:
from fastapi import FastAPI, HTTPException from pydantic import BaseModel app = FastAPI() if __name__ == "__main__": import uvicorn uvicorn.run(app, host="localhost", port=8000)
Als Nächstes habe ich ein Article
definiert, das dem Anwendungsfall der Artikel-API entspricht:
class Article(BaseModel): id: int title: str url: str year: int
Nachdem das Modell erstellt war, musste ich die URIs hinzufügen … was sich als ziemlich einfach herausstellte:
# Route to add a new article @app.post("/articles") def create_article(article: Article): articles.append(article) return article # Route to get all articles @app.get("/articles") def get_articles(): return articles # Route to get a specific article by ID @app.get("/articles/{article_id}") def get_article(article_id: int): for article in articles: if article.id == article_id: return article raise HTTPException(status_code=404, detail="Article not found")
Um mir die Einbindung eines externen Datenspeichers zu ersparen, habe ich beschlossen, einige meiner kürzlich veröffentlichten Artikel programmgesteuert hinzuzufügen:
articles = [ Article(id=1, title="Distributed Cloud Architecture for Resilient Systems: Rethink Your Approach To Resilient Cloud Services", url="https://dzone.com/articles/distributed-cloud-architecture-for-resilient-syste", year=2023), Article(id=2, title="Using Unblocked to Fix a Service That Nobody Owns", url="https://dzone.com/articles/using-unblocked-to-fix-a-service-that-nobody-owns", year=2023), Article(id=3, title="Exploring the Horizon of Microservices With KubeMQ's New Control Center", url="https://dzone.com/articles/exploring-the-horizon-of-microservices-with-kubemq", year=2024), Article(id=4, title="Build a Digital Collectibles Portal Using Flow and Cadence (Part 1)", url="https://dzone.com/articles/build-a-digital-collectibles-portal-using-flow-and-1", year=2024), Article(id=5, title="Build a Flow Collectibles Portal Using Cadence (Part 2)", url="https://dzone.com/articles/build-a-flow-collectibles-portal-using-cadence-par-1", year=2024), Article(id=6, title="Eliminate Human-Based Actions With Automated Deployments: Improving Commit-to-Deploy Ratios Along the Way", url="https://dzone.com/articles/eliminate-human-based-actions-with-automated-deplo", year=2024), Article(id=7, title="Vector Tutorial: Conducting Similarity Search in Enterprise Data", url="https://dzone.com/articles/using-pgvector-to-locate-similarities-in-enterpris", year=2024), Article(id=8, title="DevSecOps: It's Time To Pay for Your Demand, Not Ingestion", url="https://dzone.com/articles/devsecops-its-time-to-pay-for-your-demand", year=2024), ]
Ob Sie es glauben oder nicht, damit ist die Entwicklung für den Article API-Mikroservice abgeschlossen.
Für eine schnelle Plausibilitätsprüfung habe ich meinen API-Dienst lokal gestartet:
$ python api.py INFO: Started server process [320774] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://localhost:8000 (Press CTRL+C to quit)
Dann habe ich in einem anderen Terminalfenster eine Curl-Anfrage gesendet (und sie an json_pp
weitergeleitet):
$ curl localhost:8000/articles/1 | json_pp { "id": 1, "title": "Distributed Cloud Architecture for Resilient Systems: Rethink Your Approach To Resilient Cloud Services", "url": "https://dzone.com/articles/distributed-cloud-architecture-for-resilient-syste", "year": 2023 }
Anstatt die Article API einfach lokal auszuführen, wollte ich sehen, wie einfach ich den Microservice bereitstellen kann. Da ich noch nie zuvor einen Python-Microservice in Heroku bereitgestellt hatte, dachte ich, jetzt wäre ein guter Zeitpunkt, es zu versuchen.
Bevor ich mich in Heroku vertiefen konnte, musste ich eine requirements.txt
Datei erstellen, um die Abhängigkeiten für den Dienst zu beschreiben. Dazu habe ich pipreqs
installiert und ausgeführt:
$ pip install pipreqs $ pipreqs
Dadurch wurde für mich eine Datei requirements.txt
mit den folgenden Informationen erstellt:
fastapi==0.110.1 pydantic==2.6.4 uvicorn==0.29.0
Ich brauchte außerdem eine Datei namens Procfile
, die Heroku mitteilt, wie mein Microservice mit uvicorn
gestartet wird. Ihr Inhalt sah folgendermaßen aus:
web: uvicorn api:app --host=0.0.0.0 --port=${PORT}
Für diejenigen unter Ihnen, die (wie ich) neu bei Python sind, habe ich die Dokumentation „Erste Schritte mit Heroku mit Python“ als hilfreichen Leitfaden verwendet.
Da ich die Heroku CLI bereits installiert hatte, musste ich mich nur von meinem Terminal aus beim Heroku-Ökosystem anmelden:
$ heroku login
Ich habe darauf geachtet, alle meine Updates in mein Repository auf GitLab einzuchecken.
Als Nächstes kann die Erstellung einer neuen App in Heroku mithilfe der CLI mit dem folgenden Befehl erfolgen:
$ heroku create
Die CLI antwortete mit einem eindeutigen App-Namen sowie der URL für die App und dem mit der App verknüpften Git-basierten Repository:
Creating app... done, powerful-bayou-23686 https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/ | https://git.heroku.com/powerful-bayou-23686.git
Bitte beachten Sie: Wenn Sie diesen Artikel lesen, ist meine App nicht mehr online.
Schauen Sie sich das an. Wenn ich einen Git-Remote-Befehl ausgebe, kann ich sehen, dass dem Heroku-Ökosystem automatisch ein Remote-Befehl hinzugefügt wurde:
$ git remote heroku origin
Um die fast-api-demo
App auf Heroku bereitzustellen, muss ich nur den folgenden Befehl verwenden:
$ git push heroku main
Nachdem alles eingerichtet war, konnte ich im Heroku-Dashboard bestätigen, dass mein neuer Python-basierter Dienst einsatzbereit ist:
Wenn der Dienst ausgeführt wird, ist es möglich, den Article
mit id = 1
aus der Artikel-API abzurufen, indem Sie den folgenden Curl-Befehl ausführen:
$ curl --location 'https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/articles/1'
Der Curl-Befehl gibt eine 200 OK- Antwort und die folgende JSON-Nutzlast zurück:
{ "id": 1, "title": "Distributed Cloud Architecture for Resilient Systems: Rethink Your Approach To Resilient Cloud Services", "url": "https://dzone.com/articles/distributed-cloud-architecture-for-resilient-syste", "year": 2023 }
Durch die Nutzung der integrierten OpenAPI-Funktionalität von FastAPI können Verbraucher eine voll funktionsfähige v3-Spezifikation erhalten, indem sie zur automatisch generierten /docs
URI navigieren:
https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/docs
Durch Aufrufen dieser URL wird der Artikel-API-Mikrodienst unter Verwendung der weit verbreiteten Swagger-Benutzeroberfläche zurückgegeben:
Für diejenigen, die nach einer openapi.json
Datei suchen, um Clients für die Nutzung der Artikel-API zu generieren, kann die URI /openapi.json
verwendet werden:
https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/openapi.json
Für mein Beispiel sieht die JSON-basierte OpenAPI v3-Spezifikation wie folgt aus:
{ "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" }, "paths": { "/articles": { "get": { "summary": "Get Articles", "operationId": "get_articles_articles_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { } } } } } }, "post": { "summary": "Create Article", "operationId": "create_article_articles_post", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Article" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/articles/{article_id}": { "get": { "summary": "Get Article", "operationId": "get_article_articles__article_id__get", "parameters": [ { "name": "article_id", "in": "path", "required": true, "schema": { "type": "integer", "title": "Article Id" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } } }, "components": { "schemas": { "Article": { "properties": { "id": { "type": "integer", "title": "Id" }, "title": { "type": "string", "title": "Title" }, "url": { "type": "string", "title": "Url" }, "year": { "type": "integer", "title": "Year" } }, "type": "object", "required": [ "id", "title", "url", "year" ], "title": "Article" }, "HTTPValidationError": { "properties": { "detail": { "items": { "$ref": "#/components/schemas/ValidationError" }, "type": "array", "title": "Detail" } }, "type": "object", "title": "HTTPValidationError" }, "ValidationError": { "properties": { "loc": { "items": { "anyOf": [ { "type": "string" }, { "type": "integer" } ] }, "type": "array", "title": "Location" }, "msg": { "type": "string", "title": "Message" }, "type": { "type": "string", "title": "Error Type" } }, "type": "object", "required": [ "loc", "msg", "type" ], "title": "ValidationError" } } } }
Daher kann die folgende Spezifikation verwendet werden, um Clients in einer Reihe verschiedener Sprachen über OpenAPI Generator zu generieren.
Zu Beginn dieses Artikels war ich bereit, in die Schlacht zu ziehen und mich jedem zu stellen, der kein Interesse an einem API-First-Ansatz hat. Was ich aus dieser Übung gelernt habe, ist, dass ein Produkt wie FastAPI dabei helfen kann, schnell einen funktionierenden RESTful-Microservice zu definieren und zu erstellen und gleichzeitig eine vollständig nutzbare OpenAPI v3-Spezifikation einzubinden … automatisch.
Es stellte sich heraus, dass FastAPI es Teams ermöglicht, sich auf ihre Ziele zu konzentrieren, indem es ein Framework nutzt, das einen standardisierten Vertrag hervorbringt, auf den sich andere verlassen können. Dadurch hat sich ein weiterer Weg ergeben, um meinem persönlichen Leitbild treu zu bleiben.
Unterwegs habe ich Heroku zum ersten Mal verwendet, um einen Python-basierten Dienst bereitzustellen. Abgesehen vom Durchsehen einiger gut geschriebener Dokumentationen erforderte dies meinerseits kaum Aufwand. Daher muss auch für die Heroku-Plattform ein weiterer Mission-Statement-Bonus erwähnt werden.
Wenn Sie am Quellcode dieses Artikels interessiert sind, finden Sie ihn auf GitLab .
Habt einen richtig schönen Tag!