Spring WebFlux Server-Sent Events WEBサービスを ACA (Azure Container Apps) 環境で起動する
目的
以前の記事で作成したSpring Flux Server-Sent Events WEBサービスを Azure Container Apps 環境で起動して理解を深めます。
実現すること
Microsoft Azure Container Apps (ACA) で Spring WebFlux アプリをビルド・起動させます。
そのWEBサービスに curl コマンドからアクセスして Server-Sent Events の動作を確認します。
※ Spring WebFlux WEBサービスで Server-Sent Events を使用する 記事の続きになります。
技術背景
Spring WebFlux とは?
こちらを展開してご覧いただけます。
Spring WebFlux
非同期・反応的な Webアプリケーションを開発するための Spring Framework のモジュールです。
Spring WebFlux の主な特徴とメリットは以下のとおりです。
非同期・反応的なリクエスト処理
Spring WebFlux は、リアクティブストリームと呼ばれる仕組みを使用して、リクエストを非同期で処理することができます。これにより、少ないリソースで高いスループットを実現することができます。
WebFlux サーバー
Spring WebFlux には、Netty をベースにした WebFlux サーバーが含まれています。このサーバーは、高いスループットと低いレイテンシーを実現することができます。
Reactive Streams API のサポート
Spring WebFlux は、Reactive Streams API と呼ばれる仕様に準拠しており、他の Reactive Streams API を使用するライブラリと統合することができます。これにより、より柔軟で拡張性の高いアプリケーションを開発することができます。
Server-Sent Events (SSE) とは?
こちらを展開してご覧いただけます。
Server-Sent Events
Server-Sent Events (SSE) は、サーバーからの単方向のリアルタイム通信に特化した技術であり、サーバーがクライアントに対してリアルタイムに更新情報をプッシュすることができます。 長時間接続を維持することで、クライアントがサーバーからの更新情報をリアルタイムで受け取ることができます。
SSE は、リアルタイムの情報配信が必要なウェブアプリケーションに使用されます。例えば、以下のような状況で使用されることがあります。
ソーシャルメディアのリアルタイム更新
ソーシャルメディアのようなウェブアプリケーションでは、ユーザーが投稿した新しい情報や、フォローしている人々の活動情報などをリアルタイムに更新する必要があります。SSE を使用することで、サーバーが新しい情報を受信するたびに、クライアントにその情報をプッシュすることができます。
ストック情報のリアルタイム更新
株式市場のような業界では、株価の変動などのリアルタイム情報が必要です。SSE を使用することで、株価や取引の更新情報をリアルタイムに配信することができます。
オンラインゲームのリアルタイム更新
オンラインゲームでは、プレイヤーのアクションや他のプレイヤーの行動など、リアルタイムの情報配信が必要です。SSE を使用することで、プレイヤーにリアルタイムな情報を配信することができます。
これらは一例であり、リアルタイムな情報配信が必要なさまざまな種類のウェブアプリケーションで SSE が使用されることがあります。
Microsoft Azure Container Apps (ACA) とは?
こちらを展開してご覧いただけます。
Microsoft Azure Container Apps (ACA)
Azure 上で実行されるコンテナアプリケーションのプラットフォームです。
ACA は、Docker コンテナと Kubernetes クラスタの環境をサポートしており、開発者は Dockerイメージを作成して、Kubernetes リソースとしてデプロイすることができます。また、カスタムドメイン名をサポートしているため、自社のブランドを維持することができます。
ACA の主なメリットは以下のとおりです。
簡単なセットアップとデプロイ
ACA は、簡単なユーザーインターフェイスを備えており、数回のクリックでコンテナアプリケーションをセットアップしてデプロイすることができます。
スケーラビリティ
ACA は、自動的にスケーリングを行うことができます。アプリケーションに必要なリソースが増加した場合、ACA は自動的に必要なリソースを追加して処理を分散します。
セキュリティ
ACA は、コンテナアプリケーションを実行する際に必要なセキュリティ機能を提供しています。例えば Azure Active Directory との統合や、コンテナレベルでのアクセス制御などがあります。
コスト効率
ACA は、必要なときに必要なリソースを追加するため、無駄なリソースの使用を最小限に抑えることができます。また、必要なリソースに対してのみ支払いを行うため、コスト効率的に利用することができます。
開発環境
- Windows 11 Home 22H2 を使用しています。
- WSL の Ubuntu を操作していきますので macOS の方も参考にして頂けます。
WSL (Microsoft Store アプリ版)
> wsl --version
WSL バージョン: 1.0.3.0
カーネル バージョン: 5.15.79.1
WSLg バージョン: 1.0.47
Ubuntu
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04.1 LTS
Release: 22.04
Java JDK ※ 最小構成 Java JDK の導入と Hello World!
$ java -version
openjdk version "11.0.17" 2022-10-18
OpenJDK Runtime Environment (build 11.0.17+8-post-Ubuntu-1ubuntu222.04)
OpenJDK 64-Bit Server VM (build 11.0.17+8-post-Ubuntu-1ubuntu222.04, mixed mode, sharing)
Maven ※ 最小構成 Maven の導入と Hello World!
$ mvn -version
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 11.0.17, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Docker Desktop
Version 4.16.3 (96739)
$ docker --version
Docker version 20.10.22, build 3a2c30b
$ docker-compose --version
Docker Compose version v2.15.1
Spring WebFlux WEBサービスの作成
プロジェクトフォルダに移動
※ ~/tmp/async-spring-webflux をプロジェクトフォルダとします。
$ cd ~/tmp/async-spring-webflux
Java アプリビルド
ビルド
※ target/app.jar という jar ファイル形式の Java WEBアプリが作成されます。
$ mvn clean install
Docker イメージビルド
Dockerfile 作成
$ vim Dockerfile
ファイルの内容
FROM adoptopenjdk/openjdk11:jdk-11.0.11_9-alpine-slim
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"]
ビルド
※ ローカルの Docker 環境に app-hello-spring-webflux という名前の Docker イメージを作成します。
$ docker build -t app-hello-spring-webflux .
確認
$ docker images | grep app-hello-spring-webflux
app-hello-spring-webflux latest 5599bfe55b7d 8 seconds ago 283MB
ローカルで Docker イメージを起動して確認
$ docker run --name app-local -p 8080:8080 app-hello-spring-webflux
ログ (※ ローカル Docker 環境でコンテナ起動)
2023-02-22 11:36:46.270 [INFO ] [main] org.springframework.boot.web.embedded.netty.NettyWebServer.start:111 - Netty started on port 8080
2023-02-22 11:36:46.283 [INFO ] [main] org.springframework.boot.StartupInfoLogger.logStarted:61 - Started SpringbootApplication in 2.228 seconds (JVM running for 2.934)
2023-02-22 11:37:02.249 [INFO ] [parallel-1] com.example.springwebflux.SpringbootApplication.lambda$getFluxWithSSE$4:111 - Sending message: {message=Hello Flux 1!}
2023-02-22 11:37:04.264 [INFO ] [parallel-2] com.example.springwebflux.SpringbootApplication.lambda$getFluxWithSSE$4:111 - Sending message: {message=Hello Flux 2!}
2023-02-22 11:37:06.269 [INFO ] [parallel-3] com.example.springwebflux.SpringbootApplication.lambda$getFluxWithSSE$4:111 - Sending message: {message=Hello Flux 3!}
2023-02-22 11:37:08.275 [INFO ] [parallel-4] com.example.springwebflux.SpringbootApplication.lambda$getFluxWithSSE$4:111 - Sending message: {message=Hello Flux 4!}
2023-02-22 11:37:10.281 [INFO ] [parallel-5] com.example.springwebflux.SpringbootApplication.lambda$getFluxWithSSE$4:111 - Sending message: {message=Hello Flux 5!}
Azure のアカウントを取得
Azure CLI でサインイン
Azure CLI をインストールする手順 を参照してください。
$ az login
※ 最新バージョンに更新する場合
$ az upgrade
Azure 環境
リソースグループ
リソースグループを作成
$ az group create \
--name rg-hello \
--location japaneast
リソースグループ一覧表示
$ az group list
※ リソースグループを削除する場合
$ az group delete -n <group>
コンテナ レジストリ
コンテナ レジストリを作成
※ --sku Free では作成できません。
※ --name はパブリックで一意の値が求められます。
$ az acr create \
--resource-group rg-hello \
--name cr20230212 \
--sku Basic
コンテナ レジストリ一覧表示
$ az acr list
コンテナ レジストリログイン資格情報を表示
$ az acr update -n cr20230212 --admin-enabled true
$ az acr credential show \
--resource-group rg-hello \
--name cr20230212
{
"passwords": [
{
"name": "password",
"value": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
},
{
"name": "password2",
"value": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}
],
"username": "cr20230212"
}
※ コンテナ レジストリを削除する場合
$ az acr delete --n <acr-name>
コンテナ レジストリにログイン
$ az acr login --name cr20230212
Docker イメージをコンテナ レジストリにプュシュ
$ docker tag app-hello-spring-webflux cr20230212.azurecr.io/app-hello-spring-webflux:latest
$ docker push cr20230212.azurecr.io/app-hello-spring-webflux:latest
コンテナ レジストリのイメージを確認
$ az acr repository list --name cr20230212 --output table
Result
---------------------
app-hello-spring-webflux
ローカルの Docker イメージを Azure コンテナ レジストリにプュシュすることが出来ました。
コンテナ アプリ拡張機能
※ 初回のみ
手順を表示する
拡張機能
拡張機能をインストール
$ az extension add --name containerapp --upgrade
Microsoft.App 名前空間を登録
$ az provider register --namespace Microsoft.App
Microsoft.OperationalInsights プロバイダーを登録
$ az provider register --namespace Microsoft.OperationalInsights
コンテナ アプリ環境
コンテナ アプリ環境の作成
※ Kubernetes 基盤の環境だと思われるので少し時間が掛かるみたいです。
$ az containerapp env create \
--resource-group rg-hello \
--name cae-hello \
--location japaneast
コンテナ アプリ環境の一覧表示
$ az containerapp env list
※ コンテナ アプリ環境を削除する場合
$ az containerapp env delete -n <env-name> -g <group>
コンテナ アプリ
コンテナ アプリの作成とデプロイ
$ az containerapp create \
--resource-group rg-hello \
--environment cae-hello \
--name ca-hello-spring-webflux \
--image cr20230212.azurecr.io/app-hello-spring-webflux:latest \
--target-port 8080 \
--ingress 'external' \
--registry-server cr20230212.azurecr.io \
--registry-username cr20230212 \
--registry-password XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \
--min-replicas 1
コンテナ アプリが作成されました。
Container app created. Access your app at https://ca-hello-spring-webflux.delightfulisland-a93c2721.japaneast.azurecontainerapps.io/
※ コンテナ アプリを削除する場合
$ az containerapp delete -n <name> -g <group>
curl コマンドで Server-Sent Events 指定で HTTP リクエスト
※ "Accept: text/event-stream" ヘッダーを指定しています。
$ curl -v https://ca-hello-spring-webflux.delightfulisland-a93c2721.japaneast.azurecontainerapps.io/flux-sse -H 'Accept: text/event-stream'
* Trying 20.78.225.34:443...
* Connected to ca-hello-spring-webflux.delightfulisland-a93c2721.japaneast.azurecontainerapps.io (20.78.225.34) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* CAfile: /etc/ssl/certs/ca-certificates.crt
* CApath: /etc/ssl/certs
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS header, Finished (20):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS header, Finished (20):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-CHACHA20-POLY1305
* ALPN, server accepted to use h2
* Server certificate:
* subject: C=US; ST=WA; L=Redmond; O=Microsoft Corporation; CN=delightfulisland-a93c2721.japaneast.azurecontainerapps.io
* start date: Feb 20 08:56:06 2023 GMT
* expire date: Feb 15 08:56:06 2024 GMT
* subjectAltName: host "ca-hello-spring-webflux.delightfulisland-a93c2721.japaneast.azurecontainerapps.io" matched cert's "*.delightfulisland-a93c2721.japaneast.azurecontainerapps.io"
* issuer: C=US; O=Microsoft Corporation; CN=Microsoft Azure TLS Issuing CA 06
* SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* Using Stream ID: 1 (easy handle 0x561ef45d0e80)
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> GET /flux-sse HTTP/2
> Host: ca-hello-spring-webflux.delightfulisland-a93c2721.japaneast.azurecontainerapps.io
> user-agent: curl/7.81.0
> accept: text/event-stream
>
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
< HTTP/2 200
< vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers
< content-type: text/event-stream;charset=UTF-8
< date: Wed, 22 Feb 2023 11:57:46 GMT
<
* TLSv1.2 (IN), TLS header, Supplemental data (23):
data:{"message":"Hello Flux 1!"}
* TLSv1.2 (IN), TLS header, Supplemental data (23):
data:{"message":"Hello Flux 2!"}
* TLSv1.2 (IN), TLS header, Supplemental data (23):
data:{"message":"Hello Flux 3!"}
* TLSv1.2 (IN), TLS header, Supplemental data (23):
data:{"message":"Hello Flux 4!"}
* TLSv1.2 (IN), TLS header, Supplemental data (23):
data:{"message":"Hello Flux 5!"}
* TLSv1.2 (IN), TLS header, Supplemental data (23):
event:end
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Connection #0 to host ca-hello-spring-webflux.delightfulisland-a93c2721.japaneast.azurecontainerapps.io left intact
※ Azure 側が SSL/TLS対応と HTTP/2プロトコルを使用してくれます。
※ クライアント(curl)側にはデータは一つづつリアルタイムで取得されます。
ログファイル
※ Azure Container Apps コンテナ アプリの Spring Flux アプリのログを確認します。
コンテナ アプリに接続
$ az containerapp exec -n ca-hello-spring-webflux -g rg-hello
INFO: Connecting to the container 'ca-hello-spring-webflux'...
Use ctrl + D to exit.
INFO: Successfully connected to container: 'ca-hello-spring-webflux'.
Spring WebFlux WEBアプリ コンテナに接続後
# pwd
/app
# ls -lah
total 21M
drwxr-xr-x 1 root root 4.0K Feb 22 10:26 .
drwxr-xr-x 1 root root 4.0K Feb 22 10:26 ..
-rw-r--r-- 1 root root 21.3M Feb 22 06:17 app.jar
drwxr-xr-x 2 root root 4.0K Feb 22 10:26 log
ログディレクトリに移動して確認
# cd log
ls -lah
total 12K
drwxr-xr-x 2 root root 4.0K Feb 22 10:26 .
drwxr-xr-x 1 root root 4.0K Feb 22 10:26 ..
-rw-r--r-- 1 root root 2.4K Feb 22 10:38 app.log
# head -n 100 app.log
ACA 上の Spring WebFlux アプリのログ
2023-02-22 11:56:38.588 [INFO ] [main] org.springframework.boot.web.embedded.netty.NettyWebServer.start:111 - Netty started on port 8080
2023-02-22 11:56:38.681 [INFO ] [main] org.springframework.boot.StartupInfoLogger.logStarted:61 - Started SpringbootApplication in 10.609 seconds (JVM running for 12.782)
2023-02-22 11:57:46.583 [INFO ] [parallel-1] com.example.springwebflux.SpringbootApplication.lambda$getFluxWithSSE$4:111 - Sending message: {message=Hello Flux 1!}
2023-02-22 11:57:48.596 [INFO ] [parallel-1] com.example.springwebflux.SpringbootApplication.lambda$getFluxWithSSE$4:111 - Sending message: {message=Hello Flux 2!}
2023-02-22 11:57:50.597 [INFO ] [parallel-1] com.example.springwebflux.SpringbootApplication.lambda$getFluxWithSSE$4:111 - Sending message: {message=Hello Flux 3!}
2023-02-22 11:57:52.599 [INFO ] [parallel-1] com.example.springwebflux.SpringbootApplication.lambda$getFluxWithSSE$4:111 - Sending message: {message=Hello Flux 4!}
2023-02-22 11:57:54.600 [INFO ] [parallel-1] com.example.springwebflux.SpringbootApplication.lambda$getFluxWithSSE$4:111 - Sending message: {message=Hello Flux 5!}
※ Spring WebFlux は2秒ごとに並列処理のスレッドで Flux を処理しています。
※ ACA の中ではローカルの Docker 環境でのマルチスレッド動作と異なり、Flux が parallel-1 スレッドで全て処理されています。
ローカルでも確認
※ コンテナからローカルで実行して確認してみます。
Spring WebFlux WEBアプリ コンテナに curl をインストール
# apk update
# apk add curl
# curl --version
curl 7.79.1 (x86_64-alpine-linux-musl) libcurl/7.79.1 OpenSSL/1.1.1k zlib/1.2.11 brotli/1.0.9 nghttp2/1.43.0
ローカルから curl コマンドでリクエスト
# curl http://localhost:8080/flux-sse -v
* Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /flux-sse HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< transfer-encoding: chunked
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< Content-Type: text/event-stream;charset=UTF-8
<
data:{"message":"Hello Flux 1!"}
data:{"message":"Hello Flux 2!"}
data:{"message":"Hello Flux 3!"}
data:{"message":"Hello Flux 4!"}
data:{"message":"Hello Flux 5!"}
event:end
* Connection #0 to host localhost left intact
ログを確認
# cd log
# head -n 100 app.log
ログ
2023-02-22 23:22:05.820 [INFO ] [main] org.springframework.boot.web.embedded.netty.NettyWebServer.start:111 - Netty started on port 8080
2023-02-22 23:22:05.837 [INFO ] [main] org.springframework.boot.StartupInfoLogger.logStarted:61 - Started SpringbootApplication in 10.298 seconds (JVM running for 12.407)
2023-02-22 23:29:16.829 [INFO ] [parallel-1] com.example.springwebflux.SpringbootApplication.lambda$getFluxWithSSE$4:111 - Sending message: {message=Hello Flux 1!}
2023-02-22 23:29:18.838 [INFO ] [parallel-1] com.example.springwebflux.SpringbootApplication.lambda$getFluxWithSSE$4:111 - Sending message: {message=Hello Flux 2!}
2023-02-22 23:29:20.840 [INFO ] [parallel-1] com.example.springwebflux.SpringbootApplication.lambda$getFluxWithSSE$4:111 - Sending message: {message=Hello Flux 3!}
2023-02-22 23:29:22.842 [INFO ] [parallel-1] com.example.springwebflux.SpringbootApplication.lambda$getFluxWithSSE$4:111 - Sending message: {message=Hello Flux 4!}
2023-02-22 23:29:24.843 [INFO ] [parallel-1] com.example.springwebflux.SpringbootApplication.lambda$getFluxWithSSE$4:111 - Sending message: {message=Hello Flux 5!}
ローカルでリクエストしても全ての Flux が parallel-1 スレッドで処理されています。
※ JVMコンテナのチューニングが必要と思われます。
まとめ
- Azure Container Apps (ACA) にデプロイした Spring WebFlux アプリの Server-Sent Events リクエストを curl コマンドから呼び出すことが出来ました。
- しかし、デフォルトの状態ではローカル Docker 環境の Flux を処理するスレッドと、ACA 環境で Flux を処理するスレッドの構成が違うことが分かりました。
- ローカルの Docker 環境と ACA 環境で同一の Spring Flux WEBアプリの Docker イメージを起動させているのですが、コンテナ実行環境 (※ ここでは Kubernetes) のリソースに依存・影響されるようです。
個人的見解
- Microsoft Azure Container Apps (ACA) はコンテナ化されたアプリをデプロイして運用する環境として非常に優れていると感じました。
- 本来 Azure は、WEBのポータルサイトから GUI のコンソールで操作すると思うのですが、一方、おそらくほとんどの業務がターミナルから一貫したインタフェースで実行可能だと思われます。そして一番重要な要素だと感じたのが 公式にドキュメントが完璧に揃っている ということでした。
- 昔 "Windows Azure=拡張性・可用性の高いクラウド専用OS" と呼ばれてたものが本当に実現したなと思いました。