GraphQL mit AppSync und Lambda

Dieser Beitrag soll einen Einblick in die generelle Denkweise hinter GraphQL vermitteln und das Zusammenspiel von AWS AppSync mit AWS Lambda Funktionen vorstellen. Warum finden wir das spannend? Wer mit uns zusammenarbeitet, weiß, dass wir versuchen den Fokus auf den Kern zu legen um diesen nach vorne zu entwickeln; Serverless ist hierzu eines unserer bevorzugten Werkzeuge und Mindsets. Mit Serverless und dem Ansatz von FaaS können sich Entwickler*innen auf ihren Kern konzentrieren: Business-Logik.

Warum GraphQL?

Mit GraphQL existiert ein weiteres Werkzeug um besseren Fokus zu ermöglichen. Dank GraphQL richtet sich der Blick beim Betreiben einer API auf das Wesentliche: Daten.

Im Gegensatz zu den meisten Strategien für den Betrieb einer API, ist die primäre Frage nicht wie man die Daten und Informationen abruft, sondern welche Daten benötigt werden und in welcher Struktur sie vorliegen. Verbringt man bei klassischen API Modellen viel Zeit damit URL Strukturen und HTTP Pfade zu definieren, rückt bei GraphQL direkt die Frage in den Fokus, welche Daten zur Verfügung stehen und wie diese miteinander verbunden sind.

Dank GraphQL kann der Konsument der API granular bestimmen welche Felder von Objekten benötigt werden und wie diese in der Antwort vom Server strukturiert werden sollen.

Schema

Die Basis von GraphQL ist ein Schema. Das GraphQL Schema beschreibt die Struktur der Daten und wie diese zusammengehören.

Die oberste Ebene im Schema nehmen Query und Mutation ein. So unterteilt GraphQL die grundlegende Interaktion mit Daten in ein Lesen (Query) und ein Schreiben (Mutation).

Objekte

Unterhalb von Queries und Mutations beginnt dann direkt die Struktur der Daten mit Objekten und Typen; die vorhandenen Möglichkeiten sind vergleichbar mit einer Vielzahl an typisierten Programmiersprachen: Es gibt grundlegende Typen wie Integer, String, und Boolean; Listen dieser Typen oder ENUM Felder. Zusätzlich können eigene Typen auf Basis der vorhandenen definiert werden, um den Zweck der Daten besser zu umschreiben.

Zur besseren Ordnung von komplizierten Strukturen unterstützt GraphQL auch Interfaces und Union Types.

Non Null

Für gelegentliche Verwirrung sorgt GraphQL mit dem Ansatz, dass Daten und Felder standardmäßig nullable sind; nur verpflichtende Felder müssen gesondert markiert werden. Viele Programmiersprachen setzen hier auf den umgekehrt Ansatz.

Schema First

Ein bewährter Ansatz für die Entwicklung von GraphQL APIs ist der Schema-first Prozess. Da das GraphQL Schema die Grundlage für jede weitere Interaktion mit Daten ist, ist es sinnvoll alle involvierten Parteien an einen gemeinsam Tisch zu bringen und zusammen die Anforderung an das Schema zu erarbeiten.

Als greifbares Beispiel für ein GraphQL Schema dient hier eine Liste an Personen mit freundschaftlichen Verbindungen untereinander. Die Basis für ein simples GraphQL Schema wäre demnach eine Person nach folgender Struktur:

type Person {
  id: ID!
  name: String!
  age: Int!
  birthday: String!

  friends: [Person!]!
}

Für den lesenden Zugriff auf die Daten werden zusätzlich noch definierte Queries benötigt; um so eine Liste an Personen, oder eine spezifische Person, abfragen zu können.

type Query {
  people: [Person!]!
  person(id: ID!): Person
}

Bereits bei dieser einfachen Datenstruktur und -verbindung fallen vermutlich zwei markante Dinge auf:

  • Wenn eine Person ein Feld age für das Alter in Jahren besitzt und ein Feld birthday mit dem Geburtsdatum als Zeitstempel, sollten diese irgendwie zusammenhängen.
  • Wenn eine Person eine Liste an Freund*innen besitzt, können diese vermutlich wiederum auch eine Liste an Bekanntschaften besitzen, und diese dann natürlich auch wieder; das lässt sich ins Endlose steigern.

Resolver

Beide Beobachtungen bezüglich dem Schema führen zu dem Grund wieso GraphQL so gut zu Serverless und dem Prinzip von FaaS passt: Resolver.

Neben dem grundlegenden Schema sind Resolver der zentrale Baustein von GraphQL. Nach der Definition welche Daten vorhanden sind und wie sie zusammenhängen, bleibt natürlich die Frage wie diese Daten vom API Server abgefragt und zusammengesteckt werden.

In GraphQL sind Resolver die Quelle der jeweiligen Daten. Resolver können für Felder definiert werden (z.B. für die Kalkulation des Alters einer Person auf Basis des Geburtsdatums) oder für einen Query oder eine Mutation.

Da Resolver einen konkret abgesteckten Funktionsrahmen haben, eine definierte Liste an Parameter besitzen und feste Rückgabewerte liefern, schließt sich somit der Kreis zu FaaS und Serverless.

Um das definierte Schema mit Daten zu befüllen, werden folgende GraphQL Resolver benötigt:

  • Kalkulation des Alters auf Basis des Geburtsdatums: Person.age
  • Abfrage aller Freunde einer Person: Person.friends
  • Abfrage aller Personen: Query.people
  • Abfrage einer Person auf Basis der ID: Query.person

Die Spezifikation von GraphQL ist in Bezug auf die Definition der Resolver sehr offen. Jede Implementierung von GraphQL hat hierzu eigene Ansätze, abhängig von der gewählten Technologie. Einige Frameworks können direkt aus Datenbanken lesen und andere erlauben es Callbacks mit dem Schema zu verknüpfen. AWS AppSync ermöglicht es Lambda Funktion mit Feldern im Schema zu verbinden.

AppSync & Lambda Resolver

Wie bei jedem Ansatz bei AWS, der den Fokus auf das Schreiben von konkreter Logik lenken soll, wird ein mögliches Framework durch ein Tooling oder einen Service von AWS ersetzt. AppSync ermöglicht eine einfache Verknüpfung von Feldern im GraphQL Schema mit Lambda Funktionen.

Die Orchestrierung in AppSync kann wie bei allen AWS Produkten entweder über die grafische Oberfläche in der AWS Console geschehen, über das Command Line Interface, oder auch über CloudFormation Templates. Auf GitHub gibt es ein fertiges und betriebsbereites Projekt mit der vorgestellten Datenstruktur inklusive aller CloudFormation Templates und den notwendigen Schritten für das Deployment.

Da natürlich so wenig Aufwand wie möglich mit dem Betrieb der GraphQL API verbunden sein soll, eignet sich AppSync mit der Integration von Lambda Funktionen als perfekte Kombination. Die theoretische Liste an benötigten Resolvern könnte mit Go wie in folgenden Beispielen aussehen.

Resolver für die Liste aller Personen

Alle konkrete Abfragen wie genau die Daten ausgelesen werden, und wo diese konkret gespeichert werden, sind in einem Repository definiert. Auf dieses Repository kann mittels einer Library zugegriffen werden.

func resolver() (repository.People, error) {
  return repository.All(), nil
}

Resolver für eine konkrete Person

Für die Abfrage einer einzelnen Person ist im Schema ein Argument id definiert. Dank AppSync und der Lambda-Integration von Go genügt es die Argumente für die Resolver-Funktion entsprechend zu definieren:

type personPayload struct {
  ID string `json:"id"`
}

func resolver(payload personPayload) (repository.Person, error) {
  return repository.PersonByID(payload.ID)
}

Resolver für das Alter einer Person

Da sich das Alter einer Person kontinuierlich verändert, und nur das Geburtsdatum feststeht, soll das Alter auf Basis des Geburtstags berechnet werden. In einer Datenbank würde man das Alter der Person ebenfalls nicht abspeichern, jedoch den Zeitstempel der Geburt.

Im Vergleich zu den Resolvern für Queries gibt es bei den Feld-Resolvern in GraphQL eine Besonderheit: Die Funktionen erhalten nicht nur mögliche Argumente als Parameter, sondern auch das Datenelement welches zuvor abgefragt wurde.

Sowohl der Resolver für people() wie auch für person(id:ID!) greifen beispielsweise auf eine Datenbank zu und laden das Datenobjekt einer Personen. Das GraphQL Framework führt auf Basis dieser Daten dann die definierte Feld-Resolver bei Bedarf aus. So kann das Alter einer Person auf Grundlage des Geburtsdatums kalkuliert werden:

type personAgePayload struct {
	Birthday time.Time `json:"birthday"`
}

func resolver(payload personAgePayload) (int, error) {
	return int(time.Since(payload.Birthday) / time.Hour / 24 / 365), nil
}

Resolver für alle Freunde einer Person

Ebenso wie beim Resolver für das Alter einer Person, existiert bei der Ausführung des Resolvers für die Freund*innen einer Person bereits das Grundlegende Datenobjekt einer Person; die ID einer Person ist also direkt verfügbar:

type personFriendsPayload struct {
  ID int `json:"id"`
}

func handle(payload personFriendsPayload) (repository.People, error) {
  return repository.FriendsByID(payload.ID)
}

Schlussfolgerungen

Mit den aufgezeigten Beispielen für AppSync Resolver lassen sich direkt Schlussfolgerungen ableiten die hervorragende Argumente für Zusammenspiel von GraphQL und Serverless sind:

Alle gezeigten Code-Zeilen haben eine konkrete Intention. Alle Resolver haben einen klar definierten Funktionsumfang und lassen sich mit Testszenarien abdecken und sicherstellen. Jeder Resolver kennt nur die Daten die benötigt werden; keinerlei Wissen über das gesamte Bild der Datenstruktur ist notwendig für die Teilaufgaben.

Zugriff

Die Kommunikation mit der GraphQL API geschieht über einen zentralen Endpunkt der über HTTPS erreichbar ist. Der API Konsument schickt an den API Endpunkt einen GraphQL Query der die benötigten Daten beschreibt. Auf Basis der Personenstruktur könnte ein Query wie folgt aussehen:

query {
  person(id: "1") {
    name
    birthday
  }
}

Natürlich zeigt das einfache Abfragen des Namens und des Geburtsdatums noch nicht die eigentlichen Stärken von GraphQL auf. Interessanter werden dann jedoch die Abfragen die Resolver beanspruchen:

query {
  person(id: "1") {
    name
    age

    friends {
      name
      
      friends {
        name
        age
      }
    }
  }
}

Alleine auf der Struktur des Schemas und der Orchestrierung von Schema mit Resolver können nun mit einem GraphQL unkompliziert die Freund*innen der Freund*innen von einer Person abgefragt werden.

Schlussworte

Wir hoffen, dass wir durch diesen Einblick das Interesse an GraphQL und auch am Zusammenspiel mit Serverless wecken konnten. Auf GitHub gibt es das skizzierte Beispiel mit einem CloudFormation Template und allen notwendigen Schritten um eine funktionierende GraphQL API mit AppSync bei AWS zu betreiben.

Viel Spaß beim Ausprobieren und falls ihr fragen habt, gerne auf Twitter melden oder per E-Mail Kontakt aufnehmen. Gerne bieten wir auch individuelle Workshops an.

Ausblick

Natürlich ist der Einsatz von eigener Logik, zum Abfragen der Daten in einer Datenbank, immer fehleranfällig. Der nächste logische Schritt ist somit der direkte Zugriff auf eine Datenbank ohne eigenen Programmcode in Lambda Funktionen; AppSync kann dies bereits. In einem kommenden Artikel werden wir diese alternative Implementierung vorstellen. Auf GitHub gibt es das Beispiel aus dem diesem Artikel fast ohne den Einsatz von eigenen Code.

photo of Sebastian

Sebastian is a Senior Cloud Consultant at superluminar GmbH, AWS Serverless Hero and AWS Certified Solutions Architect. He writes here in German about AWS, Serverless, Software development, Go, TypeScript and React. English Articles can be found on his Website sbstjn.com and he can be found on Twitter under @sbstjn.