Ballerina: Die Programmiersprache für die Cloud und deine Integration-Needs

13.03.2024Stefan Welsch
Cloud Cloud Computing Developer Experience Microservices Serverless open-source programming

Banner

Endlich mal wieder eine neue Programmiersprache

Heute widme ich mich Ballerina, einer Open-Source-Cloud-native Programmiersprache, die für die Integration optimiert wurde. Laut Webseite heisst es sogar folgendermassen:

“Ballerina is the ONLY programming language designed for integration.”

Entwickelt und unterstützt wird sie durch WSO2. Laut Angabe der Entwickler soll es mit Ballerina möglich sein, problemlos Microservices, API-Endpunkte und -Integrationen sowie jede andere Anwendung für die Cloud zu entwickeln. Das will ich mir heute mal genauer anschauen und herausfinden, was die neue Programmiersprache denn wirklich so drauf hat.

Vorher vielleicht noch ein paar Details über Ballerina. Das erste Release von Ballerina war am 01.Mai 2019. Die Semantik von Ballerina wird nicht durch die Implementierung definiert, sondern durch Spezifikationen. Derzeit gibt es aber nur eine Implementierung dieser Spezifikation (jBallerina) die, wie der Name bereits vermuten lässt, den Source Code in Java Bytecode kompiliert. Es soll aber in Zukunft noch andere Implementierungen geben. Ein Blick auf die Github Page von Ballerina lohnt sich auch. Hier hat man eine recht gute Übersicht, welche Module und Erweiterungen es rund um die Sprache gibt und auch wie aktiv derzeit daran entwickelt wird.

So aber jetzt geht es dann los. Also als Erstes muss man sich die Ballerina Distribution für sein entsprechendes System installieren. Für mich als Mac User mit Homebrew ist es recht einfach:

1
2
3
4
5
> brew install bal
> bal version
Ballerina 2201.8.5 (Swan Lake Update 8)
Language specification 2023R1
Update Tool 1.4.2

Danach kann ich schon mit meinem ersten Experiment beginnen. Für die Entwicklung bietet sich Visual Studio Code an. Es gibt hier eine schöne Extension, die neben dem üblichen Syntax Highlighting und Code Completion auch noch einen entsprechenden Dev Container zur Verfügung stellt, wenn wir das wollen. Ausserdem gibt es eine UI, mit welcher wir den Code als eine Art Sequenzdiagramm(Service Diagramm) abbilden können und auch über das Diagramm den Code editieren können. Es gibt ausserdem auch einen HTTP- und einen GraphQL-API Designer.

Wollen wir uns nun aber mal dem ersten Beispiel widmen und fangen ganz einfach mit einem “Hello World” Beispiel an. Dies können wir uns mit dem folgenden Befehl erstellen:

1
bal new hello-world 

Schauen wir uns die Struktur des Projekts an. Dazu öffnen wir das Projekt in Visual Studio Code.

image-20240229075738599

Es gibt die Datei Ballerina.toml in der verschiedene Metadaten enthalten sind. Daneben wurde noch eine .gitignore und eine .devcontainer.json Datei erstellt, mit dem wir, wie bereits oben erwähnt, den DevContainer starten können. Für uns wichtig ist aber erstmal nur die Datei main.bal in welchen wir unseren Code schreiben können.

Von der Syntax her sehen wir, dass die Sprache wie eine Mischung aus Java und Go aussieht. Wie wir sehen können, gibt es über der main() Methode nun 3 “Buttons”, welche wir klicken können. Wollen wir die Applikation also erstmal ausführen, indem wir auf “Run” klicken. image-20240229082231618

Nun interessiert uns erstmal weniger was im Output passiert, sondern eher vielmehr was denn genau beim Klick auf Run passiert. Im target Folder sehen wir, wie bereits oben erwähnt, dass die Source zu einer Java Applikation kompiliert wurde. Im Output sehen wir natürlich ein “Hello, World!”.

Ok wollen wir mal einen Schritt weiter gehen und versuchen eine HTTP API aufzurufen. Hierzu bietet uns Ballerina das Modul http.

Wir ändern unseren Code in der main.bal also folgendermassen ab.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import ballerina/http;

service / on new http:Listener(9090) {

    resource function get breeds() returns json|error? {
        http:Client jcClient = check new ("https://api.thedogapi.com/");
        map<string> headers = {
            "x-api-key": "***"
        };

        json response = check jcClient->/v1/breeds('limit = 3, headers = headers);

        return response;
    }
}

Mit dem Code wollen wir bei der Dog API nach Rassen von Hunden suchen. Ihr könnt euch dort kostenlos einen API Key erstellen, wenn ihr damit spielen wollt.

In Zeile 3 erstellen wir uns einen HTTP Listener, welcher auf den Port 9090 gebunden ist. Wenn unserer Applikation gestartet ist, können wir diese also auf dem Port 9090 aufrufen. Dies ist quasi unser Server. Auf Zeile 5 registrieren wir anschliessend einen GET Endpunkt, welcher auf den Pfad breeds hört. Als Rückgabe erwarten wir entweder ein json oder einen error, falls es ein Problem beim Aufruf der Dog API gibt.
In Zeile 6 erstellen wir uns einen HTTP Client, welcher mit dem Dog API Endpunkt kommunizieren kann.

[!NOTE]

Das check Keyword erspart uns das Error-Handling in Ballerina. Stattdessen werfen wir den Error einfach an die aufrufende Methode oder den aufrufenden Block weiter. Alternativ könnten wir auch folgendes machen, um den Error sofort zu behandeln.

1
2
3
4
http:Client|http:ClientError jcClient = new ("https://api.thedogapi.com/");
if jcClient is error {
  // handle error here
}

Zeile 7-9 erstellt uns lediglich den Header, welchen wir zur Authentifizierung an den Dog API Endpunkt mitschicken müssen. Auf Zeile 11 führen wir nun den Request gegen die Dog API aus. Mit 'limit geben wir einen Query Parameter an welcher dem Service sagt, wie viele Ergebnisse wir zurück erwarten und zusätzliche geben wir auch noch den api-key Header mit. Als Response erwarten wir ein json, welches wir an den aufrufenden Client zurückgeben.

Wir können unser Programm nun mit dem folgenden Befehl starten:

1
2
3
4
5
> bal run
Compiling source
        swelsch/hello_world:0.1.0

Running executable

Nun öffnen wir ein zweites Terminal und testen unseren soeben angelegten Endpunkt.

1
2
❯ curl localhost:9090/breeds                                         4s
[{"weight":{"imperial":"6 - 13", "metric":"3 - 6"}, "height":{"imperial":"9 - 11.5", "metric":"23 - 29"}, "id":1, "name":"Affenpinscher", "bred_for":"Small rodent hunting, lapdog", "breed_group":"Toy", "life_span":"10 - 12 years", "temperament":"Stubborn, Curious, Playful, Adventurous, Active, Fun-loving", "origin":"Germany, France", "reference_image_id":"BJa4kxc4X", "image":{"id":"BJa4kxc4X", "width":1600, "height":1199, "url":"https://cdn2.thedogapi.com/images/BJa4kxc4X.jpg"}}, {"weight":{"imperial":"50 - 60", "metric":"23 - 27"}, "height":{"imperial":"25 - 27", "metric":"64 - 69"}, "id":2, "name":"Afghan Hound", "country_code":"AG", "bred_for":"Coursing and hunting", "breed_group":"Hound", "life_span":"10 - 13 years", "temperament":"Aloof, Clownish, Dignified, Independent, Happy", "origin":"Afghanistan, Iran, Pakistan", "reference_image_id":"hMyT4CDXR", "image":{"id":"hMyT4CDXR", "width":606, "height":380, "url":"https://cdn2.thedogapi.com/images/hMyT4CDXR.jpg"}}, {"weight":{"imperial":"44 - 66", "metric":"20 - 30"}, "height":{"imperial":"30", "metric":"76"}, "id":3, "name":"African Hunting Dog", "bred_for":"A wild pack animal", "life_span":"11 years", "temperament":"Wild, Hardworking, Dutiful", "origin":"", "reference_image_id":"rkiByec47", "image":{"id":"rkiByec47", "width":500, "height":335, "url":"https://cdn2.thedogapi.com/images/rkiByec47.jpg"}}]

Wie wir sehen können bekommen wir wie erwartet 3 Ergebnisse von unserem Service zurück.

Nun wollen wir uns die Namen und die Lebenserwartung der Tiere in einer Datenbank abspeichern. Wir nutzen intern eine Serverless AWS DynamoDB Datenbank und glücklicherweise stellt uns Ballerina auch schon eine entsprechende Extension zur Verfügung. Bevor wir aber die Werte in der DB speichern, wollen wir diese erstmal in einem spezifischen Type speichern. Auch hierzu bietet uns Ballerina sehr ähnlich zu golang einen Typ an. Wir erweitern also unser main.bal File um den folgenden Inhalt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import ballerina/http;
import ballerina/os;

type Breed readonly & record {
    int id;
    string name;
    string life_span;
};


service / on new http:Listener(9090) {

    resource function get breeds() returns Breed[]|error? {
        http:Client jcClient = check new ("https://api.thedogapi.com/");

        Breed[] breeds = check jcClient->/v1/breeds('limit = 3, has_breeds=1, headers = {
            "x-api-key": os:getEnv("THE_DOG_API_KEY")
        });

        return breeds;
    }

Wir speichern die Response vom Webservice nun also nicht mehr in einem generischen Response, sondern in einem sogenannten Record. Hierbei handelt es sich um einen sogenannten open record. Was das ist, sehen wir gleich.

Wenn wir unser Programm nun starten und wieder einen curl auf den Endpunkt breeds ausführen erhalten wir erstaunlicherweise das gleiche Ergebnis wie vorher:

1
2
> curl localhost:9090/breeds_record
[{"id":1, "name":"Affenpinscher", "life_span":"10 - 12 years", "image":{"id":"BJa4kxc4X", "width":1600, "height":1199, "url":"https://cdn2.thedogapi.com/images/BJa4kxc4X.jpg"}, "breed_group":"Toy", "temperament":"Stubborn, Curious, Playful, Adventurous, Active, Fun-loving", "origin":"Germany, France", "weight":{"imperial":"6 - 13", "metric":"3 - 6"}, "bred_for":"Small rodent hunting, lapdog", "reference_image_id":"BJa4kxc4X", "height":{"imperial":"9 - 11.5", "metric":"23 - 29"}}, {"id":2, "name":"Afghan Hound", "life_span":"10 - 13 years", "country_code":"AG", "image":{"id":"hMyT4CDXR", "width":606, "height":380, "url":"https://cdn2.thedogapi.com/images/hMyT4CDXR.jpg"}, "breed_group":"Hound", "temperament":"Aloof, Clownish, Dignified, Independent, Happy", "origin":"Afghanistan, Iran, Pakistan", "weight":{"imperial":"50 - 60", "metric":"23 - 27"}, "bred_for":"Coursing and hunting", "reference_image_id":"hMyT4CDXR", "height":{"imperial":"25 - 27", "metric":"64 - 69"}}, {"id":3, "name":"African Hunting Dog", "life_span":"11 years", "image":{"id":"rkiByec47", "width":500, "height":335, "url":"https://cdn2.thedogapi.com/images/rkiByec47.jpg"}, "temperament":"Wild, Hardworking, Dutiful", "origin":"", "weight":{"imperial":"44 - 66", "metric":"20 - 30"}, "bred_for":"A wild pack animal", "reference_image_id":"rkiByec47", "height":{"imperial":"30", "metric":"76"}}]

Stimmt nicht ganz, denn wer genauer hinsieht der bemerkt, dass sich die Reihenfolge der Attribute geändert hat. Es kommen erst die Attribute, welche im Record definiert wurden und anschliessend alle anderen, welche uns die API zurückliefert. Nun hier kommt der bereits oben erwähnte Open Record ins Spiel. Dieser nimmt zusätzliche Attribute als Type any einfach mit auf. Hätten wir einen Closed Record definiert, hätten wir hier einen Fehler erhalten, weil es Attribute gibt, welche nicht gemappt werden können. Ein Closed Record wird wie ein Open Record definiert, allerdings ist nach der geschweiften Klammer noch eine Pipe

1
2
3
4
5
type Breed readonly & record {|
    int id;
    string name;
    string life_span;
|};

Die Antwort vom Service wäre dann die folgende gewesen:

1
{"timestamp":"2024-03-07T17:35:40.821494Z", "status":500, "reason":"Internal Server Error", "message":"Payload binding failed: 'json[]' value cannot be converted to 'hello_world:Breed[]': \n\t\tfield '[0].weight' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[0].height' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[0].bred_for' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[0].breed_group' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[0].temperament' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[0].origin' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[0].reference_image_id' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[0].image' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[1].weight' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[1].height' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[1].country_code' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[1].bred_for' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[1].breed_group' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[1].temperament' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[1].origin' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[1].reference_image_id' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[1].image' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[2].weight' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[2].height' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[2].bred_for' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\t...", "path":"/breeds_record", "method":"GET"}

Ok wollen wir uns also um das Schreiben in die Datenbank kümmern. Hierzu erstellen wir uns ein neues File dynamob.bal in welches wir unser Logik zum Schreiben in die Datenbank kapseln wollen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import ballerinax/aws.dynamodb;
import ballerina/os;
import ballerina/log;

dynamodb:ConnectionConfig amazonDynamodbConfig = {
    awsCredentials: {
        accessKeyId: os:getEnv("ACCESS_KEY_ID"),
        secretAccessKey: os:getEnv("SECRET_ACCESS_KEY")
    },
    region: os:getEnv("AWS_DEFAULT_REGION")
};

dynamodb:Client amazonDynamodbClient = check new (amazonDynamodbConfig);

function persist(Breed[] breeds) returns boolean|error {
    foreach var breed in breeds {
        dynamodb:ItemCreateInput createItemInput = {
            tableName: "dogs",
            item: {
                "Id": {"N": breed.id.toString()},
                "Name": {"S": breed.name},
                "Lifespan": {"S": breed.life_span}
            }
        };
        dynamodb:ItemDescription response = check amazonDynamodbClient->createItem(createItemInput);
        log:printInfo(response.toString());
    }
    return true;
}

[!NOTE]

Leider gibt es momentan noch keine Möglichkeit eine “lokale” DynamoDB zu nutzen. Im Code vom Dynamodb Client wird der AWS Host fix gesetzt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public isolated function init(ConnectionConfig config) returns error? {
        self.accessKeyId = config.awsCredentials.accessKeyId;
        self.secretAccessKey = config.awsCredentials.secretAccessKey;
        self.securityToken = config.awsCredentials?.securityToken;
        self.region = config.region;
        self.awsHost = AWS_SERVICE + DOT + self.region + DOT + AWS_HOST;
        string endpoint = HTTPS + self.awsHost;

        http:ClientConfiguration httpClientConfig = check config:constructHTTPClientConfig(config);
        self.awsDynamoDb = check new (endpoint, httpClientConfig);
    }

Ok, wollen wir uns mal anschauen was hier passiert. In Zeile 4 - 12 wird der DynamoDB Client mit den erforderlichen Daten erstellt. Diese habe ich einfacherweise als Environment Variablen angezogen, da ich im Setup tlr.dev nutze. Ein Tool welches ich wirklich sehr empfehlen kann, wenn man Secrets für die lokale Entwicklung managen will. Ein entsprechendes Techup habe ich vor fast 2 Jahren auch darüber schon geschrieben.

Anschliessend implementieren wir in Zeile 15 - 29 unsere Methode zum Speichern der Hunderasse. Der Code ist denke ich selbsterklärend, so dass ich mir die Erklärung an dieser Stelle spare :-).

In der main.bal rufen wir die neue Methode nun einfach auf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    resource function get breeds_and_persist() returns anydata|error? {
        http:Client jcClient = check new ("https://api.thedogapi.com/");

        Breed[] breeds = check jcClient->/v1/breeds('limit = 3, has_breeds=1, headers = {
            "x-api-key": os:getEnv("THE_DOG_API_KEY")
        });

        var result = check persist(breeds);

        return result;
    }

Das wars schon. Wir haben nun eine vollständige Applikation, welche uns Daten von einem Webservice in eine DynamoDB schreibt. Codetechnisch finde ich es sehr einfach und intuitiv zu verstehen.

Ballerina Visual Studio Code UI

Wollen wir uns aber noch ein Highlight anschauen, was ich bereits oben erwähnt habe. Die Visual Stude Code Extension kommt ja mit einer UI, welche wir uns nun noch schnell anschauen wollen.

image-20240307201131770

In der rechten oberen Ecke gibt es ein Symbol, mit welcher wir das Diagramm anzeigen können. Bei einem Klick öffnet sich erstmal eine Übersicht aller Komponenten.

image-20240307201254321

Wir können nun durch einen Klick auf die entsprechende Komponente einen genaueren Blick darauf werfen und diese sogar über die UI editieren. Schauen wir uns einmal die Funktion genauer an.

image-20240307201426817

Wir sehen in der Darstellung was genau vom Ablauf her in der persist Funktion passiert. So hat man immer eine schöne visuelle Darstellung von dem geschriebenen Code und sieht auch, wann die Kommunikation mit der DynamoDB passiert. Wir können nun auf eines der Kästchen klicken um den Code direkt zu editieren. Beim Klick auf das Kästchen in der Mitte, also dort wo “createItem” aufgerufen wird, sehen wir das folgende Fenster:

image-20240307202000662

Wollen wir uns noch den Service anschauen. Durch einen Klick auf das Haus in der oberen linken Ecke, gelangen wir zurück zur Komponenten-Übersicht. Dort klicken wir nun auf den Service / und sehen alle Endpunkte, welche in unserer Applikation registriert sind. Ich habe zum Testen mehrere Endpunkte angelegt, daher habe ich 4 Endpunkte in meiner Ansicht.

image-20240307202238759

Das UI Feature ist zwar ganz nett, aber ich als Code Enthusiast mag es dann doch noch lieber in Textform. Ich finde das Rumgeklicke immer sehr mühsam und meiner Meinung ist man auch deutlich schneller unterwegs, wenn man “guten alten Code” schreibt. In diesem Sinne ein Gruss an alle NoCode und LowCode Enthusiasten ;-)

Fazit

Wie geht es bei b-nova nun weiter mit Ballerina? Nun wir werden in einem halbtägigen Hackathon verschiedene Szenarien mit Ballerina ausprobieren, da es gerade für kleine Projekte wirklich spannend ist. Auch spannend für uns ist, wie eine CI/CD Pipeline mit Ballerina aussieht und wie die Applikation unter Last läuft. Wer also mehr über Ballerina erfahren möchte, der wird bestimmt noch das ein- oder andere bei uns erfahren können, also “Stay Tuned :rocket: “!

Das gesamte Projekt findet ihr natürlich in unserem Techup Github.

Stefan Welsch

Stefan Welsch – Manitu, Pionier, Stuntman, Mentor. Als Gründer von b-nova ist Stefan immer auf der Suche nach neuen und vielversprechenden Entwicklungsfeldern. Er ist durch und durch Pragmatiker und schreibt daher auch am liebsten Beiträge die sich möglichst nahe an 'real-world' Szenarien anlehnen.