Über unsMediaKontaktImpressum
Christian Schwörer & Constantin Weißer 12. Februar 2019

API Gateways – eine praktische Einführung

API Gateways – auch als Edge-Services bekannt – sind ein fundamentaler Bestandteil einer cloud-nativen Microservice-Architektur. Sie bilden den zentralen Zugangspunkt für alle Requests zu den Backend-Services. In diesem Artikel geben wir einen praktischen Einstieg in die Thematik. Zunächst werden dazu die verschiedenen Aufgaben eines API Gateways vorgestellt. Anschließend werden konkrete Umsetzungsmöglichkeiten skizziert – angefangen bei der rudimentären Proxy-Grundfunktionalität über Cloud-Plattform-Lösungen bis hin zu programmatischen Gateway-Frameworks.

Sinn und Zweck von API Gateways

Abb. 1 zeigt eine einfache Microservice-Architektur, bei der zwei Clients – eine Mobile App und eine Angular-Single Page Application (SPA) – mit drei Backend-Services (Users, Images und Comments) kommunizieren. Dieser Aufbau bringt einige Nachteile mit sich:

  • Viele Kommunikationsverbindungen: Die ersichtlich große Anzahl an Verbindungen bedeutet auch, dass jeder Client jeden Backend-Service kennen muss. Das ist spätestens dann problematisch, wenn sich die Backends verändern, weil beispielsweise ein Service in zwei kleinere Services aufgebrochen werden soll.
  • Same-Origin-Policy: Damit die Clients mit den verschiedenen Backend Services kommunizieren dürfen, muss bei jedem Backend eine Cross-Origin-Resource-Sharing-(CORS)-Ausnahme definiert sein. Dies trifft insbesondere beim Einsatz von JavaScript-Clients wie der aufgeführten Angular-SPA zu.
  • Schutz von internen Endpunkten: Wie in Abb. 1 zu sehen, gibt es eine Verbindung zwischen dem Users- und dem Images-Service. Da es sich um öffentliche Endpunkte handelt, sind diese aber auch von den Clients erreichbar, auch wenn das nicht gewollt wäre.
  • Cross-cutting concerns: Zusätzlich gibt es eine ganze Reihe querschnittlicher Belange, die in jedem Backend-Service implementiert werden müssen. Dazu zählt beispielsweise eine erste Authentifizierung, eine mögliche SSL-Terminierung oder das Prüfen oder Setzen von bestimmen (Security-)Headern wie etwa CSRF oder HSTS. Aber auch Schutzmechanismen wie Rate Limiting und Throttling sind hier zu nennen.

Durch die Einführung eines API Gateways, das sozusagen auf der Grenze zum Backend-System liegt (daher auch der Name "Edge-Service") und als zentraler Zugangspunkt dient, können diese Nachteile kompensiert werden.

Wie der prinzipielle Aufbau in Abb. 2 zeigt, verläuft die Kommunikation der Clients zu den Backend-Services immer über das API Gateway. Die Clients müssen folglich auch nur noch dessen Adresse kennen – das Gateway leitet die Anfragen dann an den spezifischen Service weiter. Somit ist seine Hauptaufgabe das Reverse Proxying, d. h. die Kapselung der Backend-Services für die Clients. In dieser zentralen Stelle lassen sich die oben erwähnten querschnittlichen Belange umsetzen, sodass diese nicht von jedem Service separat bereitgestellt werden müssen. Ebenfalls können hier einheitlich die CORS-Regeln definiert werden, um der Same-Origin-Policy gerecht zu werden.

Grundfunktionalität: Reverse Proxying

Während API-Gateway-Produkte oft eine Vielzahl von Features liefern, ist eine einfache Funktionalität von zentraler Bedeutung: das Reverse Proxying. Warum? Es können dadurch mehrere verschiedene Endpunkte unter einem zusammengefasst werden. Ohne Reverse Proxying müssen die drei beispielhaften Services Users, Comments und Images unter drei verschiedenen Adressen erreicht werden. Schlimmer noch: skaliert man die Services, vervielfachen sich die Endpunkte weiter. Das führt schnell zu einer unübersichtlichen Sammlung von Endpunkten seitens des Clients. Gibt es Clients, die man nicht kontrollieren kann (z. B. eine ausgerollte Mobile App ohne Update-Zwang), ist man in den möglichen Änderungen sehr beschränkt, die man an den Endpunkten vornehmen kann.

Zur Zusammenfassung gleichwertiger Endpunkte, von zum Beispiel drei Instanzen des User-Services, verwendet man in der Regel Loadbalancer. Für verschiedenartige Endpunkte, zum Beispiel mehrere Services, wird ein Reverse Proxy eingesetzt. Diese Begriffe beschreiben sowohl die Konzepte als auch konkrete Implementierungen hiervon. Oft kann man aber auch mehrere Konzepte in einer Implementierung zusammenfassen.

Wir betrachten beispielhaft NGINX [1]. Der Webserver lässt sich mit einer einfachen Konfiguration als zuverlässiger Reverse Proxy betreiben. In vielen Plattformen kommt er deshalb auf diese Weise zum Einsatz. Googles API-Management mit dem Namen "Cloud Enpoints" basiert beispielsweise darauf. Der relevante Teil einer NGINX-Konfiguration sieht wie folgt aus:

Listing 1: NGINX-Konfiguration

server {
    listen   443;
    location /users {
        proxy_pass localhost;
    }
    location /comments {
        proxy_pass localhost;
    }
    location /images {
        proxy_pass localhost;
    }
}

Jeder location-Abschnitt beschreibt einen Pfad, den der Webserver anbietet. Anstatt auf dem Pfad lokale Dateien anzubieten, kann man mit Hilfe der proxy_pass-Direktive andere Server angeben, an die Requests weitergeleitet werden. Bei mehreren Instanzen kann man entweder einen Loadbalancer einsetzen und auf die Adresse des Loadbalancers verweisen, oder mit kleinem Aufwand die Konfiguration um Servergruppen erweitern und so mit Hilfe von NGINX die Last verteilen.

Neben der Konfiguration stellt sich grundsätzlich die Frage, wie man Software zuverlässig betreiben kann. Software von Hand auf Maschinen installieren, virtuell oder physisch, gehört eigentlich der Vergangenheit an. Als initiales Setup zum Zeitpunkt der Entwicklung kann es natürlich so gemacht werden. Für den produktiven Einsatz ist es wichtig, das Setup reproduzierbar zu machen. Hier können natürlich die bekannten Configuration Management Werkzeuge verwendet werden.

Heute werden in der Cloud meist Application- oder Container-Runtimes verwendet um Anwendungen auszuführen. Ein prominentes Beispiel ist Cloud Foundry. Tatsächlich ist es auf der Plattform möglich, einen Reverse Proxy mit sehr wenig Aufwand zu konfigurieren. Im folgenden Abschnitt wird diese Lösung genauer vorgestellt.

Plattformlösung: Cloud Foundry

Cloud Foundry ist eine beliebte Plattform, um Applikationen in der Cloud zu betreiben [2]. Zu den unterstützten Runtimes gehören beispielsweise Java, Go oder Python. Der Nutzer erhält sehr komfortable Werkzeuge, um Anwendungen schnell in Betrieb zu nehmen und zu skalieren. Die Umgebung für eine einzelne ausgeführte Anwendung wird von Cloud Foundry bereitgestellt. Im Falle einer Java-Anwendung ist das ausführbare Artefakt eine Jar-Datei. Das Command Line Werkzeug CF-CLI bietet dem Entwickler mit dem Befehl cf die Möglichkeit, seine Anwendung direkt zur Ausführung in die Cloud-Foundry-Instanz zu pushen, d. h. einen Upload der ausführbaren Jar-Datei vorzunehmen und die Anwendung zu starten.

Standardmäßig wird jeder Applikation dabei eine sogenannte Serviceroute zugewiesen. Dahinter verbirgt sich eine DNS-Subdomain, unter der genau diese Applikation erreichbar ist. Wenn man eine Anwendung auf mehr als eine Instanz skaliert, wird für den Aufrufer transparent die Last zwischen den Instanzen verteilt. Cloud Foundry platziert also automatisch einen Loadbalancer vor jede Anwendung. Nichtsdestotrotz hat man bei mehreren verschiedenen Anwendungen (zum Beispiel mehreren Microservices) auch mehrere Endpunkte. Bei Veränderungen in der Microservice-Landschaft treffen dann eben jene Nachteile ein, die oben beschrieben wurden: der Client muss ständig angepasst werden, um alle Endpunkte zu kennen. In vielen Fällen ist das gar nicht möglich. Welche Möglichkeiten haben wir also, um auf Cloud Foundry die Funktionalität eines API Gateways zu erhalten? Wir betrachten zwei verschiedene Ansätze:

Einsatz eines API Gateways als Anwendung

Nehmen wir eine beliebige Implementierung eines API Gateways, zum Beispiel das später vorgestellte Spring Cloud Gateway, Netflix Zuul oder eben für einfache Fälle NGINX. Sofern sich die Software auf Cloud Foundry deployen lässt, ist sie für den Einsatz geeignet.

Die Anwendung bekommt – wie jede andere Anwendung auch – eine Serviceroute. Andere Anwendungen können auch innerhalb der Plattform wiederum über deren Route erreicht werden. Anstatt also im Client die einzelnen Routen des User-, Comment- und Image-Services  zu verwenden, rufen wir nur noch die Route der API-Gateway-Anwendung auf. Die Abb. 3 verdeutlicht den Unterschied.

Diese Lösung erfordert das Instandhalten einer weiteren Anwendung, auch wenn diese oft hauptsächlich konfiguriert und nur selten programmiert ist. Trotzdem muss man den zusätzlichen Aufwand für das Team im Auge behalten. Gleichzeitig nutzen wir aber die Vorzüge der Plattform aus: Cloud Foundry unterstützt den Entwickler beim gesamten Lifecycle der Gateway-Applikation. APM- und Monitoring-Lösungen, die ohnehin im Einsatz sind, müssen nur auf diese weitere Applikation ausgeweitet werden. Zudem bietet dieser Ansatz größtmögliche Flexibilität. Welche konkrete Implementierung für das API Gateway verwendet wird, spielt keine Rolle und kann auch im Laufe der Zeit immer wieder den Bedürfnissen entsprechend geändert werden. Es bleibt jedoch anzumerken, dass die Endpunkte der einzelnen Services hierbei weiterhin öffentlich sind und ohne API Gateway zugänglich sind.

Cloud Foundry Plattform-Features

Cloud Foundry bietet aber auch eine Minimallösung ohne Betriebsaufwand: Oben beschriebene Service-Routen lassen sich mit einfachen Befehlen auch so konfigurieren, dass sie Pfadinformationen für das Routing nutzen. Zusätzlich zur automatisch angelegten Serviceroute können so pfadbasierte Routen konfiguriert werden. Listing 2 zeigt die entsprechenden CF-CLI-Befehle:

Listing 2: Route-Mapping mit Cloud Foundry

cf map-route user-service pcf-url.com --path users --hostname mygw
cf map-route image-service pcf-url.com --path images --hostname mygw

Nach Ausführen dieser Befehle gibt es eine neue Subdomain namens mygw.pcf-url.com auf der wiederum zwei Pfade /users und /images verfügbar sind. /users wird auf die Anwendung user-service gelenkt und /images entsprechend auf image-service. Diese Lösung bietet wenig Konfigurationsspielraum, ist dafür sehr einfach und für Entwickler praktisch wartungsfrei.

Programmatische Lösungen

Müssen im API Gateway jedoch spezielle Anforderungen implementiert werden, welche die vorgestellten Reverse Proxy- oder Plattformlösungen nicht abbilden können, steht eine Reihe von Gateway-Frameworks zur Verfügung, die eine programmatische Umsetzung ermöglichen. Die meisten dieser Frameworks haben den in Abb. 4 dargestellten Aufbau.

Sie ermöglichen, Filter zu definieren, die auf die gerouteten Requests angewendet werden:

  • Pre-Filter: Werten den Request aus, bevor er an den Backend-Service weitergeleitet wird. Erfüllt der Request bestimmte Bedingungen – etwa wenn ein bestimmter Header vorhanden ist – wird der Request modifiziert. Hier können Anfragen auch direkt abgelehnt werden, beispielsweise wenn diese ungültig sind.
  • Routing: Hier erfolgt die eigentliche Weiterleitung an den Backend-Service
  • Post-Filter: Werden auf die Response angewandt, bevor die Antwort zum Client zurückgegeben wird. Das heißt, dass auch die Response noch manipuliert werden kann.

Spring Cloud Gateway

Spring Boot [3] und Spring Cloud [4] sind zwei aufeinander aufbauende JVM-Frameworks zur Erstellung und Betrieb von cloud-nativen Microservice-Anwendungslandschaften.

Spring Cloud bietet schon seit Längerem ein auf Netflix Zuul basierendes API Gateway an [5]. Allerdings bringt die zugrundeliegende Zuul-Version einige Nachteile mit sich: So werden weder HTTP/2 noch WebSockets unterstützt. Das gravierendere Problem ist jedoch, dass das Framework Requests nur blockierend behandeln kann. D. h. für jeden ankommenden Request wird so lange ein Thread blockiert, bis die Antwort vom Backend-Service erfolgt und abgearbeitet ist.

Mit Veröffentlichung von Spring Boot 2.0, das auf Spring Framework 5 und dem Project Reactor basiert, bietet das Framework jedoch die Möglichkeit, nicht-blockierende, reaktive Anwendungen zu erstellen. Folgerichtig entschied man sich mit Spring Cloud Gateway [6], ein komplett auf dem reaktiven Spring-Ökosystem basierendes Gateway-Framework zu veröffentlichen. Seit der Version Greenwich.RC1 [7] von Spring Cloud ist daher Spring Cloud Gateway die empfohlene Lösung.

Szenario

Im Folgenden wird beschrieben, wie mit Spring Cloud Gateway ein eigener Edge-Service erstellt werden kann. In Abb. 5 ist das Szenario, das umgesetzt werden soll, zu sehen.

Das API Gateway hat die Aufgabe, einen "Token-Tausch" durchzuführen. Dazu muss aus dem Request ein Cookie mit dem Name customer-id ausgelesen werden. Ist das Cookie vorhanden, wird ein Bearer Token erstellt und im Authorization-Header gesetzt. Anschließend wird der modifizierte Request an einen von drei Backend-Services geroutet. Folglich braucht es einen Pre-Filter, der das Cookie ausliest und den Header setzt sowie eine Routing-Komponente zur Weiterleitung an das richtige Backend.

Anmerkung: Um die Code-Beispiele auf das Wesentliche zu reduzieren, wird lediglich der Wert des Cookies ausgelesen und in den Authorization-Header kopiert. Auf die eigentliche Erzeugung des Bearer Tokens wird nicht eingegangen.

Die Code-Beispiele sind in Kotlin verfasst – was allerdings keine Bedingung ist. Da es sich bei Spring Boot/Cloud um JVM-Frameworks handelt, ist selbstverständlich auch Java möglich. Eine lauffähige Spring-Boot-Anwendung inklusive Backend-Services mit allen Code-Beispielen befindet sich in GitHub [8].

Projekt-Setup

Am einfachsten lässt sich die Projektstruktur für einen auf Spring Cloud Gateway basierenden Edge-Service über den Spring Initializr erzeugen. Unter https://start.spring.io muss dazu lediglich die Dependency "Gateway" ausgewählt werden (s. Abb. 6). Anschließend lasst sich ein Projekt-Archiv exportieren, das in die Entwicklungsumgebung geladen werden kann.

Routing

Spring Cloud Gateway bietet zwei Möglichkeiten, um Routen zu definieren: Einerseits – wie in Spring Boot üblich – mittels Konfiguration in der application.yml. Andererseits über eine Fluent API-DSL. Da sich der erste Weg wenig von anderen Spring-Boot-Konfigurationen unterscheidet, zeigt Listing 3 den Einsatz der API-DSL.

Listing 3: Routing mit der Fluent API-DSL


@SpringBootApplication
class SpringCloudGatewayApplication {
  
    @Bean
    fun customRouteLocator(builder: RouteLocatorBuilder,
             authFilter: AuthorizationFilter): RouteLocator {
        return builder.routes {
            route(id = "users") {
                path("/users")
                uri("http://localhost:8081/users")
                filters { filter(AuthorizationFilter()) }
            }
            route(id = "comments") {
                path("/comments")
                uri("http://localhost:8082/comments")
                filters { filter(AuthorizationFilter()) }
           }
           route(id = "images") {
                path("/images")
                uri("http://localhost:8083/images")
                filters { filter(AuthorizationFilter()) }
           }
        }
    }
}

In Zeile 1 wird die Anwendung als SpringBootApplication markiert. Die eigentliche Routen-Konfiguration erfolgt ab Zeile 4. Dort wird eine Bean customRouteLocator definiert, die das Interface RouteLocator implementiert. Über die Methode RouteLocatorBuilder.routes() können nun Routen hinzugefügt werden.

So wird in Zeile 8 bis 12 die Route users konfiguriert. Anhand des path Prädikats wird festgelegt, dass alle Requests, die im Pfad das Pattern /users haben, an den Backend Service http://localhost:8081/users weitergeleitet werden. Mittels filters werden die Filter definiert, die auf diese Requests angewendet werden sollen – in dem Beispiel der AuthorizationFilter.

In dem einfachen Beispiel erfolgt die Weiterleitung an Backend Services, die auf localhost laufen. In einer Cloud-Umgebung macht das natürlich keinen Sinn. Im Regelfall kommt bei einer cloud-nativen Architektur eine Service Registry wie zum Beispiel Netflix Eureka oder Consul zum Einsatz, bei der sich Services an- und abmelden. In dem Fall würde das Routing über einen DiscoveryClient erfolgen, der die aktuelle Adresse einer Backend Service Instanz aus dieser Service Registry ermittelt.

Pre-Filter zur Modifikation des Requests

Der Aufbau des AuthorizationFilter ist in Listing 4 aufgeführt.

Listing 4: Filter zur Manipulation der Requests


@Component
class AuthorizationFilter : GatewayFilter {

    override fun filter(exchange: ServerWebExchange,
                        chain: GatewayFilterChain): Mono<Void> {

        val cookie = exchange.request.cookies.getFirst("customer-Id")

        if (cookie?.value.isNullOrEmpty()) {
            exchange.response.statusCode = HttpStatus.BAD_REQUEST
            return exchange.response.setComplete()
        } else {
            val request = exchange.request
                    .mutate()
                    .header(HttpHeaders.AUTHORIZATION, cookie!!.value)
                    .build()
            return chain.filter(exchange.mutate().request(request).build())
        }
    }
}

Indem die Komponente das Interface GatewayFilter implementiert, muss die Methode filter() überschrieben werden. Die Methodensignatur lässt bereits die asynchrone, reaktive Umsetzung erkennen: Der Rückgabewert ist vom Typ Mono – dem reaktiven Äquivalent zu CompletableFuture. In Zeile 7 wird das Cookie aus dem Request ausgelesen. Ist es nicht vorhanden oder enthält keinen Wert, kann die Bearbeitung bereits abgebrochen werden und dem Client wird der HTTP-Statuscode 400 zurückgegeben (Zeile 9 bis 11). Somit wird der Backend-Service gar nicht erst aufgerufen. Ist das Cookie vorhanden, wird im Request der Authorization-Header gesetzt. Anschließend wird der modifizierte Request der GatewayFilterChain hinzugefügt, so dass der nächste potentielle Filter ausgeführt werden kann (Zeile 12 bis 18).

Mit diesen beiden einfachen Klassen sind die im Szenario beschriebenen Anforderungen umgesetzt. Die Spring-Boot-Applikation kann nun in der Cloud – beispielsweise als Cloud Foundry App – bereitgestellt werden und agiert dort als Edge-Service.

Alternative Gateway-Frameworks

Neben dem aufgeführten Spring Cloud Gateway existiert eine ganze Reihe weiterer Frameworks, auf deren Basis ein eigener Edge-Service implementiert werden kann. So hat etwa Netflix im Mai 2018 die Version 2 von Zuul Open Source gestellt [9]. Technische Basis bildet das asynchrone, nicht-blockierende Netty-Framework [10]. Eine Integration in Spring Cloud ist aber seitens der Spring-Community nicht geplant. Details zur Erstellung eines API Gateway mit Zuul 2 inklusive Code-Beispiele finden sich in diesem Blogpost [11]. Darüber hinaus gibt es eine ganze Reihe weitere Frameworks wie beispielsweise die in Go geschriebenen KrakenD [12] oder Tyk [13], um nur zwei zu nennen.

Fazit

Im Wesentlichen sind zwei Entscheidungen zu treffen: soll ein API-Gateway eingesetzt werden und wenn ja, welche Lösung kommt zum Einsatz?

API-Gateway: ja oder nein?

Die zwei Kernargumente für ein Gateway sind die Einfachheit für Clients und die Kapselung der Microservice-Architektur. Hat man mehr als einen Endpunkt, ist es unserer Auffassung nach unerlässlich, diese zu bündeln. Die Verwaltung von "Endpunktlisten" in Clients sollte vermieden werden, da sie schnell unübersichtlich werden können.

Da eine Architektur selten statisch ist, sondern sich evolutionär entwickelt, ändern sich im Laufe der Zeit auch die Endpunkte immer wieder. Services können aufgebrochen oder verschmolzen werden. Kommt hier kein Gateway zum Einsatz, sind die Clients diesen Änderungen direkt ausgesetzt. Wird Software ausgerollt (beispielsweise eine mobile App), kann das Entwicklerteam nicht mehr garantieren, dass die Benutzer die neueste Version verwenden. Ein Gateway ermöglicht es in diesem Fall, die API kompatibel zu halten, selbst wenn die Microservice-Landschaft sich immer wieder verändert.

Dabei muss man aber immer im Blick behalten, welche Herausforderungen ein API Gateway mit sich bringt. In erster Linie ist das der Single Point of Failure, den das Gateway in der Architektur darstellt. Damit ergibt sich eine Reihe von Anforderungen an die Verfügbarkeit, Belastbarkeit und an die Fähigkeiten des Gateways. Es muss mindestens so verfügbar sein, wie die Höchstanforderung an einen beliebigen Service dahinter. Es muss auch alle Kommunikationstechnologien unterstützen, welche die aufgerufenen Services benötigen (beispielsweise HTTP/2 oder Websockets).

Wahl der richtigen API-Gateway-Lösung

Wenn es darum geht, ein konkretes Produkt auszuwählen, raten wir dazu, möglichst einfach zu beginnen. Einige Produkte versprechen unzählige Features; das wirkt sich aber auch auf die Komplexität aus. Ein Austausch des Produktes ist meist in Produktion möglich und für die Clients völlig transparent. Deshalb lohnt es sich, die exakten Anforderungen abzuwarten und komplexere Lösungen erst bei Bedarf später einzusetzen.

Auch sollte man es vermeiden, Geschäftslogik oder "Features" aus den Services in das Gateway zu verschieben. Das führt oft zu unübersichtlichen und schwer zu wartenden Systemen mit vielen Abhängigkeiten. Besonders in einer Microservice-Architektur kommt es dadurch zu schwierigen Fragen bei der Verantwortlichkeit (s. dazu: Avoid overambitious API Gateways [14]).

Zusammenfassend lässt sich festhalten, dass für jeden Anwendungsfall und Infrastruktur eine passende Lösung existiert. Gemäß dem Motto "You aren't gonna need it (YAGNI)" empfiehlt es sich, mit einer simplen Implementierung zu starten und diese erst bei Bedarf zu erweitern bzw. auszutauschen.

Autoren

Christian Schwörer

Christian Schwörer unterstützt Kunden bei der digitalen Transformation hin zu verteilten, cloudbasierten Microservice-Architekturen.
>> Weiterlesen

Constantin Weißer

Constantin Weißer ist schwerpunktmäßig in der Java-Entwicklung tätig und betreut in Kundenprojekten verschiedene DevOps-Tätigkeiten und die Administration von Linux-Systemen.
>> Weiterlesen
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben