Strategie

API-Development

API-Development

In den vorherigen Teilen dieser Serie wurde in die Themen API-Economy und API-Design eingeführt. Basierend auf den Vorgaben des API-Designs beschäftigt sich API-Development mit der Umsetzung bzw. Implementierung der vereinbarten Schnittstellen. Als stark technisch geprägtes Thema werden hier Entscheidungen zu den eingesetzten Techniken und den zu verwendenden Frameworks gefällt.

Die Entwicklung und die Übergabe in den Betrieb folgen dabei üblicherweise der Idee des “DevOps” (also der gemeinsamen Verantwortung für Entwicklung und Betrieb) und sind geprägt von häufigen Tests und Roll-Outs (Continuous Integration / Continuous Delivery). Aufgrund der heute üblichen Microservice-Architekturen besteht große Freiheit bei der Auswahl geeigneter Programmiersprachen (z.B. Java, ECMAScript oder Go ) und Laufzeit-Umgebungen (z.B. NodeJS und Spring Boot ).

Disziplin API-Development im Kontext

Disziplin API-Development im Kontext

Virtualisierung von Hardware und die Verwendung von Containern erlauben darüber hinaus den leichtgewichtigen Betrieb im eigenen Rechenzentrum oder bei einem Cloud- bzw. IaaS-Anbieter. Häufig kommen dabei Orchestrierungswerkzeuge (Docker Swarm oder Kubernetes) zum Einsatz.

Die tatsächliche Implementierung von APIs durch entsprechende Services unterscheidet sich nicht wesentlich von anderen Umsetzungen im Bereich IT-Architektur und Software-Entwicklung. Jedoch gibt es einige aktuelle Paradigmen und Tendenzen im Software-Development, welche den Aspekt API-Development beeinflussen.

Agiles “Full Stack”-Deployment

Diese Art der “Full-Stack”-Entwicklung kann nur von heterogenen oder “cross-funktionalen” Teams durchgeführt werden, also Teams, in denen UI- und Datenbank-Designer mit Solution-Architekten, Entwicklern und Testern gemeinsam an Arbeitspaketen arbeiten. Die Arbeitspakete sollten außerdem in kurzen Zeiträumen umgesetzt werden und idealerweise aufeinander aufbauen, also inkrementellen Zuwachs an Funktionalität bewirken.

Beide Aspekte sind Indikatoren für die Anwendung agiler Methoden wie Scrum in der Software-Entwicklung:

  • Die Arbeiten werden als User-Stories formuliert und über Topics gruppiert. Zu den User-Stories werden entsprechende Tasks formuliert, welche dann im Rahmen einer Iteration (Sprint) abgearbeitet werden.
  • Die Entwicklung findet in Gruppen von 5-9 Personen statt, die bzgl. ihrer Fachlichkeit und Erfahrung durchmischt sind. Als Team sind alle verantwortlich für die Erreichung des Sprint-Ziels und arbeiten gemeinsam an der Erledigung der Aufgaben.

Continuous Integration / Continuous Delivery

Die Entwicklung und Bereitstellung von Microservices nach agilen Prinzipien hat allerdings nur Sinn, wenn die erzeugten Inkremente (oder Artefakte) auch in kurzer Zeit und ohne größere organisatorische Barrieren in den Betrieb überführt werden können. Dabei ist – neben einem hohen Anteil an automatisierten Tests – auch eine entsprechende IT-Umgebung notwendig, welche Continuous Delivery, also fortlaufende Installationen ermöglicht.

Idealerweise deckt die verfügbare Build-and-Deploy-Strecke dabei alle Phasen der Software-Erstellung und -Bereitstellung ab, wobei die einzelnen Stufen (Stages) ineinandergreifen:

  • Die Verwaltung der Quelltexte erfolgt revisionssicher in einem Source-Code-Management-System (SCM), das dezentral arbeitet, parallele Versionsverläufe (Branches) und gute Unterstützung bei der Konfliktauflösung (Merge Conflict) bietet. Der aktuelle Platzhirsch ist hierbei ohne jeden Zweifel Git. Ein definiertes Repository (auf einem eigenen Server oder bei einem Anbieter wie Github ) dient dabei als Ausgangspunkt für den späteren Build-Prozess.
  • Änderungen im zentralen Repository gelten als Indikator für das Continuous-Integration-System. Entsprechend konfiguriert wird der CI-Server (oder Service) die aktuellen Quelltexte aus dem Repository kopieren und den Übersetzungsprozess starten. Kommt es dabei zu keinen Fehlern (in Syntax oder Abhängigkeiten zu Bibliotheken), werden automatisierte Unit-Tests und Integrationstests gestartet. Die Entwickler haben bei der Testerstellung darauf zu achten, dass Abhängigkeiten zu externen Systemen über entsprechende Emulatoren (Mocks) simuliert werden.
  • Bleiben auch diese Tests ohne Fehler bzw. überschreitet die Fehlerrate keine gesetzte Toleranzgrenze, kann eine Installation auf ein Testsystem erfolgen. In der Regel und besonders im Umfeld von Web-Anwendungen werden dann automatisierte UI-Tests gestartet, d.h. die generierten Web-Oberflächen werden in einem automatisierten Browers aufgerufen und über Test-Werkzeuge wie Selenium auf Fehler oder Abweichungen geprüft. Denkbar ist an dieser Stelle auch eine manuelle Freigabe, wobei diese niemals von einer Einzelperson, sondern von einem Team abhängen sollte.
  • Schließlich erfolgen die Installation in der Produktionsumgebung und ein rollierender Neustart der Anwendung. Das heißt, das neue Instanzen der aktualisierten Anwendung gestartet werden und sukzessive eingehende Anfragen auf diese umgeleitet werden. Sobald einzelne oder alle vorherigen Versionen nicht mehr mit Anfragen versorgt sind, werden diese abgeschaltet und entfernt. Dieses Vorgehen der sog. Rolling Updates wird durch den Einsatz von Containern und entsprechenden Orchestrierungswerkzeugen erheblich vereinfacht.

Microservices

Wie im Post API-Design bereits erwähnt, sollten APIs sich auf Ressourcen konzentrieren und daher einen überschaubaren, fokussierten Funktionsumfang bieten. Dieser Anspruch wird von Microservices erfüllt, welche sich als autonome, oftmals geschlossene Einheiten mit Fokus auf Einfachheit, Parallelität und Skalierung darstellen.

Microservices werden in der Regel von kleinen Teams in Eigenregie entwickelt und sind nur wenigen Vorgaben bzgl. des Entwicklungsmodells und der eingesetzten Techniken unterworfen. Es gibt tatsächlich auch wenig logische Gründe für Einschränkungen, solange die entwickelten Services die API – die vereinbarte Schnittstelle – einhalten. Lediglich Restriktion bzgl. der Laufzeitumgebung können auferlegt sein, doch dafür gibt es inzwischen Lösungen (siehe nächster Absatz).

Wo Einschränkungen bzgl. Software-Architektur, Programmiersprache und Laufzeitumgebung reduziert werden, ergibt sich zwangsläufig Raum für anwendungs-orientierte, effiziente Sprachen und Frameworks. Erwähnenswert sind hierbei sicherlich NodeJS und Go. Während es sich bei NodeJs eine mächtige Umgebung für server-seitiges JavaScript handelt, is Go eine relative neue, von Google entwickelte Programmiersprache, die auf Einfachheit, Skalierung und Parallelität setzt. Aber auch für etablierte “Enterprise”-Sprachen wie Java existieren interessante Initiativen und Frameworks. Während das interne Projekt “Jigsaw” Modularisierung in den Sprachkern integriert, setzen Spring Boot und jHipster auf Prototypen und Bootstrapping aktueller Java-Versionen in Hinblick auf Microservices.

Containerisierung

Software-Entwicklung wird üblicherweise auf die Plattform ausgerichtet, auf welcher sie später betrieben wird. Schon länger beschreibt der Begriff “Plattform” dabei keine physische Komponente mehr, sondern eine virtuelle Maschine, welche in mehreren Instanzen auf reeller Hardware betrieben wird. Das Ziel sind eine bessere Auslastung vorhandener Hardware und eine einheitliche, durch Abstraktion erreichte Zielplattform.

Doch für Microservices stellt selbst eine virtuelle Machine (VM) einen gewaltigen Overhead dar, da hier eine vollständige Hardware-Umgebung inkl. ihrer (virtuellen) Bausteine und Schnittstellen simuliert wird. Zuviel für eine atomare Komponente wie einen Microservice, der in der Regel aus einem Prozess besteht und über ein Netzwerk angesteuert wird.

Dieses Problem adressiert Containerisierung. Während VMs Hardware virtualisieren, dienen Container der Virtualisierung von Prozessen. Sie isolieren also einen Prozess von anderen Prozessen der gleichen (physischen) Umgebung und gewähren dennoch kontrollierten Zugriff auf die physischen Schnittstellen der umgebenden Plattform. Um die daraus resultierenden Risiken unter Kontrolle zu halten, werden oftmals virtuelle Maschinen und Container-Umgebungen kombiniert.

Container stellen dabei Instanzen einer Vorlage, des sogenannten Images, dar. Ein Image seinerseits kann auf anderen Images basieren, welche in einem Mehrschichtenmodell sich ergänzen. So lässt sich die gewünschte Laufzeitumgebung für den späteren Prozess im Image granular und effizient definieren und über die Container-Instanz manifestieren. Werden nach der Erzeugung des Containers Änderungen am Image oder den Basis-Images vorgenommen, haben diese keine Auswirkungen mehr auf den Container.

Dieser Umstand und die daraus resultierende Anforderung der “Vergänglichkeit” für Container führt zu Herausforderungen bei der Speicherung von Daten (Persistenz), welche aber über Netzwerk-Speicher (NAS/SAN) und Cloud-Drives gelöst werden können. Die Prozesse, die in Containern laufen, konzentrieren sich daher auf die Verarbeitung und nutzen die Daten gemeinsam. Eine wichtige Voraussetzung für Skalierung und Redundanz.

Auch wenn wesentliche Techniken mittlerweile an die Open Container Initiative (OCI) übergeben wurden – Treiber des Themas Containerisierung ist unverändert die Firma Docker Inc. mit ihrem gleichnamigen Technologie-Stack. Rund um Docker sind weitere Frameworks entstanden, welche die Verwaltung mehrerer Container und deren Skalierung unterstützen, z.B. Google Kubernetes (k8s), RedHat OpenShift und Docker Swarm.

Container-Infrastrukturen können sowohl im eigenen Rechenzentrum (on-premise) als auch in Cloud-Zentren bei Amazon, Google oder Microsoft betrieben werden. Die großen Cloud-Anbieter haben inzwischen auch vorgefertigte Installationen im Angebot, z.B. Amazons Elastic Container Services für Kubernetes (Amazon EKS).

Lambdas / “serverless”

Die zunehmende Verbreitung und Nutzung von Containern sowie eine fortschreitende, fachliche Fokussierung von zustandslosen Microservices haben letztendlich zu Überlegungen geführt, ob der Betrieb einer (virtuellen) Hardware oder Container-Infrastruktur überhaupt noch im Bereich der Service-Implementierung stehen sollte oder ob man diese Leistung einkauft bzw. an höher qualifizierte Anbieter abgibt. Diesen Überlegungen folgend hat sich ein Markt für Function-as-a-Service- (FaaS) bzw. Lambda-Angebote entwickelt. Ein Lambda ist dabei ein Service, welcher genau eine Methode bereitstellt. Er ist gemäß des REST-Paradigmas zustandslos und kann daher mit minimalem Aufwand an beliebiger Stelle in beliebiger Breite (skaliert) installiert werden.

Function-as-a-Service bzw. Lambdas treiben also die Idee eines Microservice auf die Spitze, indem der Service auf genau eine Operation reduziert wird. Jeder dieser Operationen kann getrennt entwickelt, bereitgestellt und optimiert werden. Gleichzeitig können mit Mitteln des API-Managements nach Art einer Fassade mehrere Lambdas zu (thematischen) Gruppen zusammengeführt und orchestriert werden. Da der Lebenszyklus einer FaaS äußerst begrenzt ist und Ressourcen-Management von der Laufzeitumgebung übernommen wird, können sich Entwickler vollständig auf die fachliche Funktionalität konzentrieren. Durch die Nutzung eines umgebungs-agnostischen Frameworks wie OpenFaaS können dabei sogar die Deployment-Besonderheiten des konkreten Anbieters ignoriert werden.

Sicherheit

Richtlinien und Hinweise zur Realisierung sicherer Services sind Ergebnisse konsequenten API-Designs. In der Disziplin API-Development gilt es diese umzusetzen bzw. auf die aktuellen Rahmenbedingungen und technischen Möglichkeiten abzubilden. Die Best Practices zur Fehlerermittlung und Schwachstellen-Analyse über technische Tools und Methoden wie Pair Programming finden im API-Development unverändert Anwendung. Ein konsequentes Monitoring der Software-Qualität über Tools wie SonarQube darf als selbstverständlich betrachtet werden.

Von besonderer Bedeutung ist dabei, dass eine Service-Implementierung in sich sicher ist. Sie darf sich nicht auf eventuell vorgeschaltete Infrastruktur-Komponenten wie eine Firewall oder ein API-Gateway verlassen. Der Grund hierfür ist der Anspruch der jederzeitigen Verschiebbarkeit eines Service. Durch Werkzeuge wie Kubernetes und Docker Swarm kann eine Service-Instanz jederzeit und sogar über Cluster-Grenzen hinweg verschoben werden. Dabei kann sie auch (unabsichtlich) außerhalb des aufgespannten “Schutzraums” betrieben werden. Mittels pro-aktiver Penetrationstests (“PenTest”) kann die Belastbarkeit der Sicherheitsaussage belegt werden.

Authentifizierung und Autorisierung

API und Services fungieren als Schnittstellen, d.h. sie wurden geschaffen für die Kommunikation mit Consumern. Für die Sicherheit ist es unabdingbar, dass unberechtigte Aufrufe unterbunden werden oder anders formuliert, dass nur bekannte und berechtigte Kommunikationspartner zugelassen werden.

Die grundsätzliche Identifizierung der Partner kann über Zertifikate geschehen, welche von einer gemeinsamen (öffentlichen) Certificate Authority (CA) beglaubigt wurden. So kann allgemein sichergestellt werden, dass ein bekannter Partner die Gegenstelle der Kommunikation bildet. Alternativ dazu kann auch ein beiderseits bekanntes Geheimnis (Shared Secret) verwendet werden, sofern dies über ein sicheres Transportmedium ausgetauscht wird.

Weiterführend ist oftmals eine Authentifizierung des Nutzers notwendig, d.h. der Person (oder Fraktion), welche über eine Consumer-Anwendung Zugriff auf eine API nimmt. Auch hierfür kann ein Shared Secret zum Einsatz kommen, wozu im weiteren Sinne auch eine Benutzername- und Passwort-Kombination gehört. Stand der Technik (und Voraussetzung für komfortable Single Sign On-Lösungen) sind allerdings Token-basierte Verfahren wie OAuth oder OpenID Connect .

OAuth

OAuth ist ein Authentifizierungsprotokoll, welches ab 2006 von einem Twitter-Mitarbeiter entwickelt und später an ein Standardisierungsgremium übergeben wurde. Das Protokoll regelt das Zusammenspiel von vier Aktoren, die miteinander in OAuth-Flows interagieren.

Der OAuth-Server stellt den Authentisierungsmechanismus durch die Abfrage der Credentials und fordert das Einverständnis des Benutzers für die Delegation (Consent Server). Der Server persistiert und verwaltet die Tokens.

Der Resource Server bietet geschützte Daten und Services für authentisierte und autorisierte Nutzer über APIs an. Im Kontext API-Management ist darauf zu achten, dass der Zugriff auf die OAuth-APIs frei von Zugangsbeschränkungen ist.

Der Resource Owner ist der Besitzer (User) der angestrebten Daten (Protected Resources). OAuth ermöglicht, falls gewünscht, die Delegation der Zugriffsrechte vom Resource Owner an den Client. Der Client (Third-Party) ist zu meist eine Anwendung wie z. B. eine Mobile App oder Webanwendung. Der Client strebt in diesem Zusammenhang die geschützten Daten des Resource Owner an.

OAuth 2.0 differenziert strikt zwischen dem Erlangen eines Tokens durch verschiedene Grant Types und die Verwendung dieser. Somit ist es möglich, dieselbe API mit verschiedenen Endgeräten und Geschäftsmodellen zu verwenden.

Der Authorization Code Flow basiert auf dem Grant Type Authorization Code und wird oft auch als “3-legged OAuth” bezeichnet, da die Identität von drei Aktoren geprüft werden.

  1. Der OAuth Server authentifiziert den Resource Owner durch die Eingabe der Credentials
  2. Der OAuth Server authentifiziert den Client durch ClientID/ClientSecret über Basic Auth
  3. Der Client wiederum authentifiziert den OAuth Server durch dessen Zertifikat
OAuth Authorization Code Flow

OAuth Authorization Code Flow

  1. Der Client erstrebt den Zugriff auf die Daten des Benutzers und sendet hierzu eine Autorisierungsanfrage ‒ über eine Browserweiterleitung des OAuth Server – an den Resource Owner. Der OAuth Server dient dabei als Vermittler und fragt in einem Browserdialog die Credentials des Resource Owner ab. Ist der Dialog erfolgreich, sendet OAuth Server einen Authorization Code an den Client.
  2. Der Client sendet den Authorization Code an den OAuth Server, der im Gegenzug ein kurzlebiges Access Token und ein Refresh Token mit einer langen Lebenszeit ausstellt
  3. Mit dem Access Token hat der Client die Autorisierung für Zugriffe auf den Resource Server
  4. Ist die Gültigkeit des Access Token abgelaufen, kann mit dem Refresh Token ohne Benutzerinteraktion eine neues Access Token vom OAuth Server erstellt werden

Der stark browserbasierte Authorization Code Flow besitzt dabei im OAuth-Protokoll eine fundamentale Rolle, da alle anderen Flows nur eine Abänderung oder Minimierung des Flows darstellen. Jeder Flow ist auf bestimmt Anwendungsfälle optimiert.

Weitere Flows

Je nach Nutzungsszenarien können die OAuth-Aktoren in einem Flow verschieden miteinander interagieren. Dabei nutzen verschiedene Flows meist gleichnamige Grant Types:

  • der Implicit Flow wird genutzt, wenn der Client das Refresh Token und auch das Client Secret nicht sicher speichern kann. Dies tritt zum Bespiel ein, wenn die Clients auf JavaScript basieren. Dieser Flow minimiert dabei den Authorization Flow um die Auslieferung des Refresh Tokens.
  • der Resource Owner Password Credentials Flow wird verwendet, wenn der Resource Owner dem Client für kurze Zeit seine Credentials anvertraut. Username und Passwort dürfen jedoch nicht vom Client abgespeichert werden und müssen nach der Interaktion wieder gelöscht werden. Ein solches Vertrauen ist z. B. nur dann möglich, wenn sowohl Client als auch Resource Owner aus dem gleichen Unternehmen stammen.
  • im Client Credentials Flow ist der Client gleichzeitig auch Resource Owner, somit kann der Client mit seiner ClientID und ClientSecret mit einer Anfrage ein Access Token erhalten.

OAuth lässt sich je nach Bedarf mit so genannten Profilen erweitern, diese sind vielfach schon standardisiert.

OpenID Connect Profil

OAuth lässt sich je nach Bedarf mit so genannten Profilen erweitern, diese sind vielfach schon standardisiert. OpenID Connect kann als ein Aufsatz für OAuth2.0 betrachtet werden und ermöglicht den Clients, die Identität des Nutzers anhand der Authentifizierung durch einen Autorisierungsserver zu überprüfen. Informationen über den Resource Owner können in einer zur Weiterverarbeitung geeigneten Form überführt und ausgelesen werden. OpenID Connect eignet sich dabei für alle Arten von Clients (Cloud, Web oder Mobil) und wird allen notwendigen Anforderungen für ein Single Sign-On ‒ wie z. B. Verschlüsselung und Session Management ‒ gerecht.