Multiplayer Game with AWS | StadtLandFluss

Dieser Blogbeitrag soll einen Einblick in die Entwicklung unserer Webanwendung mit den unten definierten Funktionen geben sowie unsere Lösungsansätze, Herausforderungen und Probleme aufzeigen. 

Cloud Computing Vorlesung

Ziel der Vorlesung “Software Development for Cloud Computing” ist es, aktuelle Cloud Technologien kennen zu lernen und diese im Rahmen von Übungen und kleinen Projekten anzuwenden. Unser Team hat sich im Rahmen dieser als Prüfungsleistung zu erbringenden Projektarbeit dazu entschieden, das bekannte Spiel „Stadt, Land, Fluss“ als Multiplayer-Online Game umzusetzen. 

Projekt Idee & Inspiration

Zu Beginn der Vorlesung war sich unsere Projektgruppe noch sehr unsicher, was wir als Projekt mit Cloudkomponenten umsetzen wollten, da wir noch keine bis sehr geringe Vorerfahrung in der Cloud-Entwicklung hatten. Erste Brainstormings hatten ergeben, dass wir gerne eine Webanwendung entwerfen wollten. Jedoch war es gar nicht so leicht Zugriff zu interessanten Daten zu bekommen. 

Letztendlich hat sich unsere Gruppe dazu entschieden, sich nicht von Daten abhängig zu machen, sondern etwas Eigenes zu kreieren. 

Die Inspiration für unsere finale Idee (Stadt-Land-Fluss) war das Online-Spiel Skribbl IO, ein kostenloses Multiplayer-Zeichen- und Ratespiel. Dabei wird in jeder Runde ein Spieler ausgewählt, der etwas zeichnet, das die anderen erraten sollen. Skribbl ermöglicht es dem Spieler auch, einen eigenen Raum zu erstellen und Freunde einzuladen, die einen Link zu diesem Raum teilen.

Im Rahmen unseres Projektes hat uns die Idee gefallen etwas zu entwickeln, was man danach mit Freunden zusammen nutzen kann. Den Multiplayer Ansatz fanden wir spannend, da wir so etwas noch nie umgesetzt haben. Da wir alle Stadt-Land-Fluss Fans sind, fiel unsere Wahl auf dieses Spiel. 

Ziel

Primäres Ziel des Projektes war es für uns, erste Erfahrungen in Cloud-Computing zu sammeln und gleichzeitig unsere Fähigkeiten im Software-Engineering auszubauen. 

Konkret war es die Idee ein Stadt-Land-Fluss Spiel mit den folgenden Funktionalitäten zu entwickeln: 

  • Schritt 1: Raumerstellung
    • Spieler kann einen neuen Raum erstellen, oder über eine Raum-Id einem Raum beitreten
  • Schritt 2: Spieldaten bestimmen
    • Der Spieler, welcher einen Raum erstellt, soll die Kategorien selber bestimmen können, sowie die Zeit, welche man für das Ausfüllen einer Spielrunde hat, ebenfalls sollen Mitspieler- und Rundenanzahl bestimmt werden können
  • Schritt 3: Waiting Room 
    • Nach Erstellen oder Beitreten eines Raumes, kommt der Spieler in einem Warteraum, wo er die festgelegten Parameter der Spielrunden sieht und informiert wird, welche Spieler der Runde schon beigetreten sind 
  • Schritt 4: Letter Generator 
    • Der Buchstabe für eine Runde soll zufällig generiert werden, sich aber nicht wiederholen innerhalb eines Spiels
  • Schritt 5: Spielrunde
    • Auf der Seite der Texteingaben, soll ein Spieler die Runde stoppen können, sobald er alles ausgefüllt hat, dies triggert den Stopp bei allen Mitspielern
  • Schritt 6: Kontrollieren der Eingaben 
    • Alle Spieler sehen nach einer Runde ihre eigenen, aber auch alle anderen Eingaben der Mitspieler sowie die Punkte, die dabei erreicht wurden
    • Dabei werden die Punkte nach folgendem Schema berechnet:
      • Hat ein Spieler als Einziger in dieser Kategorie eine Eingabe und ist diese auch gültig (beginnt mit dem generierten Buchstaben), dann erhält er für dieses Feld 20 Punkte
      • Haben andere in diesem Feld auch Eingaben, erhält der Spieler für eine gültige Eingabe 10 Punkte
      • Hat ein anderer Spieler in der gleichen Kategorie die gleiche Angabe, erhält der Spieler 5 Punkte
      • Ist die Eingabe leer oder beginnt sie nicht mit dem generierten Buchstaben, werden keine Punkte vergeben
  • Schritt 7 : Hall of Fame  
    • Darstellung der Spieler-Ränge und ihrer Punkte nach Abschließen aller Runden

Das erste Mockup der zu erstellenden Webanwendung entsprach folgendem Design und war unser Leitfaden für die Entwicklungsphase: 

Skizze der groben Web Anwendung zu Beginn

Einblick in das Spiel – Demo 

Frameworks – Cloud Services – Infrastructure

Frontend

Aufgrund von vorhandenen Vorerfahrungen wurde die zweite Entscheidung getroffen, das Frontend mit Hilfe des Angular Frameworks umzusetzen. Angular ist ein TypeScript-basiertes Front-End-Webapplikationsframework. Das Backend wurde mit Python als Programmiersprache umgesetzt. Zum einen war hier mehr Vorerfahrung vorhanden bei einigen Teammitgliedern und zum anderen haben wir mehr Beispiele zur Anwendung von Websockets und AWS im Zusammenhang mit Angular gefunden, was uns sehr geholfen hat. 

Backend

Wie zu Beginn schon erwähnt, hat uns die parallel zum Projekt laufende Vorlesung gleich zu Beginn den großen Funktionsumfang von AWS aufgezeigt.  Besonders interessant fanden wir die Einsatzmöglichkeiten von Lambda Funktionen. Im Zusammenhang damit hat uns die Funktion gefallen ein API Gateway aufzubauen zu können. Da man bei der Programmiersprache völlig frei wählen kann, haben wir uns für Python entschieden. In der Python Programmierung hatten wir als Team zwar wenig Erfahrung, haben aber in dem Projekt eine Chance gesehen, uns in dieses Thema weiter einarbeiten zu können und unsere Fähigkeiten zu verbessern. 

Architektur

Architektur

Cloud Komponenten 

Vor dem Projektstart hatten wir zu Beginn die Schwierigkeit zu entscheiden, welchen Cloud-Anbieter wir für die Entwicklung nutzen wollen. Voraussetzungen für die Entscheidungen waren, dass es eine ausführliche Dokumentation der Möglichkeiten und Funktionen gibt (aufgrund der mangelnden Vorerfahrung), ebenfalls wollten wir nicht eine Kreditkarte als Zahlungsoption hinterlegen müssen und auch keine bis sehr wenig Kosten verursachen. 

Zu Beginn der Vorlesung hieß es noch, dass wir eventuell ein Konto bei der IBM-Cloud oder über AWS von der Hochschule bekommen würden. Allerdings war dies leider doch nicht der Fall, weswegen wir nach erstem Warten selbst eine Entscheidung treffen mussten. Wir haben uns schlussendlich für AWS (Amazon Web Services) entschieden, da es einer der führenden Anbieter im Cloud Computing ist. Hierbei hat uns gefallen, dass es sehr viele Tutorials und gute Dokumentation zu den einzelnen AWS Services gab. Ein Nachteil war, dass man beim Anlegen eines Kontos eine Kreditkarte hinterlegen musste. Vorteil war andererseits, dass man mit einem Gratis-Kontingent (Free Tier) an Funktionsaufrufen, Rollen, und DB Kapazitäten etc. startet, weswegen im Rahmen des Projektes dahingehend keine Kosten entstehen sollten. Im späteren Verlauf haben wir herausgefunden, dass man allerdings für die Funktionalität von AmazonCloudWatch, welches ein Service zur Einsicht der Logs ist, zahlen muss. Die Kosten waren nicht hoch, weswegen es kein Problem darstellte, allerdings sollte man sich eindeutig über die Kosten, welche bei der Entwicklung entstehen können, im Klaren sein, um nicht böse überrascht zu werden. 

Amazon Free Tier

Übersicht Free Tier : https://aws.amazon.com/de/free/?all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=tier%23always-free&awsf.Free%20Tier%20Categories=*all

AWS Services 

Serverless 

Für das Anlegen unseres Projektes und auch der späteren Möglichkeit einer übersichtlichen Vorlage des Codes zur Bewertung, haben wir nach einer Möglichkeit gesucht, alle für AWS benötigten Konfigurationen sowie das Anlegen der verschiedenen Lambda-Funktionen in unserem GitLab Reporsitory hinterlegen zu können und direkt im Code Editor bearbeiten und ändern zu können. 

Nach einiger Recherche sind wir dabei auf das Serverless Framework gestoßen. Vorteile des Serverless Frameworks gegenüber der AWS Grafikoberfläche sind, dass alle man alle Elemente wie Datenbanken, Buckets, API-Routen und Aufrufe sowie Lambda Functions über eine serverless.yaml Datei verwalten kann. Zudem ist es nicht nötig, AWS-Kontoschlüsseln oder anderen Kontoanmeldeinformationen in Skripte oder Umgebungsvariablen zu kopieren oder einzufügen. Über die serverless.yaml können alle Ressourcen und Funktionen übersichtlich und schnell angelegt und bearbeitet werden. 

Functions

In der serverless.yaml angelegte Lambda Funktionen entsprechen dem folgenden Schema : 

Funktion Definition in serverless.yml 

Die in der Datei definierten Funktionen werden jeweils über einen eigenen „handler“ referenziert. Das bedeutet, dass sie über ein „Event“ aufgerufen werden können. Ein Event entspricht dabei beispielsweise einem API-Aufruf. 

broadcast_to_room Lambda-Funktion

Resources 

Neben unseren Funktionen sind in der serverless.yml ebenfalls alle Tabellen sowie Buckets definiert. Dies erleichtert die Einrichtung neuer Ressourcen, welche benötigt werden und gibt eine gute Übersicht. 

iamRoleStatements

Zudem können IAM-Rollen und Berechtigungen, die auf Lambda-Funktionen angewendet werden, konfiguriert werden. 

Allgemeine Konfiguration 

Es können ganz einfach allgemeine Settings angegeben werden, wie zum Beispiel: 

Übersicht der Möglichkeiten: https://www.serverless.com/framework/docs/providers/aws/guide/serverless.yml/

Dynamo DB 

Um unser Spiel umsetzen zu können, waren wir darauf angewiesen, bestimmte Daten in einer Datenbank zu speichern, dies waren beispielsweise Elemente wir eine Raum-ID, die Spielernamen, die gewählten Spielparameter, sowie die benutzen Buschstaben und eingetragene Ergebnisse der Spieler. Da die Einträge in der Datenbank immer variieren und keinem starren Muster folgen, beispielsweise ist es möglich eine unterschiedliche Zahl an Kategorien pro Spielraum zu haben, haben wir nach einer NoSQL Lösung gesucht. Wir wollten eine Datenbank mit nicht-relationalen Ansatz, um nicht von einem festen Tabellenschemata abhängig zu sein. Unsere Wahl fiel dabei auf Amazon DynamoDB, welches eine schnelle NOSQL-Schlüssel-Wert-Datenbank für beliebig große Datenmengen ist. 

Es gibt eine Tabelle „game_data“, welche alle relevanten Informationen enthält. 

Datenstruktur

Ein Beispiel unserer Datenstruktur ist in der folgenden Abbildung zu erkennen:

Diese JSON-Datei zeigt ein laufendes Spiel mit insgesamt zwei Spielern. Ganz oben ist die Room-Id gespeichert, welche einen einzigartigen Wert aufweist. Diese ist für die Spielrunde besonders wichtig, da so mehrere Räume generiert und gleichzeitig laufen können. Der darunter gespeicherte Array „categories“ beinhaltet alle Kategorien als String-Elemente, die der Spieler, der den Raum erstellt hat, ausgewählt hat. Das Dictionary „game_players“ enthält die Informationen zu allen Spielern. Dazu gehört bspw. der Username, der als Key im Dictionary agiert, die jeweilige Raum-Id, die Punkteanzahl sowie die Eingaben, die der Spieler für eine Runde eingegeben hat. Daneben werden zwei weitere Werte gespeichert, „next_round“ und „status“, welche dazu dienen, zu erkennen, dass die nächste Runde beginnen kann, also alle Spieler bereit sind, oder ob der Spieler noch aktiv ist, also das Spiel nicht bereits verlassen hat.

Des Weiteren werden die Rundenanzahl, die Spiellänge für eine Runde, sowie in einem Array die generierten Buchstaben der Runden gespeichert. Dieser wird pro Runde immer um den nächsten zufällig generierten Buchstaben erweitert.

Datenbank-Interaktionen

Während eines Spiels müssen immer wieder Spielerdaten abgefragt, verändert, hinzugefügt oder gelöscht werden. Dazu gehören bspw. das Anlegen eines neuen Spiels, das Hinzufügen neuer Spieler zu der jeweiligen Raum-Id, das Aktualisieren der erreichten Punkte etc. Diese Interaktionen haben wir in den Lambda-Funktionen, die später noch einmal genauer erklärt werden, implementiert und sie werden aufgerufen, sobald diese benötigt werden. Da diese in Python implementiert wurden, haben wir die AWS SDK für Python, Boto3 verwendet, die eine Integration von Python-Anwendungen und Bibliotheken in AWS-Services, darunter DynamoDB, ermöglicht.

Quelle: https://aws.amazon.com/de/sdk-for-python/

Abfragen von Daten

Auch wenn Boto3 als AWS-SDK für Python nützliche Methoden bezüglich DynamoDB bereitstellt, war es nicht immer einfach, diese zu verwenden. Da unsere Datenstruktur durch die Verschachtelungen recht kompliziert war, war es aufwendig, die benötigten Informationen abzufragen. 

Die obige Abbildung zeigt einen Ausschnitt aus einer Lambda-Funktion und stellt die Abfrage von Spieldaten dar. Zunächst wird die Verbindung zu der gewünschten Datenbank (game_data) hergestellt, indem Endpoint-Url sowie die Region angegeben werden muss. Mit dieser Datenbank-Instanz kann nun mit Methoden wie „.get()“ auf die gewünschte Information in der Datenbank zugegriffen werden. Das Verwenden dieser Funktionen war demnach einfach, jedoch wurden die Daten in der DynamoDB sehr oft in Dictionaries gespeichert, welches an vielen Stellen gar nicht nötig war, was in der Abbildung durch das sich wiederholende „.get(‚L‘)“, „.get(‚M‘)“ etc. zu erkennen ist.

In Wirklichkeit waren die Daten nämlich so aufgebaut:

Man erkennt, dass die Daten durch DynamoDB zusätzlich in Dictionaries und Arrays geschachtelt werden und es war schwierig zu erkennen, um was es sich nun handelt. Das Abfragen wurde dadurch viel komplizierter gestaltet, als es eigentlich ist und war auch sehr fehleranfällig.

Anlegen von Daten

Das Anlegen neuer Daten in die Datenbank erfolgt mithilfe der Methode „put_item“, welcher ein JSON-Objekt mitgegeben werden muss. Die Abbildung zeigt das Anlegen eines neuen Raumes.

Löschen von Daten

Das Löschen von Daten erfolgt ähnlich wie das Updaten von Daten, da der jeweiligen Methode „delete_item“ mehrere Parameter mitgegeben werden können, die den Befehl spezifizieren. In unserem Projekt haben wir komplizierte Aufrufe jedoch nicht gebraucht, weshalb das Löschen im Vergleich zum Updaten von Daten einfach war.

Update von Daten

Das Updaten von Daten erfolgt mithilfe der Methode „update_item“, dessen übergebene Parameter komplexer sind als die anderen und auch abhängig davon sind, welche Typen (Array, Dictionaries, ein einfacher Wert) aktualisiert werden sollen. Um zu bestimmen, welches Objekt in der Datenbank bearbeitet werden soll, kann ein sogenannter „Key“, welcher in der obigen Abbildung die Raum-Id ist, mitgegeben werden. Zusätzlich können die übergebenen Parameter überprüft (ConditionExpression) und schließlich bestimmt werden, wie das Objekt aktualisiert werden soll. In diesem Beispiel wird dem Array, der die generierten Buchstaben eines Spiels speichert, um einen Wert erweitert, weshalb als Wert für den Parameter „UpdateExpression“ ein „list_append“ verwendet wird.

S3

AWS S3 (Simple Storage Service) ist ein Service für das Speichern von Objekten. In unserem Projekt haben wir S3 benutzt, um unsere Anwendung zu hosten. Hierfür haben wir einen S3-Bucket erstellt und dort unter „Static website hosting“ die Einstiegs- und Fehlerseite unserer Anwendung angegeben. Damit Benutzer über die dabei erzeugte URL auf unsere Seite zugreifen können, haben wir diese anschließend noch öffentlich zugreifbar gemacht und eine Policy hinzugefügt, um im öffentlichen Modus den Inhalt des Buckets lesen zu können.

Über die AWS CLI können danach immer die aktuellen Files der gebauten Anwendung in S3 hochgeladen werden, wenn die Anwendung deployt werden soll (siehe auch CI/CD Kapitel):

Ausschnitt aus der CI/CD Pipeline zum Hochladen der Dateien in S3

Lambda

„AWS Lambda ist ein Serverless-, ereignisgesteuerter Computing-Service, mit dem Sie Code für praktisch jede Art von Anwendung oder Backend-Service ausführen können, ohne Server bereitzustellen oder zu verwalten. Sie können Lambda in über 200 AWS-Services und Software-as-a-Service (SaaS)-Anwendungen auslösen und Sie zahlen nur für das, was Sie nutzen.“

https://aws.amazon.com/de/lambda/?c=ser&sec=srv

Amazon API- Gateway 

Für die definierten Lambda Funktionen hat man bei AWS die Möglichkeit eine eigene Web-API mit einem http-Endpunkt zu erstellen. Dafür kann man das Amazon API Gateway verwenden. Die Funktion des API-Gateway ist es Tools für die Erstellung und Dokumentation von Web-APIs, die HTTP-Anforderungen an Lambda-Funktionen weiterleiten, zu erstellen. 

Websocket API Routen 

Api RouteBeschreibung
broadcast_to_roomSendet eine Nachricht an alle Spieler in einem Raum anhand der Raum-Id und den in der Dynamo DB gespeicherten Connection-Ids der Spieler.
check_roundsetzt den Status der nächsten Runde des jeweiligen Benutzers und sendet die Nachricht für die nächste Runde an die Spieler im Raum, wenn alle Benutzer die nächste Runde angeklickt haben.
create_room Erstellt einen neuen Raum in der Datenbank mit den angegebenen Werten oder Standardwerten, wenn keine Werte angegeben werden.
enter_roomFügt einen Benutzer zu einem Raum hinzu, indem sein Username sowie die entsprechende Connection-Id in der Dynamo DB unter dem Schlüssel der angegeben Raum-Id gespeichert werden. 
get_current_playersPrüft, ob der Spieler den Raum betreten darf und sendet alle Spielernamen des Raumes an den neuen Spieler.
get_results_for_roomSendet Raumdaten und Spielerdaten an alle Spieler des Raums mit der angegebenen Raum-Id.
load_user_inputslädt alle Benutzereingaben aus der Datenbank und sendet alle Werte (z.B. [[“Stuttgart”, “Rhein”, “Deutschland”]]) an die Spieler des Raums mit der angegebenen Raum-Id.
navigate_players_to_next_roomNavigiert alle Spieler in einem Raum zum Spielraum und wird aufgerufen, wenn der Hauptakteur die Taste “Spiel starten” drückt.
play_roundFragt Timer, Kategorien und Rundeneinstellungen für den Spielraum über die Room-Id aus der Datenbank ab. 
remove_player_from_roomSetzt den Spielerstatus auf inaktiv in der Dynamo DB und löscht die Raumdaten aus der Datenbank, wenn alle Spieler dieses Raums inaktiv sind.
save_roundSpeichert Benutzereingaben aus Kategoriefeldern in der Datenbank.
start_roundBeginnt die nächste Runde, indem er einen neuen Buchstaben erzeugt und prüft, ob dieser Buchstabe bereits ausgewählt wurde und speichert den erzeugten Buchstaben in der Datenbank.
stop_roundStoppt eine Spielrunde, wenn jemand die Stopptaste gedrückt hat. 

Amazon CloudWatch 

Amazon CloudWatch entspricht einem Überwachungs- und Beobachtungsservice für Entwickler.  Wir wollten über CloudWatch die Verwendung unserer API protokollieren. Dabei gibt es die Möglichkeit der Ausführungsprotokollierung. Hierbei verwaltet das API-Gateway die CloudWatch-Protokolle. Es können verschiedene Protokollgruppen erstellt werden, welche dann die verschiedenen Aufrufe, Abfragen und Antworten an den Protokollstrom melden. Zu den protokollierten Daten gehören beispielsweise Fehler. 

Protokollgruppen

Logs der Methode load_user_inputs

Testing 

Beim Testing haben wir uns hauptsächlich auf das Testen der Lambda-Funktionen mit Unittests fokussiert. Dafür haben wir die Bibliothek Moto benutzt, mit der man AWS Services mocken kann. Dadurch konnten wir in den Tests unsere Datenbank mocken und beispielsweise auch testen, ob beim Aufruf der Lambdas Datenbankeinträge richtig angelegt oder Daten richtig aktualisiert werden. Allgemein muss für das Mocken der Datenbank nur die Annotation @mock_dynamodb2 über der Testklasse eingefügt werden und anschließend kann in der setUp-Methode die Datenbank definiert werden, die für die Tests benutzt werden soll. Dadurch können auch Testdaten in die Datenbank eingefügt werden, um bestimmte Testfälle zu testen.

Neben Moto haben wir die Bibliothek unittest.mock benutzt, mit der zum Beispiel das Senden einer Nachricht über die Websocket-Verbindung gemockt werden kann, oder auch der Aufruf einer Lambda-Funktion. Zudem kann man mit Methoden wie assert_called() oder assert_called_with(…) überprüfen, ob und mit welchen Argumenten die gemockte Methode aufgerufen wurde. Allgemein war dies bei unserem Projekt sehr hilfreich, da wir in fast jeder Lambda-Funktion Nachrichten über die Websocket-Verbindung schicken und somit auch testen konnten, ob die richtigen Nachrichten geschickt werden.

Für manuelle Tests oder zum kurzen Testen von bestimmten Eingabewerten war auch die Seite https://www.piesocket.com/websocket-tester sehr hilfreich, da man dort die verschiedenen Lambda-Funktionen über eine Websocket-Verbindung aufrufen kann.

CI/CD Pipeline 

CI/CD Pipeline in GitLab auf dem master-Branch

Um unsere Tests automatisiert ablaufen zu lassen und auch andere Schritte wie das Deployen der Lambda-Funktionen nicht immer manuell ausführen zu müssen, haben wir in GitLab eine CI/CD Pipeline erstellt. Da wir alle vor diesem Projekt noch nie eine CI/CD Pipeline angelegt hatten, konnten wir somit durch das Projekt auch Erfahrungen in diesem Bereich sammeln. Allgemein ist unsere Pipeline unterteilt in verschiedene Stages: In der Testing-Stage werden die Unittests für unsere Lambda-Funktionen ausgeführt. In der Build-Stage werden die aktuellen Versionen der Lambda-Funktionen über das serverless-Framework deployt und anschließend wird unsere Angular-Anwendung gebaut. Am Ende wird unsere Anwendung in der Deploy-Stage deployt, sodass sie danach über eine öffentliche Url verfügbar ist. Je nachdem, auf welchen Branch ein Entwickler in unserem Repository pusht, werden unterschiedliche Jobs ausgeführt. So werden zum Beispiel die Unittests auf jedem Branch ausgeführt, das Deployen der Lambda-Funktionen jedoch nur beim Pushen auf den develop- oder master-Branch und das Deployen der Anwendung nur beim Pushen auf den master-Branch.

Insgesamt gibt es in GitLab unter Settings -> CI/CD die Möglichkeit, Variablen anzulegen, die in der CI/CD Pipeline benutzt werden. Wir haben daher in IAM (Identity and Access Management) bei AWS einen Benutzer für die Pipeline angelegt, der nur die Rechte hat, die in der Pipeline benötigt werden. Die Keys dieses Benutzers haben wir anschließend als Variablen in GitLab angelegt, sodass zum Beispiel das Deployen der Lambda-Funktionen in der Pipeline mit diesem Benutzer durchgeführt werden kann.

Schwierigkeiten 

Während unserer Arbeit am Projekt sind uns einige Schwierigkeiten begegnet, die im Folgenden näher beschrieben werden.

Datenstruktur

Wie bereits beschrieben hatte sich das Abfragen von Daten in der DynamoDB als sehr aufwendig und komplex dargestellt, auch wenn der Methodenaufruf an sich leicht zu verstehen und einfach ist. Durch die weiteren Verschachtelungen, die AWS zusätzlich zu unserer bereits komplexen Datenstruktur hinzugefügt hat, wurde das Abfragen von Daten schwieriger dargestellt, als es eigentlich ist und auch der Code, den wir dafür geschrieben haben, wurde durch dieses sehr unleserlich. Dadurch kam es in unserer Gruppe oft zu Fehlern bei den Abfragen, da der Fehler nicht direkt erkannt wurde und die endgültige Datenstruktur in der Datenbank für unsere Gruppe unklar wurde. 

Wir haben diesbezüglich, auch nachdem wir die Struktur festgelegt hatten, mehrmals gelesen, dass DynamoDB für komplexe Datenstrukturen und Interaktionen (auch das Updaten von Daten) schlichtweg nicht geeignet ist. Dennoch haben wir es dabei belassen, da wir bereits im Projekt weit fortgeschritten waren und keine Zeit mehr hatten, eine Alternative zu finden.

Deployen von Lambdas

Damit die Lambdas, die wir in der serverless.yaml definiert und in den jeweiligen Handler-Dateien implementiert hatten, in unserer Anwendung aufgerufen werden konnten, war es nötig, den Befehl „serverless deploy“ aufzurufen, der alle definierten Lambdas in den Handler-Dateien deployed. Das Problem war, dass dadurch bestehende Funktionen, an denen andere Gruppenmitglieder arbeiteten, anschließend überschrieben wurden, was das Arbeiten sehr behinderte, wenn man an dem Projekt gleichzeitig arbeitete.

Um diese Situation weitestgehend zu verhindern, haben wir beschlossen, die Methoden auf dem Development Branch allesamt mit dem Befehl „serverless deploy“ zu deployen. Anschließend wird auf unterschiedlichen Branches gearbeitet und anschließend nur die Funktion, an der man gerade arbeitet, mit dem Befehl „serverless deploy function –function [Name der Funktion]“ deployed. Dieses hat nur teilweise funktioniert, da das Deployen einer einzigen Funktion nur möglich war, wenn diese bereits existiert, also durch „Serverless deploy“ deployed wurde.

Fazit

Alles in allem haben wir durch das Projekt einen Einblick in die Möglichkeiten von Cloud Computing bekommen und konnten verschiedene Dinge in diesem Bereich ausprobieren. Wir konnten vor allem Erfahrungen mit AWS Lambda, API Gateway und dem serverless Framework sammeln, da dies der Schwerpunkt unseres Projektes war. Zudem haben wir einige grundlegende Dinge gelernt, zum Beispiel dass es sinnvoll ist, schon früh im Projekt eine CI/CD Pipeline aufzubauen oder auch CloudWatch zu aktivieren, um Fehler in den Lambda Funktionen schneller zu erkennen und beheben zu können.

Allgemein haben wir durch das Projekt auch gelernt, dass es sehr wichtig ist, sich am Anfang Gedanken zu Themen wie Security oder auch dem generellen Aufbau des Projektes zu machen. Aus Zeitgründen verfolgt man sonst oft die am schnellsten funktionierende Lösung und kann dann später nur mit viel Aufwand grundlegende Dinge ändern. Für das nächste Projekt würden wir daher mehr Zeit für die Einarbeitung und Recherche einplanen, um dies zu vermeiden. Bei unserem Projekt wäre es vor allem sinnvoll gewesen, schon von Beginn an die verschiedenen Umgebungen einzurichten, sodass alle Entwickler lokal unabhängig voneinander entwickeln und testen können und man auf der Entwicklungsumgebung Änderungen durchführen kann, ohne die Produktivumgebung zu beeinflussen.

“Studidash” | A serverless web application

by Oliver Klein (ok061), Daniel Koch (dk119), Luis Bühler (lb159), Micha Huhn (mh334)

Abstract

You are probably familiar with the HdM SB-Funktionen. After nearly four semesters we were tired of the boring design and decided to give it a more modern look with a bit more functionality then it currently has. So we created “Studidash” in the course “Software Development for Cloud Computing”. “Studidash” shows your grades and automatically calculates the sum of your ECTS and also an average of your grades. 

Since this is a project for SD4CC it runs as a serverless web application at Amazon Web Services, or AWS for short. Our tech stack for this project consists of Angular, Python, Terraform and some AWS Services like Lambda or S3.

While developing this Web-App we encountered some difficulties but also learned a lot of stuff and we hope that this blog post can give you a quick overview of what we did, what we learned, what problems we had and how we solved them so you have it easier for your next project.

What did we do? 

As mentioned in the abstract, we developed a serverless Web-App called “Studidash” because of said boring design of the SB-Funktionen. First of all, we decided that we wanted to learn a new tech stack and came to the conclusion that Angular as our frontend would be the most modern frontend framework. For our backend we decided to use Python since it’s lightweight and easy to learn. From another course we learned about Terraform so this was something we were already somewhat familiar with and decided to use it for our deployment to AWS. We also used AWS to host the Web-App since we got access to AWS Student Accounts.

After we settled for a project and our tech stack we had to think about a way to make it “cloud native” and started to research some information and came across serverless. We dug a bit deeper and found some useful information. So we came to realize that serverless might be the way to go. Serverless means that our (or maybe your application) isn’t running completely on a “on-prem”-server but is running in the cloud instead. That means the application itself isn’t coupled to the server. Servers are still there but you don’t have to think about the administrative stuff around that. This is all going to be handled by your cloud service provider. The serverless approach brings scalability, high availability and efficient resource usage and management with it. As mentioned, you can focus more on the development itself rather than thinking about servers. A connection to a CI/CD pipeline makes it easy and fast to release a new version of your application. But serverless also has its downsides. The functions have to be as small as possible to only fit one purpose and some Web-Apps can have higher latency due to a cold start (When a function isn’t used for quite some time it gets destroyed and needs to be instantiated again, which takes time). You are also going to have a bad time debugging your application since it isn’t as easy as you might be used to. In the end we went with a static frontend in a S3-Bucket, a backend running as AWS Lambda Functions and AWS API Gateway to connect them. 

Architecture

Our architecture is fully hosted on AWS and our code repositories are hosted on the HdM GitLab server. The clients can access our frontend via their favourite web browser. Our frontend application is hosted in an AWS S3-Bucket. The good thing here is that we don’t have to manage or deploy any web server by ourselves. This reduces the management overhead and in the end the costs. After the frontend is served to the client, the user can input their user credentials to access their grades from the third party service (HdM SB-Funktionen). A HTTP-Request will then be sent to a Lambda Function with an API-Gateway to receive the request. This Lambda Function contains a Python script which will parse the user credentials provided in the received HTTP-Request and use them to make a login at the SB-Funktionen platform and scrape the necessary grades and lecture data from the user. This scraped data will then be preprocessed and returned as a JSON-Object to the frontend.

From the developer side we used Git/GitLab for the version control of our code. In GitLab we created a CI/CD pipeline to build the frontend, the Python grade scraper and a Terraform image to deploy all our neccessary AWS resources. Thanks to the CI/CD pipeline the developer can just push the newest code base to the repository and it will be deployed automatically to AWS.

Architecture overview

Frontend

For our frontend we decided to build an Angular single page application. We made this decision because it’s an up-to-date framework to build fast and easy web applications.

When the user loads the website the header only displays a login component for the HdM SB-Funktionen credentials. This component triggers a POST request to the Lambda Function containing the login data. The Lambda Function then responds and returns several grade objects to the frontend which are identically defined in front- and backend. The grade object exactly maps the table structure of the HdM page. The response then triggers the rendering of the table and you will receive a login message. Also there is an error handling if the login failed. The table can be sorted according to the different values, the grade average and ECTS are calculated and displayed in the header of the page.

Screenshot of our frontend after successful login

Backend

Our backend consists of a Python script which is hosted in a Lambda Function with an API-Gateway to receive HTTP-Requests. The frontend sends a HTTP-Request with the user credentials in the request body to the API-Gateway. The request is then forwarded to the Lambda Function which then injects the HTTP-Request into our Python grade scraper script. The Python script then parses the request body and performs a login at the SB-Funktionen website of the HdM where all the student grades and lectures are stored.

Backend workflow

In our code example the event variable is the received HTTP-Request from the frontend. The received request body is a string, so the content of the body has to be parsed to JSON again. When there is no login data provided, the script will send a HTTP-Response with the status code 401 and a corresponding message.

In the next step our script scrapes all the data we need and parses them into a JSON format which our frontend can handle easily. This JSON data is then sent as response to the Lambda Function which will forward this response to the API-Gateway. The API-Gateway then also forwards this response back to our frontend application where the received data will be processed and displayed.

Code snippet – try-except

We also had to keep some other things in mind. For example what should happen when our backend throws an exception or the third-party-service isn’t available? In our backend we created an error handler which takes a HTTP-Status Code and an error message as parameter, converts the data in the right format for our frontend and then sends the response.

Code snippet – error handling

Our main lambda_handler function is then divided into different parts. Each part is surrounded by a try-except clause to catch exceptions. For example if the third party service is down or if there were no credentials provided by the frontend. This makes our backend more reliable and also gives the user enough feedback to know what’s going on. Since we use an external service we need to think of a solution for the case when the third party service is down, for example for maintenance reasons. A possible solution to this would be to implement a caching mechanism which we don’t provide in the current state.

CI/CD

To make our application as cloud native as possible we implemented a CI/CD pipeline in our project. This pipeline builds our Web-App as well as our Lambda Functions, tests our Python script and deploys them to AWS. For that we are using different stages (build, test, deploy) in our .gitlab-ci.yml file. The build_webapp stage first pulls a Node-image and runs a few lines of script to install all dependencies and then builds the Angular based frontend. While this part is running, a second instance is pulling an Alpine image and is also running a few lines of script to package our Lambda Function(s) into a ZIP file.

After that, the test stage is invoked to test the application before deployment. This is a crucial part in the pipeline since it can reveal mistakes that we made during development before going “live” with the application. When the tests succeed, the next stage is invoked.

In our case, we made the deployment stage manually since we didn’t want to push every small change to AWS and also the Student Accounts had some time limits that would forbid us doing that anyway. But what happens in the deploy stage is fairly simple. Like in the stages before we are pulling an image for Terraform to run the usual Terraform commands like init, validate, plan and apply. This initializes Terraform, validates our main.tf in the root of the repository, creates a plan for creating the different resources in this main.tf and finally applies it. 

But what exactly is in this main.tf file? This file contains every resource we need in AWS and creates it. First of all, we declared variables for our different buckets, one for the Lambda Function and one on which the Angular app is going to be hosted at. After that, we created the S3-Bucket for the Lambda Function and uploaded the ZIP file with the function to the bucket. From there, it gets deployed to AWS Lambda. We also needed to create a role and policy to give the bucket the correct access rights to execute their task properly. After that, the S3-Bucket for the Angular app is created and the needed files are uploaded. This bucket hosts the frontend as a static website which we also configured in our main.tf to do that.

.gitlab-ci.yml file for our pipeline (1/2)
.gitlab-ci.yml file for our pipeline (2/2)

Testing

Testing is one of the most important things when implementing a CI/CD pipeline and with automated deployment. When you don’t implement tests you don’t really know if your application works before deployment and after the deployment, it is too late. So implementing a stage for testing in our project was the way to go. For our Python backend we wrote some basic Unit-Tests to test functionality and also added a test stage for the backend to our CI/CD pipeline.

We also managed to write an End-To-End-Test for our frontend which checks if the Error-Snackbar is shown when the user puts in wrong credentials. The harder part in this scenario was to get it running in the CI/CD pipeline, which we unfortunatly didn’t manage to do.

What problems did we have and how did we solve them?

One of the biggest problems we encountered was due to the fact that we only had access to an AWS Student Account. It ensured that we only had restricted access to AWS. For example we needed to create different kinds of roles to deploy our Lambda Function with the correct set of rights to be executed. Due to the restrictions we were not allowed to give the roles the needed permissions which caused our CI/CD pipeline to fail and our project didn’t get fully deployed. This could only be solved by getting a “real” AWS Account which gives you all the permissions you would need.

Another problem we faced was CORS (Cross-Origin Resource Sharing). In the first steps of our development we always got a CORS-Error when our frontend was requesting the grades and lecture data from our backend service. The reason for that was because in our Python backend script we just sent back the JSON-Object containing all the data but without any HTTP-Headers to our frontend. The frontend then failed to receive the response because the URL of the API-Gateway was different from the URL that our frontend had. To fix this problem we had to set the Access-Control-Allow-Origin HTTP-Header in the response from our backend. 

Code snippet – http-headers (CORS)

After that, the request worked and our frontend could receive the scraped data.

Another problem we had was to integrate our End-to-End-Test in our CI/CD-pipeline, which we unfortunately didn’t manage to fix in time. It would’ve required us to have a runner that has a browser available but we weren’t able to set that up. We managed to implement an E2E-Test which is running locally without any problems. So at least we have a bit of code quality assurance here. Having to run the tests manually isn’t what you want to do for a fully automated cloud native approach.

Conclusion

It was quite a long way from where we started, but in the end we managed to get our Web-App running on AWS as we liked. We made it a bit difficult in the beginning because we said we wanted to learn some new technologies like Python and Angular, so we first had to learn those. But we also had to learn about serverless-architecture. It is also something to look forward to working with in the future.

At the presentations we found out about AWS Amplify, which is basically a tool by AWS to get serverless Web-Apps running as fast as possible without the need of S3-Buckets. It showed us that there isn’t really the “one and only” way to get something running in the cloud. There are many possible solutions. 

In our opinion we learned a lot about AWS, serverless-architecture and cloud in general. But also about developing an application where you don’t have to think about renting and maintaining a server. Maybe we can continue with this project in the near future and give the HdM SB-Funktionen a new look 🙂

Application Updater mit Addon-Verwaltung

von Mario Beck (mb343) und Felix Ruh (fr067)

Einleitung

Unser Ziel war es, einen Programm Updater für Entwickler zu erstellen, den diese einfach in ihre CI/CD-Pipeline integrieren können. Für die Umsetzung haben wir die IBM Cloud und eine Serverless Architektur verwendet, um eine unbegrenzte Skalierbarkeit zu erreichen. Zu den verwendeten Serverless Services zählen die Cloud Functions, DB2 und ein Object Storage.

Das Projekt besteht aus einem Uploader, mit dem der Entwickler sein Programm in den Object Storage hochladen kann. Und einem Downloader für den Benutzer, mit dem automatisch die aktuelle Version heruntergeladen wird.

Verwendung aus der Entwicklersicht:

  • Programm wird registriert und man bekommt die dazugehörigen API-Keys
  • Erstellen der Config für den Downloader
  • Mit dem Uploader kann das Programm hochgeladen werden, dies kann einfach in eine CI/CD Pipeline eingebunden werden

Verwendung aus der Benutzersicht:

  • Herunterladen des Downloaders und der Config
  • Starten des Downloaders
  • Vor Programmstart wird nach neuen Updates gesucht und diese falls vorhanden heruntergeladen
  • Nach dem Update wird das eigentliche Programm gestartet
Continue reading

Cloudbased Image Transformation

Introduction

As part of the lecture „Software Development for Cloud Computing“, we had to come up with an idea for a cloud related project we’d like to work on. I had just heard about Artistic Style Transfer using Deep Neural Networks in our „Artificial Intelligence“ lecture, which inspired me to choose image transformation as my project. However, having no idea about the cloud environment at that time, I didn’t know where to start and what is possible. A few lectures in I had heard about Infrastructure as a Service (IaaS), Platform as a Service (PaaS) and Function as a Service (FaaS). Out of those three I liked the idea of FaaS the most. Simply upload your code and it works. Hence, I went with Cloud Functions in IBMs Cloud Environment. Before I present my project I’d like to explain what Cloud Functions are and how they work.

What are Cloud Functions?

Choose one of the supported programming languages. Write your code. Upload it. And it works. Serverless computing. That’s the theory behind Cloud Functions. You don’t need to bother with Infrastructure. You don’t need to bother with Load Balancers. You don’t need to bother with Kubernetes. And you definitely do not have to wake up at 3 am and race to work because your servers are on fire. All you do is write the code. Your Cloud Provider manages the rest. Cloud provider of my choice was IBM.

Why IBM Cloud Functions?

Unlike Google and Amazon, IBM offers FREE student accounts. No need to deposit any kind of payment option upon creation of your free student account either. Since I have no experience using any cloud environment, I didn’t want to risk accidentally accumulating a big bill. Our instructor was also very familiar with the IBM Cloud, in case I needed support I could have always asked him as well.

What do IBM Cloud Functions offer?

IBM offers a Command Line Interface (CLI), a nice User Interface on their cloud website, accessible using the web browser of your choice and very detailed Documentation. You can check, and if you feel like it, write or edit your code using the UI as well. The only requirement for your function is: It has to take a json object as input and it has to return a json as well. You can directly test the Function inside the UI as well. Simply change the Input, declare an example json object you want to run it with, then invoke your function. Whether the call failed or succeeded, the activation ID, the response time, results, and logs, if enabled, are then displayed directly. You can add default input Parameters or change your functions memory limit, as well as the timeout on the fly as well. Each instance of your function will then use the updated values.

Another nice feature of IBM Cloud Functions are Triggers. You can connect your function with different services and, once they trigger your function, it will be executed. Whether someone pushed new code to your GitHub repository or someone updated your Cloudant Database, IBMs database service. Once invoked by this trigger, your function executes.

You can also create a chain of Cloud Functions. The output of function 1 will then be the input of function 2.

IBM Cloud Function use the Apache OpenWhisk service, which packs your code into a Docker Container in order to run it. However, if you have more than one source file, or dependencies you need, you can pack it in a docker image or, in some cases, like Python or Ruby, you can also zip them. In order to do that in Python, you need a virtual environment using virtualenv, then zip the virtualenv folder together with your python files. The resulting zip files and Docker images can only be uploaded using the CLI.

You can also enable your function as Web Action, which allows it to handle HTTP Events. Since the link automatically provided by enabling a function as web action ends in .json, you might want to create an API Definition. This can be done with just a few clicks. You can even import an OpenAPI Definition in yaml or json format. Binding an API to a function is as simple as defining a base path for your API, giving it a name and creating an operation. For example: API name: Test, Base path for API: /hello and for the operation we define the path /world select our action and set response content type to application/json. Now, whenever we call <domain>/hello/world, we call our Cloud Function using our REST-API. Using the built-in API-Explorer we can test it directly. If someone volunteers to test the API for us, we can also share the API Portal Link with them. Adding a custom domain is also easily done, by dropping the domain name, the certificate manager service and then Certificate in the custom domain settings.

Finally, my Project

Architecture of the Image Transformation Service

The idea was:

A user interacts with my GitHub Page, selects a filter, adds an Image, tunes some parameters, then clicks confirm. The result: They receive the transformed image.

The GitHub Page has been written with HTML, CSS and JavaScript. It sends a POST request to the API I defined, which is bound to my Cloud Function, written in Python. It receives information about the chosen filter, the set parameters and a link to the image (for the moment, only jpeg and png are allowed). It then processes the image and returns the created png byte64 encoded. The byte64 encoded data will then be embedded in the html site and the user can then save the image.

The function currently has three options:

You can transform an image into a greyscale representation.

Left: Original Image by Johannes Plenio, Right: black and white version

You can upscale an image by a factor of two, three or four

Left: Original Image by Johannes Plenio, Right: Upscaled by a factor of 2

and you can transform an image into a Cartoon representation.

Left: Original Image by Helena Lopes, Right: Cartoon version

Cartoon images are characterized by clear edges and homogenous colors The Cartoon Filter first creates a grayscale image and median blurs it, then detects the edges using adaptive Threshold, which currently still has a predefined window size and threshold. It then median filters the colored image and does a bitwise and operation between every RGBA color channel of our median filtered color image and the found edges.

Dis-/ Advantage using (IBM) Cloud Functions

Serverless Infrastructure was fun to work with. No need to manually set up a server, secure it, etc. Everything is done for you, all you need is your code, which scales over 10.000+ parallel instances without issues. Function calls themselves don’t cost that much either. IBMs base rate is currently $0,000017 per second of execution, per GB of memory allocated. 10.000.000 Executions per month with 512MB action memory and average execution time of 1.000ms only cost $78,20 per month, including the 400,000 GB-s free tier. Another good feature was being able to upload zip packages and docker images.

Although those could only be uploaded using the CLI. As a Windows user it’s a bit of a hassle. But one day I’ll finally set up the 2nd boot image on my desktop pc. One day. Afterwards, no need for my VM anymore.

The current code size limit for IBM Cloud Functions is 48 MB. While this seems plenty, any modules you used to write your code, not included by default in IBMs runtime, needs to be packed with your source code. OpenCV was the module I used before switching over to Pillow and numpy, since OpenCV offers a bilateral filter, which would have been a better option than a median filter on the color image creation of the Cartoon filter. Sadly it is 125 MB large. Still 45 MB packed. Which was, according to the real limit of 36 MB after factoring in the base64 encoding of the binary files, sadly still too much. Neither would the 550 MB VGG16 model I initially wanted to use for an artistic style transfer neural network as possible filter option. I didn’t like the in- and output being limited to jsons either. Initially, before using the GitHub Page, the idea was to have a second Cloud Function return the website. This was sadly not possible. There being only a limited selection of predefined runtimes and modules are also more of a negative point. One could always pack their code with modules in a docker imag/zip, but being able to just upload a requirements.txt and the cloud automatically downloading those modules as an option would have been way more convenient. My current solution returns a base64 encoded image. Currently, if someone tries to upscale a large image and the result exceeds 5 MB, it returns an error, saying „The action produced a response that exceeded the allowed length: –size in bytes– > 5242880 bytes.“

What’s the Issue?

Currently, due to Github Pages not setting Cross Origin Resource Sharing (CORS) Headers, this does not work currently. CORS is a mechanism that allows web applications to request resources from a different origin than its own. A workaround my instructor suggested was creating a simple node.js server, which adds the missing CORS Headers. This resulted in just GET requests being logged in the Cloud API summary, which it responded to with a Code 500 Internal Server Error. After reading up on it, finding out it needs to be set by the server, trying to troubleshoot this for… what felt like ages, adding headers to the ajax jquery call, enabling cross origin on it, trying to workaround by setting the dataType as jsonp. Even uploading Cloud Function and API again. Creating a test function, binding it to the API (Which worked by the way. Both as POST and GET. No CORS errors whatsoever… till I replaced the code). I’m still pretty happy it works with this little workaround now, thank you again for the suggestion!

Other than that, I spent more time than I’m willing to admit trying to find out why I couldn’t upload my previous OpenCV code solution. Rewriting my function as a result was also a rather interesting experience.

Future Improvements?

I could give the user more options for the Cartoon Filter. the adaptive Threshold has a threshold limit, this one could easily be managed by the user. An option to change the window size could also be added, maybe in steps?

I could always add new filters as well. I like the resulting image of edge detection using a Sobel operator. I thought about adding one of those.

Finding a way to host a website/find a provider that adds CORS Header, allowing interested people to try a live-demo and play around with it, would be an option as well.

What i’d really like to see would be the artistic style transfer uploaded. I might be able to create it using IBM Watson, then add it as sequence to my service. I dropped this idea previously because i had no time left to spare trying to get it to work.

Another option would be allowing users to upload files, instead of just providing links. Similar to this, I can also include a storage bucket, linked to my function in which the transformed image is saved. It then returns the link. This would solve the max 5 MB response size issue as well.

Conclusion

Cloud Functions are really versatile, there’s a lot one can do with them. I enjoyed working with them and will definitely make use of them in future projects. The difference in execution time between my CPU and the CPUs in the Cloud Environment was already noticeable for the little code I had. Also being able to just call the function from wherever is pretty neat. I could create a cross-platform application, which saves, deletes and accesses data in an IBM Cloudant database using Cloud Functions.

Having no idea about Cloud Environments in general a semester ago, I can say I learned a lot and it definitely opened an interesting, yet very complex world I would like to learn more about in the future.

And at last, all Code used is provided in my GitHub repository. If you are interested, feel free to drop by and check it out. Instructions on how to set everything up are included.

2 player Connect 4 in the cloud

Play Connect 4 here

Annika Strauß – as324
Julia Grimm – jg120
Rebecca Westhäußer – rw044
Daniel Fearn – cf056

Introduction

As a group of four students with little to no knowledge of cloud computing our main goal was to come up with a simple project which would allow us to learn about the basics of software development for cloud computing. We had decided a simple game would do the trick. And to make it a little more challenging it should be a two player online game. First we thought of Tic Tac Toe but that seemed too simple. Then we took a look at the Chinese game of Go, but that was too complicated. In the end we agreed on Connect4. Not too simple. Not too complicated.


Getting started / Prerequisites / Tech Choices

In our first group meeting we sat down and brainstormed on all the requirements, features and technologies we would require to realize the game. We also tried to avoid coming up with too many additional features that would be nice to have but exceed our possible workload. The main focus was to get something running in the cloud.

Our version of Connect Four therefore should have a simple user login, so that one can play a game session with other players, interrupt the game and come back later to finish where one left off, which also means game sessions need to be saved between two players. We thought about matching players via a matching algorithm, in order to ensure that players of about equal strength get matched, but then we realized that was way too much effort and our focus really should stay on getting something done. So we decided on simply make it a random match up, or adding friends and connect via a friend list, since this is a study project, not a commercial game.

Of course we don’t really assume that millions of people are going to play our game at once, but we want to learn about scalability, so we are going to act as if no one has ever played it before and everyone in the world is going to act like it’s the new Pokémon Go. In reality it will probably just be the four of us and whomever we show it to.

We already knew beforehand that we would want to program the game in Python, simply because it is getting pretty popular in the web application field and we want to get some experience with it. Also, we finally want to learn something other than Java. But we also want to make sure Python actually is a good choice, so we’re going to check the pros and cons just to be sure and possibly decide on something else later.

We also agreed that we want to use a NO-SQL Database for saving game sessions, simply because they can store arrays and we want to get more experience with it. Again, we must check first if it’s a viable option.

Next question we needed to ask, is what infrastructure, what platform would we put this on. AWS? Our own hardware? IBM Cloud? What are the options there? Also, what requirements does our game bring with it for the platform? Do we need micro services? Should we use Docker? How is the scalability going to be handled? So many questions. It was time to ask the Master for some wisdom. So we talked to lecturer Thomas Pohl, who gave us some very useful insights.

Luckily for us IBM Cloud is available for free for students. And as Thomas works for IBM and has plenty of experience with it, it’s kind of a no brainer to go with IBM Cloud. As we found out, IBM Cloud already provides a great deal of infrastructure and platform, all handled by IBM, which allows us to simply focus on deployment. The service we are looking at in particular is called Cloud Foundry. It handles all the scaling, load balancing and everything automagically for you in the background, so that we can focus on simply getting our game running. It comes with a great variety of tools for almost any technology requirements we desire.

This is definitely the sandbox we want to be playing in. With some help from Thomas we came up with this relatively simple architecture:

So what exactly do we require from the cloud foundry? To answer this we first need to ensure we have all the technology choices figured out. Meaning, programming language, database, user login authentication and so on.

First we’ll start with the programming language. Python. Is it a good choice?

To get a clear impression of whether or not to use Python some research is necessary. After reading some online articles and watching some videos, we came to the conclusion that it is a good choice. Why? Similar to Java, it is an interpreted language and is easy to use. It is portable and has a huge library and lots of prewritten functions. The main drawback is that it is not too mobile friendly, but we’re not worried about that. And it is so simple, that other languages may seem to tedious  in comparison. But it will get the job done quickly and it is simple to maintain. Debugging works very well, it has built in memory management and most importantly, it can be deployed in Cloud Foundry. So, Python is definitely a winner. Other languages may also be a good choice, but the point is not to find the best language, but to confirm that Python is not a bad choice. Cause, we want to learn Python, and we just want to be sure it’s not a waste of time. And from what I’ve read, Python is a pretty good choice.

SQL or NoSQL? We want to use NoSQL, but does that make sense? To answer this question we need to have a look at what kind of data we are actually storing and what the advantages and disadvantages are of using NoSQL and see how it compares to SQL.

SQL is great if one has complex data that is very interwoven and a write would mean updating several different places, which SQL manages well by merely linking the data via relations instead of duplicating it. NoSQL stores all relevant data in one place, which makes reading really fast, but making any changes can mean, having to replace all duplicates of the data. So, NoSQL is efficient if changes only need to take place in one area.

Now, the data we are storing is basically just a game session. This mainly consists of two users, which will not likely change. Then the game state, which only changes in the saved game. And once the game is done, the entire game session no longer needs to be stored anyway and can probably be deleted.

The player data will be coming from a UserDataBase supplied by App ID, a user login service provided by IBM. Feeding this data into either DB Type should be no problem. But the main reason for using a NoSQL Database remains arrays. As we will be storing the game progress in form of arrays, getting them translated into something a normal SQL database could handle would be way too tedious and NoSQL can handle storing arrays no problem.

We’ve also just touched on the topic of handling user accounts. As already mentioned IBM provides a called App ID. Rather than using a social media login service such as facebook provides or worse, coming up with a whole system on our own, we were very happy to discover this tool already existed in the IBM cloud foundry. So we gladly decided to use that.


Getting the show on the road. Sort of.

Now that we had decided on all of our technical resources it was time to actually make it all. So, we all created our IBM Cloud accounts and started setting everything up. Now we had to look around Cloud Foundry and figure out where and how we would find all the services we needed and which ones were the right ones for us. Our main services needed to be:
a place for the main python app to live – Python Cloud Foundry
a NO-SQL database – Cloudant
a user login manager – AppID

Everything was relatively simple to find and set up. Our main app would be living in the Python Web App with Flask. A basic web serving application. For the NO-SQL database would use IBMs Cloudant. For the login of users we set up AppID. Now, I’m not going to go into detail on how we set it up, since it was pretty simple and anyone with a basic understanding of clicking through a webpage could have done most of this.

Everything we needed was in place. Our little playground was ready. Now came the really fun part. Actually writing the code. And with it, all the problems we needed to overcome, which would help us learn the ins and outs of cloud computing.


Random Problems we encountered

Merging Cloud Foundry Python Server with our Python-Flask Server

IBM Python Cloud Foundry delivers the following server.py:


We wanted to load our Flask Templates instead. How to do that? The server.py sets the folder “static” as the starting point, which contains the static index.html. But just putting the path onto the templates is not enough, as Flask is initalized with app.run(). Could we just replace server.js with a Python-Flask server? What on the server.js do we need so that the server still runs in the cloud.
We picked out the port and added it in our app.js (Python-Flask server).

Read port selected by the cloud for our application:

Adding the port in the app start:

So we added this in the first row:

Success! It worked!


Couldn’t start the app from the Cloud anymore
The app wouldn’t start anymore after pushing it to the cloud. We got an error message: “[errno 99] cannot assign requested address in namespace”.
This meant that our app couldn’t be found under the URL in the cloud. The mistake was that when we loaded locally we were using “localhost:8000”, but in the cloud, that doesn’t work of course. What was the correct address in the cloud? Adding host=’0.0.0.0′ into app.run solved the issue. Now we could run the app from the cloud and locally.

Explanation
If you bind localhost or 127.0.0.1 it means you can only connect to the service locally.
10.0.0.1 cannot be connected, as it is not ours. We can only connect to IPs which belong to our computer.
We CAN bind 0.0.0.0 though, because this means, all IPs are on our computer, so that every IP can connect to it.



Templates not found!!!
While two of us were working with Visual Studio Code, the templates for the front end html stuff were working quite nicely locally. But one of us was using PyCharm. And PyCharm did not know where the templates were and kept saying, wrong path, template not found.
On our quest for answers we were victorious and found ProfHase85 in one of the threads on stackoverflow.com. We followed his wisdom and did as he said: “Just open the project view (View –> Tool Windows –> Project). Once there, though shalt right-click on your templates folder. Not left-click. Not double-click. And most certainly not center-click. No. Though shalt right-click on it. There you will find Mark as Direcory and from there you will find the Template Directory. It is there, that thee will find salvation. There you will set the path of your template and all shall come to life.”
So, that worked great. Thank thee, ProfHase85.



The NO-SQL Cloudant DB

When getting our Cloudant DB running we immediately ran into several problems. When we tried to create the DB, as it said in the tutorial from IBM, nothing happened. We then found that the code checks to see if the instance is a cloud instance. And we were trying to run the code locally. So we needed to push our code first and run it from the cloud.
The we needed to enter the domain name in the manifest.yml, which took us a while to find, which turned out to be eu-gb.mybluemix.net.
Then everything decided to freeze anytime when the password was being prompted. Apparently we made a mistake when we created the DB when configuring the authentication method. We said to only use IAM. So we went back and created the database again, and during setup the authentication method to use both legacy and IAM. Now we also had our in our service credentials and creating the DB and connecting to the Cloudant service finally worked.

Using the DB statically as well as dynamically
Now we were able to use Cloudant locally and in the cloud and could add static data in form of documents to our DB. The next problem we were facing was getting data like the username out of the user input into the document and making that data accessible again. Unfortunately our course on accessibility didn’t help in this case.
Unfortunately the documentation on what is possible with Cloudant didn’t seem to be very expansive.
Functions such as checking if a file exists were only possible locally, but not from the Python-Cloudant extension in the cloud. After several days of trying around we finally had the idea that maybe it was the accessibility functionality that was causing this. Maybe we needed to use IAM to access the Cloudant DB from the Python-Cloudant extension.

After a small issue with finding the right username we tried to connect using:

But were greetet with: Error: type object ‘Cloudant’ has no attribute ‘iam’

IAM requires at least Python-Cloudant version 2.9.0 and for whatever reason the version had in our requirements was 2.3.1. Problem solved. Connection finally established.
And then the next problem came flying along. When updating a file:
409 Client Error: Conclict document update at document.save()
What? OK. More reading up to do. Went and read through this article:
https://developer.ibm.com/dwblog/2015/cloudant-document-conflicts-one/
This didn’t bring us much further, but it seemed to be better to use document.updatefield() rather than trying to go directly into the DB, in order to avoid simultaneous calls.


How to sort data in CloudantDB
Three SQL programmers went into a NO-SQL bar.
They came back out after five minutes because they couldn’t find a table.


In Cloudant data is stored in completely independent documents. This makes everything more flexible, but also very cluttered and difficult to differentiate when reading. Without any kind of sorting, all data needs to be searched for a specific ID.
For our project we needed the following data structures:

We had to differenciate between users and game sessions. How could we accomplish this in Cloudant?

Use views?
A view makes a query quick and easy. But anytime a document gets updated, so does the whole view, which is counterproductive with big data sets.

Partitions?
We found out, one can create a partitioned DB in Cloudant, whereby naming the ids as follows:
<partition>:<documentid>

In our case:
games:gameID123 and users:userAbc

This way one can add the partition to the queries, resulting in much better search performance. And also the DB looks a lot tidier.

Search query example:

And that was that.


AppID – The “simple” user login service for web apps

IBM offers a nice little web app called AppID. Easy to integrate. Made for the cloud. Great security features. Easy, right? Well, they have all the code you need for Java or node.js. But not Python. So, a few more lines of code, research and effort. How hard can it be?
AppID is based on OIDC (OpenID Connect). Since we used Python we needed to fall back on Flask-pyoidc. This module is a OIDC client for Python and the Flask framework which interacts with AppID for authentication.

Configuring the OpenID Connect Client

The metadata in “appIDinfo” serves as input for configuring the OIDC client.

Securing Web Routes
After configuration the OpenID client can be used to secure single pages or sections (“Routes”) of the web-app. This is achieved by attaching a decorator to the rout definition:

“@auth.oidc_auth” ensures that the code only gets executed for authenticated users.

The first problems with using AppID already arose with establishing a connection. First we tried connecting with a direct approach via the create button, which show a connection in the browser, but not when pushing the app to the cloud. So, we created the service again directly in the project via the command line. And voila. The next test push got a connection.

Creating an instance of the AppID service
We connected to “ibmcloud resource service-instance-create connect4AppID appid lite eu-gb”. After that an alias of the service instance is created in Cloud Foundry. Then we had “ibmcloud resource service-alias-create connect4AppID –instance name connect4AppID

And we had a finally established a connnection between our app and the AppID service. Seemed like things were coming along. But then of course we encountered the next problem. Turns out the redirect_uri doesn’t work with secured connections.

And then the next problem was that the AppID login widget was probably not going to work with our Python-Flask app either. So, we decided not to use AppID after all. Instead we created our own user login in python.

Sometimes something that looks like plug and play turns out to be plug and pray. And in this case our prayers weren’t heard. But now we know that one needs to thoroughly check the capabilities of these services before trying to implement them.


The heart of the game/Game Engine

Probably the most challenging part of creating this game was writing the main application, as the first slew of questions arose. How do you write a game for two players? How to connect the database? How do make it refresh when a player makes a move so the opponent can see it? How do make taking turns work, so the opponent is blocked from making another move?

Reloading the window after a player made a move
After some online research we figured out we could use a socket server to handle the multiplayer functionality. But that seemed like way too much overhead as it meant possibly having to learn an entire new framework.
The first issue we needed to takle was getting the web page to update/reload for both players, anytime one made a move.
With Python-Cloudant one can listen to changes in the DB, but unfortunately this loop blocks all other actions in Python. Were cloud functions maybe the answer? They are like serverless event listeners. The function gets triggered when a watched event occurs. And fortunately the was even a quickstart template available from IBM Cloud. You can create an action sequence and a trigger on the DB. We would need to call a Flask template in the cloud function. But it was unclear if that was possible. So we tried a Python-Cloudant only approach instead. Same as before, but this time asynchronous. That way the feed can run continuously and listen for changes in the DB.
But now the problem is that the asynchronous loop, which is waiting for changes in the DB, cannot be executed at the same time as the return render_template and is blocking. Which also means that it’s blocking server side, which is causing the website to freeze.
According to a post on stackoverflow threading is a better solution. One can deamonize a thread and thereby make it run in the background.
But, then it was time for a new approach. Getting a better understanding. What is a feed? What is a trigger? Several documentations and coffees later we finally came up with a proper solution.

The Solution to Cloud Functions
After enough reading we finally uncovered the proper way to do this.
Under cloud.ibm.com/functions/actions select “Trigger”
1. Create new Trigger. Select a premade Cloudant Trigger. Select your DB and the actions selected in the next step will be triggered anytime something changes in the DB.
2. Create the actions, which are supposed to be triggered.
3. Create a sequence, which executes the actions in order.
4. Add the actions to the sequence.
5. Add the sequence to the trigger.
6. Call the API link from getDocument in Javascript: wait with Async await and recursive timeout functions till the trigger gets triggered. If the DB change occures during the current game, the waiting players window gets reloaded.
As we’ve never done anything with async await, cloud functions or APIs we definitely learned a lot getting this done.

Javascript code:


The Architecture

The final architecture


In the end, this is what our final architecture looked like. As seen in the square, our main app is comprised of Python-Flask, which handles all the python code and displaying the front-end view. The python code itself is the game engine and also handles all the back end connectivity. Javascript supports front end interactivity and listens to cloud functions. The cloud functions are bound to the DB and are set to trigger when changes in the DB occur. Javascript then reloads the web page to display the current state of the game. The DB contains the user data and current game sessions and is continuously updated by the main app. And the users sit in front of the web browser and enjoy all the magically appearing fun on their screen, without a clue of how much blood sweat it takes to make that magic happen.

Conclusion

What can one say? Everything always sounds so simple and easy in theory. But when it comes down to it, one often gets stuck on little things. Some choices we made in the beginning were good, some were quite challenging and some lead to dead ends. When we started out, we had only developed software for local use on computers or mobile devices. The closest we’ve gotten to something like cloud computing was maybe getting something to run over a network. It is quite challenging getting everything to run in the cloud. It’s a whole new game. Similar, but different rules. And even with all the services provided by IBM, we still ran into many obstacles. Especially when developing locally and then trying to make it work in the cloud.
Also, getting all the different types of technology to work together is pretty tricky. Only with experience will one get good at it. Because you won’t know if the service can provide the functionality one requires until you try it. And often we needed additional features or functions we didn’t think of beforehand. Aspects we didn’t consider. The software technologies we’ve encountered may be very powerful, but with great power comes great confusion. But that is where progress happens. Not when everything is going smoothly, but when one is faced with difficult challenges. And we’ve had plenty.
I would say that this project, this course, has been one of the most beneficial in our studies at the HdM. We had the opportunity to get our hands dirty, with expert guidance in a safe environment. The experience we’ve gained is priceless. Our understanding of cloud computing and our ability to develop software for such has progressed several levels. And since this was our main goal I would have to say that our project was a complete success.

Production Monitoring – Industry 4.0

When I was invited to a design thinking workshop of the summer school of Lucerne – University of Applied Sciences and Arts, I made my first experience with the end user interaction part of Industry 4.0. It was an awesome week with a lot of great people and made me interested in the whole Industry 4.0 theme. So when we did projects in the lecture of cloud development I was sure to do a production monitoring project. Continue reading