キーポイント
- Full-stack developers no longer select a popular “stack.” Instead, they look at the best possible technology to implement their requirements at each layer in the “full stack.”
- Frontends consume one or more backend APIs that are typically tailor-made to match the frontend UX. While RESTful services are the de facto standard for backend APIs, GraphQL and WebSocket APIs are emerging.
- Ballerina is an open-source cloud native programming language that has inbuilt support for networking primitives, including listeners, services, remote/resource methods, and clients.
- We first look at Ballerina’s intuitive syntax for writing REST APIs. Then at authentication, authorization, OpenAPI tool, observability, SQL/NoSQL client libraries, and key language features.
- At the end of this article, you will have a good understanding of why Ballerina is a prominent candidate for writing your next backend API.
開発の背景
最も単純な形式のWebアプリは、3つの層で構成されています。すなわち、クライアント再度(フロントエンド)、サーバサイド(バックエンド)、そして永続化層(データベース)です。Webアプリケーションの3層すべてを開発するプラクティスは、フルスタック開発(full-stack development)と呼ばれます。
- フロントエンド開発にはHTML、CSS、通常のJavaScript(vanilla JavaScript)、あるいはJavaScriptフレームワークやライブラリ(JQuery、React.jsなど)が関わっています。フロントエンド開発で利用可能なツールやフレームワーク、ライブラリの数は、この数年間で急激に増加しました。それに伴ってフロントエンド開発そのものが、現在では非常に幅広いテーマになっています。
- バックエンド開発には、一般的にはサーバサイドスクリプト言語(PHPやJSP)、あるいはフロントエンドの使用するサービスが関与します。シングルページ・アプリケーション(SPA)の登場により、開発者は従来的なサーバサイドスクリプト言語を離れて、API(REST、GraphQL、WebSocket)をバックエンドとして採用するようになりました。
- 永続化層にはひとつ以上のSQLあるいはNoSQLデータベースが含まれます。しかし、"ビッグデータ"の出現によって、データストレージは従来のデータストアに留まらないものになっています。
一般的なスタック
これまでに説明した各層で使用されるテクノロジの中には、他のテクノロジよりも人気のあるものがいくつかありました。3層すべてをカバーするようなテクノロジの組み合わせを、私たちは"スタック"と呼んでいます。私たちはこれまで、何年にもわたって、人気のあるスタックをいくつも見てきました。
- LAMP / LEMP — フロントエンド向けJavaScript。ホスティングとしてLinux、WebサーバとしてApache/Nginx、永続化層としてMySQL、バックエンド(あるいはサーバサイド)言語としてPHPを使用します。このスタックでは、LaravelやSymphonyといったフレームワークが一般的に使用されます。
- MEAN — データ永続化層にMongoDB、バックエンドフレームワークとしてExpress、フロントエンドのJavaScriptフレームワークとしてAngularJS、サーバランタイムとしてNodeJSを使用します。
- MERN — Angularに代えてReactJSを選択することを除けば、MEANスタックと同じです。
- Ruby on Rails — Rubyで記述されたWebアプリケーションフレームワーク。
- Django — フロントエンドにJavaScript、バックエンドにPython Djangoフレームワークを使用し、適切なSQLないしNoSQLデータベース層を選択します。
とは言っても、スタックはそのような単純なものではありません。すべての層において、利用可能な代替テクノロジの数が増え続けているのです。ひとつの層にさまざまなテクノロジを組み合わせて、最終的なプロダクトを組み上げることも多くなっています。
最新のフルスタック開発
フルスタック開発ではSPA(single-page applications)とPWA(progressive web applications)が主流となっていますが、それらの制限に対処するSSR(server-side rendering)などの概念も現れています。これらのWebアプリケーション(フロントエンド)は、エンドユーザに優れた機能を提供する上で、バックエンドAPI(REST、GraphQLなど)と連携することを前提としています。それに伴って、フロントエンドのユーザエクスペリエンス(UX)にバックエンドを整合させるという、BFF(Backend for Frontend)のような概念も現れました。
BFF — Backend for Frontend
企業には、モバイルアプリケーションやWebアプリケーション、その他のサービスやAPI、さらには外部パーティなど、さまざまなパーティによって使用されるサービスを提供するマイクロサービスがいくつも存在します。その一方で、最新のWebアプリケーションは、フロントエンドUXと強く結合して密接に動作するAPIを必要としています。BFFはそのために、フロントエンドとマイクロサービスとのインターフェースとして機能するものです。
フロントエンドでビューを構築するために、BFFは、多数のダウンストリームサービスを呼び出します。使用するダウンストリームAPIのタイプはさまざまです(REST、GraphQL、gRPCなど)。BFFアーキテクチャパターンを詳しく知るには、"Pattern: Backends For Frontends"を読むとよいでしょう。
これまで説明した概念に基づいて、最新のWeb開発についてもう少し議論してみましょう。
フルスタック開発のコンテキストにおけるバックエンド開発
バックエンドAPI開発には2つの意味があります。
- BFFの開発 — BFFはフロントエンドのUXとバックエンドのマイクロサービスの間で、アダプタの役割を果たします。
- マイクロサービスの開発 — フロントエンドから直接的に、あるいは(BFFを介して)間接的に使用される、個々のマイクロサービスを開発します。
フルスタック開発のコンテキストにおいては、フロントエンドから直接起動されるバックエンドAPIのみを考慮することになります。バックエンドAPIはBFFとして記述することも、独立したマイクロサービスとして書くことも可能です。
スタックの選択
最近の開発者は、"一般的だから"という理由のみでスタックを選択するようなことはありません。まずフロントエンドとして、自身が実装したいと思うUI/UXに最も適したテクノロジを選択します。その上で、市場投入までの期間、保守性、開発者の経験など、いくつものファクタを考慮した上で、バックエンドのテクノロジを選ぶのです。
今回の記事では、バックエンド開発の新しい有望な候補であるBallerinaを紹介したいと思います。将来のフルスタック開発過程において、バックエンドテクノロジを評価する際の検討項目になるでしょう。
Ballerinaとは何か?
Ballerinaは、ネットワークサービスの利用、構成、開発を容易にする目的で開発された、オープンソースのクラウドネイティブなプログラミング言語です。今年リリースされた次期メジャーバージョンである"Ballerina Swan Lake"では、型システムの改善、SQLライクな言語組み込みクエリの強化、強化された直感的なサービス宣言など、あらゆる面において大きな改良が加えられています。
Ballerinaの大きな目標は、開発者がクラウドネイティブなテクノロジの統合に時間を取られずに、ビジネスロジックに集中できるようにすることにあります。Ballerinaに組み込まれたネットワークプリミティブを使って、クラウド上で直感的にサービスを定義したり、実行したりすることが、このレボリューションで重要な役割を果たしています。フレキシブルな型システム、データ指向の言語組み込みクエリ、強力かつ直感的な並列処理といったものが、言語に組み込まれた可観測性やトレース機能のサポートと相まって、Ballerinaをクラウド時代における主力言語のひとつにするのです。
フロントエンドから直接機能されるAPIの開発には、いくつかの一般的な選択肢が存在します。
- REST API
- GraphQL API
- WebSocket API
適切なAPIタイプを選択したならば、次のようなファクタについて検討する必要があります。
- セキュアな通信
- 認証
- 承認
- 監視、可観測性、ログ
これらのファクタに加えて、APIの開発では、メンテナンス性や開発者の経験も考慮しなければなりません。ここで挙げたAPIタイプの開発において、Ballerinaがどのようなサポートを提供しているのか、バックエンドAPI開発においてBallerinaが有力な候補である理由は何か、といったことを説明したいと思います。
Ballerinaとネットワークインタラクション
プログラミング言語Ballerinaの大きな目標のひとつは、ネットワークインタラクションの記述を簡単にすることです。そのためにBallerinaは、ネットワークプリミティブを言語に組み込んでいます。他のすべての主流言語がネットワークを単なるI/Oリソースとして扱っているのに対して、Ballerinaはネットワークインタラクションのために、ファーストクラスのサポートを用意しているのです。それを活用する目的で、以下のような優れたコンポーネントが用意されています。
-
リスナ(Listener) — ネットワーク層とサービス層の間のインターフェースとして、HTTP、gRPC、TCP、あるいはWebSocketといったトランスポート基盤を表現します。
-
サービス(Service) — エンドユーザに対して組織の能力を公開するサービスを表現します。HTTP、GraphQL、gRPC、WebSocketなどが代表的な例です。
-
リソースメソッド(Resource Method) — サービス内の機能ユニットを表現します。インベントリを管理する単純なCRUDサービスを例にすれば、インベントリの追加がひとつのリソースメソッドとして、インベントリの削除操作がまた別のリソースメソッドとして、それぞれ表現されます。
-
クライアント(Client) — 外部あるいは内部のサービスの実行を含むようなサービスを記述します。最近のサービスでは、ひとつないし複数のサービスを呼び出すことが一般的になっています。例えば、
1. サービス内からEメールを送信する必要があるかも知れません。そのためには、Eメールクライアントが必要です。2. 同じサービスに、ひとつないし複数の内部gRPCサービスを起動する要件があるかも知れません。その場合は、gRPCクライアントが必要になります。サービスの開発では、外部サービスを起動することも必要になります。そのためにBallerinaには、クライアントと呼ばれるリッチな概念があります。そこでの外部呼出しは、リモートメソッドによって表現されています。Ballerinaの実行時におけるリモートメソッドの起動は、非同期(ノンブロッキングですが、明示的なコールバックやリスナは不要)で行われます。
言語に組み込まれたこれらネットワークプリミティブが、明示的なエラー処理、json/xmlの組み込みサポート、フレキシブルな型システムなどと相まって、直感的でメンテナンス性に優れたネットワークインタラクションの迅速な開発を可能にしています。この結果として、開発者や企業は、これまで以上にイノベーションに注力することができるのです。
BallerinaのサポートするREST APIとGraphQL APIを使うことで、直感的で有意なバックエンドAPIの記述がどのように可能になるのか、これから探ってみましょう。まずは"getting started"ガイドに従って、Ballerinaのインストールとセットアップを行います。
REST APIの開発
BallerinaでREST APIを記述する方法を見てみましょう。
"Hello World!"
Ballerinaで書いた"Hello World" REST APIは次のようなものになります。
import ballerina/http;
service / on new http:Listener(8080) {
resource function get greeting() returns string {
return "Hello!";
}
}
構文の各部分について解読してみましょう。
import ballerina/http;
— ballerina/httpパッケージをインポートします。service /on new http:Listener(8080)
— ポート8080をリッスンするHTTPリスナに、コンテキストパス"/"でサービスを生成します。サービスのタイプは、アタッチされるリスナのタイプによって決定されます。ここではリスナがHTTPリスナなので、HTTPサービスになります。resource function get greeting() returns string
— このHTTPサービス上で実施可能なオペレーションのひとつを表しています。"get"は"リソースアクセサ"です。簡単に言うと、HTTPメソッド(get、post、deletenなど)を表します。"greeting"は関数名です。関数名はパスになります。つまり、リソースパス"/greeting"がこのメソッドによって処理される、ということです。"return string"は、このサービスが文字列を返すという意味です。もっと複雑なオブジェクトを返すことも可能です。return "Hello World!"
; — リソースメソッドの返す戻り値を示しています。"Hello World!"という文字列がそれです。
このBallerina HTTPサービス構文の概要を示したのが、次の図です。
Balleria HTTPサービスの構文、特にクエリやパスパラメータの使用方法、ペイロードデータのバインディングなどの詳細を理解するためには、次の記事を参照してください。
HTTP Deep-Dive with Ballerina: Services
インテグレーション例 — 通貨換算API
次に示すのは、もう少し複雑なREST APIです。このAPIは、baseCurrency、targetCurrency、そして数量を指定することで、通貨の換算結果を返します。最新の為替レートの取得に、外部サービスを使用しています。
import ballerina/log;
import ballerina/http;
configurable int port = 8080;
type ConversionResponse record {
boolean success;
string base;
map<decimal> rates;
};
service / on new http:Listener(port) {
resource function get convert/[string baseCurrency]/to/[string targetCurrency](decimal amount) returns decimal|error {
http:Client exchangeEP = check new ("https://api.exchangerate.host");
ConversionResponse response = check exchangeEP->get(string `/latest?base=${baseCurrency}`);
if !response.success {
return error("Exchange rates couldn't be obtained");
}
decimal?rate = response.rates[targetCurrency];
if rate is () {
return error("Couldn't determine exchange rate for target currency", targetCurrency = targetCurrency);
}
log:printInfo("converting currency", baseCurrency = baseCurrency, targetCurrency = targetCurrency, amount = amount);
return rate * amount;
}
}
先程の"Hello World!"に比べると、今回の例では、もう少し興味深いBallerinaの機能を使用しています。
service / on new http:Listener(port)
— 今回のベースパスは"/"です。ポートは指定可能で、実行時に設定することができます。"configurable int port = 8080
"と定義されているので、portは変更可能(configurable)で、デフォルト値8080を持ちます。このconfigurable変数も、注目すべき機能のひとつです。resource function get convert/[string baseCurrency]/to/[string targetCurrency](decimal amount) returns decimal|error
— 今回のリソースパスは"/convert/{baseCurrency}/to/{targetCurrency}"で、"amount"という名称のクエリ引数が必要です。このリソースメソッドは、10進値(換算値)またはエラー("500 — Internal Server Error"にマップされる)を返します。ConversionResponse response = check dccClient->get(string `/latest?base=${baseCurrency}`)
— 外部の為替レートAPIを呼び出して、その応答をオープンレコード(open record)であるConversionResponseに変換します。この呼び出しは非ブロックで実行されて、応答のペイロードはBallerinaの"Open by Default"の原則を表すオープンレコードに、シームレスに変換されます。
これを実行すると、curlを使った以下のような要求で100米ドルをGBPに換算できるようになります。
curl http://localhost:8080/convert/USD/to/GBP?amount=100
おまけ: ローコード開発
Ballerinaはリークフリー(leak-free、後述)なグラフィックインターフェースを備えています。これによって、ソースコードとローコードビュー(グラフィカルな表現)を同期的に編集することが可能です。次の図は、前述のAPIのローコードビューです。
今回の記事ではBallerinaのローコード開発については詳しく触れませんが、技術者でない人や技術に詳しくない人がコードを理解し、コードを書くには理想的なものです。こちらもぜひ試してみてください。
リークフリー — すべてがコードでプログラム可能であると同時に、コードで記述されるものはすべて視覚的である、ということ。
単純なCRUDサービス
次に示すのはBallerinaで書かれたCRUDサービスの例で、メモリ上に保持しているプロダクトのセットを操作するものです。
import ballerina/http;
import ballerina/log;
import ballerina/uuid;
# Represents a product
public type Product record {|
# Product ID
string id?;
# Name of the product
string name;
# Product description
string description;
# Product price
Price price;
|};
# An enum to represent currencies
public enum Currency {
USD,
LKR,
SGD,
GBP
}
# Represents price
public type Price record {|
# Currency
Currency currency;
# Amount
decimal amount;
|};
# Represents an error
public type Error record {|
# Error code
string code;
# Error message
string message;
|};
# Error response
public type ErrorResponse record {|
# Error
Error 'error;
|};
# Bad request response
public type ValidationError record {|
*http:BadRequest;
# Error response.
ErrorResponse body;
|};
# Represents headers of created response
public type LocationHeader record {|
# Location header. A link to the created product.
string location;
|};
# Product Created response
public type ProductCreated record {|
*http:Created;
# Location header representing a link to the created product.
LocationHeader headers;
|};
# Product updated response
public type ProductUpdated record {|
*http:Ok;
|};
# The product service
service / on new http:Listener(8080) {
private map<Product> products = {};
# List all products
# + return — List of products
resource function get products() returns Product[] {
return self.products.toArray();
}
# Add a new product
#
# + product — Product to be added
# + return — product created response or validation error
resource function post products(@http:Payload Product product) returns ProductCreated|ValidationError {
if product.name.length() == 0 || product.description.length() == 0 {
log:printWarn("Product name or description is not present", product = product);
return <ValidationError>{
body: {
'error: {
code: "INVALID_NAME",
message: "Product name and description are required"
}
}
};
}
if product.price.amount < 0d {
log:printWarn("Product price cannot be negative", product = product);
return <ValidationError>{
body: {
'error: {
code: "INVALID_PRICE",
message: "Product price cannot be negative"
}
}
};
}
log:printDebug("Adding new product", product = product);
product.id = uuid:createType1AsString();
self.products[<string>product.id] = product;
log:printInfo("Added new product", product = product);
string productUrl = string `/products/${<string>product.id}`;
return <ProductCreated>{
headers: {
location: productUrl
}
};
}
# Update a product
#
# + product — Updated product
# + return — A product updated response or an error if product is invalid
resource function put product(@http:Payload Product product) returns ProductUpdated|ValidationError {
if product.id is () || !self.products.hasKey(<string>product.id) {
log:printWarn("Invalid product provided for update", product = product);
return <ValidationError>{
body: {
'error: {
code: "INVALID_PRODUCT",
message: "Invalid product"
}
}
};
}
log:printInfo("Updating product", product = product);
self.products[<string>product.id] = product;
return <ProductUpdated>{};
}
# Deletes a product
#
# + id — Product ID
# + return — Deleted product or a validation error
resource function delete products/[string id]() returns Product|ValidationError {
if !self.products.hasKey(<string>id) {
log:printWarn("Invalid product ID to be deleted", id = id);
return {
body: {
'error: {
code: "INVALID_ID",
message: "Invalud product id"
}
}
};
}
log:printDebug("Deleting product", id = id);
Product removed = self.products.remove(id);
log:printDebug("Deleted product", product = removed);
return removed;
}
}
ほとんどの構文は自明ですが、このサービスには4つのリソースメソッドがあります。
- すべてのプロダクトの一覧 — GET /products
- プロダクトの追加 — POST /products
- プロダクトの更新 — PUT /product
- プロダクトの削除 — DELETE /products/{id}
プロダクト、価格、通貨を表す型がどのように定義されているか、注目してください。次に、目的とするスキーマを実現するために必要な応答の型を定義します。"ProductCreated"はプロダクト追加の応答を、"ValidationError"はバリデーションにおけるエラーを、それぞれ表現します。
# Bad request response
public type ValidationError record {|
*http:BadRequest;
# Error response.
ErrorResponse body;
|};
# Product Created response
public type ProductCreated record {|
*http:Created;
# Location header representing a link to the created product.
LocationHeader headers;
|};
このようなスキーマがあることによって、コードを容易に理解できるようになるのです。リソースパスは何か、どのようなクエリ/パスパラメータが必要なのか、ペイロードは何か、可能性のある戻り型は何か、といったリソースメソッドの概要が、定義を見るだけで明確に把握できるようになります。例えば、
resource function post products(@http:Payload Product product) returns ProductCreated|ValidationError {
}
これはPOST要求で、"/products"(リソースメソッドの名称から分かります)に送信されます。Product型のペイロードが必要で、バリデーションエラー(400)か"HTTP CREATED"応答(201)のいずれかがlocationヘッダとともに返されます。
OpenAPI仕様の生成
Ballerinaでサービスを記述することができれば、単にそのソースファイルを指定するだけで、OpenAPI仕様を生成することが可能になります。指定されたソースを調べることによって、対応するステータスコードとスキーマを備えたOpenAPI仕様を出力します。
詳細については、下記資料の"OpenAPI"のセクションを参考にしてください。
完全なOpenAPI仕様を生成すれば、必要なクライアントを生成することも可能になります。今回のケースであれば、JavaScriptクライアントを生成することで、フロントエンドをバックエンドに簡単に統合することができます。
サービスの保護
次のように、HTTPリスナをHTTPSリスナに変更すれば、サービスを保護することができます。
http:ListenerSecureSocket secureSocket = {
key: {
certFile: "../resource/path/to/public.crt",
keyFile: "../resource/path/to/private.key"
}
};
service /hello on new http:Listener(8080, secureSocket = secureSocket) {
resource function get world() returns string {
return "Hello World!";
}
}
相互SSLを有効にして、さらに高度な構成を行うことも可能です。詳しい情報については、HTTPサービスセキュリテイに関するBallerinaの例を参照してください。
認証
Ballerinaは3つの認証メカニズムを組み込みでサポートしています。
JWT
certファイルを用意するか、あるいは認証サーバのJWKsエンドポイントURLを提供すれば、JWTシグネチャ検証を有効にすることができます。例えば、サービスに次のようなアノテーションを加えるだけで、AsgardeoのようなIDaaS(Identity as a Service)によるサーバの保護が可能になります。
@http:ServiceConfig {
auth: [
{
jwtValidatorConfig: {
signatureConfig: {
jwksConfig: {
url: "https://api.asgardeo.io/t/imeshaorg/oauth2/jwks"
}
}
}
}
]
}
さらに
- サービス全体を保護することも、リソースパスのサブセットのみを保護対象にすることも可能です。
- JWTのイシュア(issuer、発行者)およびオーディエンス(audience)の検証が可能です。
- クレームに基づいた検証(後述します)の実施が可能です。
詳細は"REST API Security section of Ballerina examples"を参照してください。
OAuth2
JWTと同じように、OAuth2を使用してサービスを保護することも可能です。詳細は"Service — OAuth2"の例を参照してください。
ベーシック認証
ベーシック認証では、ファイルとLDAPという、2つのユーザストアが選択できます。方法については、以下の例を参考にしてください。
承認
OAuthとJWTを使うことで、サービス単位またはリソース単位をスコープとする検証が可能になります。どちらの場合でも、独自のスコープキーを指定することができます。デフォルトは"scope"です。
JWTを使用する場合には、ユーザロール(ロールベースのアクセス制御 — RBAC)またはパーミッション(詳細なアクセス制御)を含むカスタムクレームを使用して、個々のオペレーションを承認することが可能です。
import ballerina/http;
public type Product record {|
string id?;
string name;
string description;
|};
Product[] products = [];
@http:ServiceConfig {
auth: [
{
jwtValidatorConfig: {
issuer: "wso2",
audience: "example.com",
scopeKey: "permissions",
signatureConfig: {
jwksConfig: {
url: "https://api.asgardeo.io/t/imeshaorg/oauth2/jwks"
}
}
}
}
]
}
service /products on new http:Listener(8080) {
@http:ResourceConfig {
auth: {
scopes: "product:view"
}
}
resource function get .() returns Product[] {
return products;
}
@http:ResourceConfig {
auth: {
scopes: "product:create"
}
}
resource function post .(@http:Payload Product product) returns error?{
products.push(product);
}
}
上の例で示したように、"/products"サービスでは、到着したJWTがプロダクト一覧のための"product:view"パーミッションと、プロダクト生成の"product:create"パーミッションを持っているかどうかを検証しています。scopeKeyは、検証のために調査するJWTのクレーム名称である"permissions"に設定されます。イシュアとオーディエンスも検証されます。
クライアント
バックエンドAPIの開発において、外部サービスとのコミュニケーションが必要なのは間違いありません。最低でもデータベースクライアントは必要なはずです。DBクライアント、HTTPクライアント、あるいはgRPCクライアントに関しても、Ballerinaは非常にうまくカバーしてくれます。最も重要なのは、Ballerinaのクライアントコールが非ブロックでありながら、コールバックやリスナの追加を必要としないことです。
Ballerinaのクライアントがいかに便利であるか、確かめてみてください。
- 基本的なRESTクライアント
- Bearerトークン認証を使用するRESTクライアント
- OAuthクライアント認証グラントタイプ(Grant Type)を使用するクライアント
- OAuth2 JWT Bearerグラントタイプを使用するクライアント
レジリエンスに関するサポートも充実しています。具体的な内容は、以下の例を参照してください。
GraphQL API
記事を長編にしないために、GraphQL APIの開発には深く立ち入らないことにしますが、GraphQLサービスについても、REST APIと同じレベルのサポートが用意されています。この話題に関しては、以下のリンクを参照してください。
GraphQLによるサービス開発の詳細については、Ballerina Webサイトの"Referenece by Example"セクションで、GraphQLに関する例を参照してください。
WebSocket API
この話題についても、今回は詳しく論じません。WebSocketサービス開発に関する詳細は、Ballerina Webサイトの"Reference by Example"セクションにある、WebSocketおよびWebSocketセキュリテイに関する章を参照してください。
可観測性
可観測性(observability)は、この言語の重要な組み込み機能のひとつです。Ballerinaでは、分散型のトレースやメトリクス監視が最初から実行可能になっています。
トレース
分散トレースはJaegerとChoreoで実現されています。トレースをJaeger(またはChoreo)にパブリッシュするためには、コードにimportを加える必要があります。実行時には、Open Telemetry標準に基いてJaeger(またはChoreo)へのパブリッシュを行います。
ロギング
Ballerinaの提供するロギングフレームワークは、logstashなどのログアナライザを使ったログ分析に理想的なものです。コードを記述する時に、ログ行にキーと値のペアを渡すことができます。
log:printInfo("This is an example log line", key1 = "key1", payload = { id: 123});
上記のログ行の出力は次のようなものになります。
time = 2022-01-26T16:19:38.662+05:30 level = INFO module = "" message = "This is an example log line" key1 = "key1" payload = {"id":123}
メトリクス
リアルタイムメトリクスは、Prometheusとgrafanaを使って監視することができます。Choreoを使ったメトリクスのライブ監視も可能です。
リアルタイムメトリクスの公開と監視を行うには、分散トレースと同じくソースコードにimportを追加して、作成済のgrafanaダッシュボードをインポートする必要があります。
Ballerinaの可観測性機能については、以下のリンクも参照してください。
永続化層
バックエンド開発における次の重要な側面は永続化層(persistence layer)です。Ballerinaには、SQLおよびNoSQLのDBクライアントが豊富に用意されています。
クライアントによるDB呼び出しは非ブロックで実施されます。
SQL
現時点では、以下のクライアントが利用可能です。
さらにBallerinaには、RawTemplateを使用してプリペアードステートメント(prepared statement)を記述する、非常に便利な方法が用意されています。
mysqlClient->execute(`insert into products (product_name, price, currency) values (${product.productName}, ${product.price}, ${product.currency})`);
mysqlClient->execute(`update products set product_name = ${product.productName}, price = ${product.price}, currency=${product.currency} where id=${product?.id}`);
上の例では、"${<variableName>}"がクエリにバインドされた変数を表しています。実行時には上記のコードがプリペアードステートメントとして実行されます。
データバインディングとストリーム
同じように、以下のSELECTクエリを使用すれば、ユーザ定義型のストリームとしてデータをフェッチできます。次のようなProductレコードがあるものとします。
type Product record {|
int id?;
string productName;
float price;
string currency;
|};
productテーブルは次のように定義されています。
CREATE TABLE `products` (
`id` int NOT NULL AUTO_INCREMENT,
`product_name` varchar(255) NOT NULL,
`price` float DEFAULT NULL,
`currency` varchar(5) DEFAULT NULL,
PRIMARY KEY (`id`)
)
この場合、次のように、Productレコードのストリームとしてデータをフェッチすることが可能です。
stream<Product, error?> productStream = mysqlClient->query(`select id, product_name as productName, price, currency from products`);
レコードのフィールド名とフェッチしたカラム名が同じである点に注意してください。
NoSQL
通常のNoSQLによるメリットに加えて、非構造あるいは半構造データを扱う上で、Ballerinaに組み込まれたJSONとオープンレコードタイプが役に立ちます。
注記: Ballerinaのエコシステムは発展途上であるため、完全な機能を持ったORMライブラリはまだありません。
その他の注目機能
JSON/XMLの組み込みサポート
すでに紹介したように、Ballerinaは、JSONやXMLといった一般的なワイヤフォーマット(wire format)を組み込みでサポートしています。これにより、HTTPサービスやDBの例で見たように、ユーザ定義型とJSONをシームレスに変換することが可能です。
構造型システム
Ballerinaは、(JavaやKotlinのような)公称型(nominal typing)ではなく、(GoやTypeScriptのように)構造上の形状に基づいてサブタイプを決定します。これによってユーザ定義型間、あるいはユーザ定義型とJSONの間のシームレスな変換が可能になります。
静的型付け
Ballerinaは静的型付けであるため、信頼性とメンテナンス性の高いコードを記述するための豊富なツールセットが提供されています。さらにBallerinaは、関心のあるものだけを定義すればよいという、"open by default"の原則を採用しています。オープンレコードは、この使い方の一例です。
明示的なエラー処理
エラーは明示的に処理されるべきです。例で見たように、クライアント呼び出しは結果とエラーの共用体を返すので、開発者はエラーの型チェックを行った上で、明示的にエラー処理を行う必要があります。
null安全
Ballerinaはnull安全(null safe)です。
メンテナンス性
これまでに挙げた側面をすべて組み合わせることにより、Ballerinaはネットワークプログラミングに特化した、メンテナンス性と信頼性に優れた言語になっています。
グラフィック表現
前述したようにBallerinaは、非常に優れたリークフリーなローコードである、という一面も持っています。Ballerinaはシーケンス図を使って、ネットワークインタラクションを可視化します。技術者でない人や技術に詳しくない人がプログラムしたり、プログラムを理解したりする上で、これは非常に有効です。
要約
今回の記事では、プログラミング言語BallerinaのバックエンAPI開発サポートについて、簡単ではありますが包括的な概要を提供することを目的としました。REST APIの開発については、特に詳細に取り上げました。安全なサービスを開発する方法、認証や承認の実施方法、OpenAPI仕様の生成についても見てきました。
続いて、GraphQLやWebSocketといったサービスの記述方法を簡単に紹介しました。可観測性機能や永続化層のサポート(SQLおよびNoSQLデータベース)についても説明しました。最後に、注目に値するBallerina言語の機能をいくつか紹介しました。
今回紹介した内容が、プログラミング言語Ballerinaをさらに探求し、バックエンドAPI開発におけるその卓越性を理解する上での一助となれば、と願っています。今回の記事を通じてBallerinaが、ネットワークプリミティブを一級市民として扱う、真のクラウドネイティブ言語であることを実証できたのではないか、と自分では思っています。プログラミング言語Ballerinaについて、もっと詳しく探求してみてください。
今回の記事を読んで頂いて、ありがとうございました。意見や感想を、気軽に寄せてください。質問や疑問は、私あるいはBallerinaコミュニティまでどうぞ。