GraalVM でビルドした Java ネイティブイメージアプリを ACA (Azure Container Apps) 環境で起動する

# GraalVM でビルドした Java ネイティブイメージアプリを ACA (Azure Container Apps) 環境で起動する

## 目的

以前の記事で作成した[GraalVM ビルド Java ネイティブイメージアプリ](https://qiita.com/hiroxpepe/items/2907cdaa769a370dc1b3)を [Azure Container Apps](https://learn.microsoft.com/ja-jp/azure/container-apps/overview) 環境で起動して理解を深めます。  

## 実現すること

[Microsoft Azure Container Apps (ACA)](https://learn.microsoft.com/ja-jp/azure/container-apps/overview) で GraalVM を使用してネイティブイメージビルドした Java アプリを実行してみます。  

※ [GraalVM で Java ネイティブイメージビルドの Hello World!](https://qiita.com/hiroxpepe/items/2907cdaa769a370dc1b3) 記事の続きになります。

## 技術背景

### クラウド環境におけるネイティブイメージビルドの必要性は？  

<details>
<summary>こちらを展開してご覧いただけます。</summary>

### アプリケーションがコンテナ化された

コンテナは、アプリケーションをコンテナ内に包み、移植性やスケーラビリティを高めるための仮想化技術です。コンテナは、仮想マシンよりも軽量で、より素早く起動し、より高速なパフォーマンスを提供するため、多くの企業がコンテナを使用してアプリケーションを展開しています。

しかし、コンテナ内のアプリケーションは、通常はネイティブマシンコードにコンパイルされていません。そのため、アプリケーションの実行には、ネイティブイメージビルドによって生成されたバイナリが必要になります。特に、コンテナを使ったデプロイメントにおいては、コンテナの起動時間を短くするために、アプリケーションの起動時間を短縮するための最適化が求められます。ネイティブイメージビルドによって、アプリケーションの起動時間を短縮し、コンテナの起動時間を短縮することができます。

また、クラウド上でのアプリケーションのデプロイメントにおいては、リソース効率を最大化することが求められます。ネイティブイメージビルドによって生成されたバイナリは、通常のJava アプリケーションに比べてメモリ使用量が少なく、処理速度が高速なため、クラウド上でのアプリケーションのデプロイメントにおいてリソース効率を高めることができます。

</details>

### Microsoft Azure Container Apps (ACA) とは？  

<details>
<summary>こちらを展開してご覧いただけます。</summary>

### [Microsoft Azure Container Apps (ACA)](https://learn.microsoft.com/ja-jp/azure/container-apps/overview)

Azure 上で実行されるコンテナアプリケーションのプラットフォームです。  

ACA は、Docker コンテナと Kubernetes クラスタの環境をサポートしており、開発者は Dockerイメージを作成して、Kubernetes リソースとしてデプロイすることができます。また、カスタムドメイン名をサポートしているため、自社のブランドを維持することができます。  

ACA の主なメリットは以下のとおりです。  

#### 簡単なセットアップとデプロイ

ACA は、簡単なユーザーインターフェイスを備えており、数回のクリックでコンテナアプリケーションをセットアップしてデプロイすることができます。

#### スケーラビリティ

ACA は、自動的にスケーリングを行うことができます。アプリケーションに必要なリソースが増加した場合、ACA は自動的に必要なリソースを追加して処理を分散します。

#### セキュリティ

ACA は、コンテナアプリケーションを実行する際に必要なセキュリティ機能を提供しています。例えば Azure Active Directory との統合や、コンテナレベルでのアクセス制御などがあります。

#### コスト効率

ACA は、必要なときに必要なリソースを追加するため、無駄なリソースの使用を最小限に抑えることができます。また、必要なリソースに対してのみ支払いを行うため、コスト効率的に利用することができます。

</details>

## 開発環境

+ Windows 11 Home 22H2 を使用しています。  
+ [WSL](https://learn.microsoft.com/ja-jp/windows/wsl/about) の Ubuntu を操作していきますので macOS の方も参考にして頂けます。  

<details>

WSL (Microsoft Store アプリ版)

```sh
> wsl --version
WSL バージョン: 1.0.3.0
カーネル バージョン: 5.15.79.1
WSLg バージョン: 1.0.47
```

Ubuntu

```sh
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.1 LTS
Release:        22.04
```

JDK (GraalVM) [※ Java JDK の導入と Hello World!](https://qiita.com/hiroxpepe/items/8650664e466906a57996)

```sh
$ java -version
openjdk version "17.0.6" 2023-01-17
OpenJDK Runtime Environment GraalVM CE 22.3.1 (build 17.0.6+10-jvmci-22.3-b13)
OpenJDK 64-Bit Server VM GraalVM CE 22.3.1 (build 17.0.6+10-jvmci-22.3-b13, mixed mode, sharing)
```

Maven (GraalVM) [※ Maven の導入と Hello World!](https://qiita.com/hiroxpepe/items/40f7f199b9be7f9e3b5b)

```sh
$ mvn -version
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 17.0.6, vendor: GraalVM Community, runtime: /usr/lib/jvm/graalvm-ce-java17-linux-amd64
```

Docker Desktop

```sh
Version 4.16.3 (96739)
```

```sh
$ docker --version
Docker version 20.10.22, build 3a2c30b
```

```sh
$ docker-compose --version
Docker Compose version v2.15.1
```

</details>

※ この記事では基本的に Ubuntu のターミナルで操作を行います。

## "Hello World" を表示する手順

### Java ネイティブイメージビルド アプリの作成

※ [GraalVM で Java ネイティブイメージビルドの Hello World!](https://qiita.com/hiroxpepe/items/2907cdaa769a370dc1b3)  
※ 以前の記事の続きになります。  

### プロジェクトフォルダに移動

※ ~/tmp/hello-graalvm をプロジェクトフォルダとします。

```sh
$ cd ~/tmp/hello-graalvm
```

### Java クラスの修正  

※ WEBブラウザに表示するには、最低限以下のように修正する必要がありました。  

```sh
$ vim HelloWorld.java
```

ファイルの内容

```java:HelloWorld.java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Content-type: text/html\n\nHello World!");
    }
}
```

GraalVM ビルド

```sh
$ javac HelloWorld.java
$ native-image HelloWorld
```

### Docker イメージビルド

Dockerfile 作成

```sh
$ vim Dockerfile
```

ファイルの内容  
※ 絶対に真似しないでください。🙇‍♂️
※ 実行ファイルの標準出力を素のCGIとしてWEBブラウザに出力するというのは現代では全く必要ありません！

```sh
FROM ubuntu:22.04

# updates packages and installs httpd.
RUN apt update \
    && apt install -y apache2 \
    && apt clean \
    && rm -rf /var/lib/apt/lists/*

# enable the cgi module in httpd.
RUN a2enmod cgi

# creates a directory for the cgi.
RUN mkdir -p /usr/lib/cgi-bin/app

# copies the local app to the container.
COPY helloworld /usr/lib/cgi-bin/app/helloworld

# sets permissions for the cgi.
RUN chmod +x /usr/lib/cgi-bin/app/helloworld

# sets a script alias directive in httpd config file.
RUN sed -i '/ScriptAlias \/cgi-bin\/ \/usr\/lib\/cgi-bin\//d' /etc/apache2/conf-available/serve-cgi-bin.conf && \
    echo "ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/" >> /etc/apache2/conf-available/serve-cgi-bin.conf && \
    a2enconf serve-cgi-bin

# sets the server name directive in httpd config file.
RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf

# expose port 80 for web.
EXPOSE 80

# starts httpd.
CMD ["apachectl", "-D", "FOREGROUND"]
```

<details>
<summary>処理の説明を表示します。</summary>

#### Dockerfile の概要

Ubuntu 22.04 の公式 Docker イメージをベースにしています。  

その後、apt コマンドを使用して、Apache HTTP サーバーをインストールし、CGI モジュールを有効にします。  

また、CGIアプリケーション用のディレクトリを作成し、そこに helloworld という名前のネイティブイメージアプリケーションをコピーします。  

その後、CGI スクリプトのパーミッションを変更し、Apache の設定ファイルを編集して CGI スクリプトが実行できるようにします。  

最後に、コンテナ内でポート 80 を公開し、Apache HTTP サーバーを起動します。

このイメージを実行することで、Apache HTTP サーバーが起動し、ネイティブイメージアプリケーションが CGI アプリケーションが実行され、ブラウザなどのHTTPクライアントからアクセスできるようになります。

</details>

ビルド  
※ ローカルの Docker 環境に app-hello-native という名前の Docker イメージを作成します。

```sh
$ docker build -t app-hello-native .
```

確認

```sh
$ docker images | grep app-hello-native
app-hello-native   latest   fc9c97e95a6b   3 minutes ago   210MB
```

ローカルで Docker イメージを起動して確認

```sh
$ docker run --name app-local -p 80:80 app-hello-native
```

ローカルから curl コマンドでリクエスト

```
# curl http://localhost/cgi-bin/app/helloworld
*   Trying 127.0.0.1:80...
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET /cgi-bin/app/helloworld HTTP/1.1
> Host: localhost
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Fri, 24 Feb 2023 10:08:39 GMT
< Server: Apache/2.4.52 (Ubuntu)
< Content-Length: 13
< Content-Type: text/html
<
Hello World!
* Connection #0 to host localhost left intact
```

### Azure のアカウントを取得

公式 [Azure の無料アカウントを使ってクラウドで構築](https://azure.microsoft.com/ja-jp/free/)

### Azure CLI でサインイン

[Azure CLI をインストールする手順](https://qiita.com/hiroxpepe/items/20aeb54a676f190d5c96) を参照してください。

```sh
$ az login
```

※ 最新バージョンに更新する場合

```sh
$ az upgrade
```

### Azure 環境

#### リソースグループ

リソースグループを作成

```sh:Microsoft.Resources/resourceGroups
$ az group create \
    --name rg-hello \
    --location japaneast
```

リソースグループ一覧表示

```sh
$ az group list
```

※ リソースグループを削除する場合

```sh
$ az group delete -n <group>
```

#### コンテナ レジストリ

コンテナ レジストリを作成  
※ --sku Free では作成できません。  
※ --name <name> はパブリックで一意の値が求められます。

```sh:Microsoft.ContainerRegistry/registries
$ az acr create \
    --resource-group rg-hello \
    --name cr20230212 \
    --sku Basic
```

コンテナ レジストリ一覧表示

```sh
$ az acr list
```

コンテナ レジストリログイン資格情報を表示

```sh
$ az acr update -n cr20230212 --admin-enabled true
$ az acr credential show \
    --resource-group rg-hello \
    --name cr20230212
```

```json
{
  "passwords": [
    {
      "name": "password",
      "value": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    },
    {
      "name": "password2",
      "value": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    }
  ],
  "username": "cr20230212"
}
```

※ コンテナ レジストリを削除する場合

```sh
$ az acr delete --n <acr-name>
```

コンテナ レジストリにログイン

```sh
$ az acr login --name cr20230212
```

Docker イメージをコンテナ レジストリにプュシュ

```sh
$ docker tag app-hello-native cr20230212.azurecr.io/app-hello-native:latest
$ docker push cr20230212.azurecr.io/app-hello-native:latest
```

コンテナ レジストリのイメージを確認

```sh
$ az acr repository list --name cr20230212 --output table
Result
---------------------
app-hello-native
```

ローカルの Docker イメージを Azure コンテナ レジストリにプュシュすることが出来ました。

#### コンテナ アプリ拡張機能

※ 初回のみ

<details>
<summary>手順を表示する</summary>

#### 拡張機能

拡張機能をインストール

```sh
$ az extension add --name containerapp --upgrade
```

Microsoft.App 名前空間を登録

```sh
$ az provider register --namespace Microsoft.App
```

Microsoft.OperationalInsights プロバイダーを登録

```sh
$ az provider register --namespace Microsoft.OperationalInsights
```

</details>

#### コンテナ アプリ環境

コンテナ アプリ環境の作成  
※ Kubernetes 基盤の環境だと思われるので少し時間が掛かるみたいです。

```sh:Microsoft.App/managedEnvironments
$ az containerapp env create \
    --resource-group rg-hello \
    --name cae-hello \
    --location japaneast
```

コンテナ アプリ環境の一覧表示

```sh
$ az containerapp env list
```

※ コンテナ アプリ環境を削除する場合

```sh
$ az containerapp env delete -n <env-name> -g <group>
```

#### コンテナ アプリ

コンテナ アプリの作成とデプロイ

```sh:Microsoft.App/containerApps
$ az containerapp create \
    --resource-group rg-hello \
    --environment cae-hello \
    --name ca-hello-native \
    --image cr20230212.azurecr.io/app-hello-native:latest \
    --target-port 80 \
    --ingress 'external' \
    --registry-server cr20230212.azurecr.io \
    --registry-username cr20230212 \
    --registry-password XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \
    --min-replicas 1
```

コンテナ アプリが作成されました。

```sh
Container app created. Access your app at https://ca-hello-native.ashywater-1b561d5b.japaneast.azurecontainerapps.io/
```

※ コンテナ アプリを削除する場合

```sh
$ az containerapp delete -n <name> -g <group>
```

### 確認

WEBブラウザでアクセス

```sh
https://ca-hello-native.ashywater-1b561d5b.japaneast.azurecontainerapps.io/cgi-bin/app/helloworld
Hello World!
```

※ WEBブラウザに "Hello World!" と表示されました。  

curl コマンドで確認

```
$ curl -v https://ca-hello-native.ashywater-1b561d5b.japaneast.azurecontainerapps.io/cgi-bin/app/helloworld

*   Trying 20.78.254.33:443...
* Connected to ca-hello-native.ashywater-1b561d5b.japaneast.azurecontainerapps.io (20.78.254.33) 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=ashywater-1b561d5b.japaneast.azurecontainerapps.io
*  start date: Feb 22 08:56:07 2023 GMT
*  expire date: Feb 17 08:56:07 2024 GMT
*  subjectAltName: host "ca-hello-native.ashywater-1b561d5b.japaneast.azurecontainerapps.io" matched cert's "*.ashywater-1b561d5b.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 0x564e72925e80)
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> GET /cgi-bin/app/helloworld HTTP/2
> Host: ca-hello-native.ashywater-1b561d5b.japaneast.azurecontainerapps.io
> user-agent: curl/7.81.0
> accept: */*
>
* 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
< date: Fri, 24 Feb 2023 10:30:26 GMT
< server: Apache/2.4.52 (Ubuntu)
< content-length: 13
< content-type: text/html
<
Hello World!
* Connection #0 to host ca-hello-native.ashywater-1b561d5b.japaneast.azurecontainerapps.io left intact
```

ターミナルに "Hello World!" と表示することが出来ました。  

※ Azure が SSL/TLSと HTTP/2プロトコルに自動的に対応してくれます。  

## まとめ

+ Azure Container Apps (ACA) 環境にて GraalVM でビルドした Java のネイティブイメージアプリを実行することが出来ました。  

## 個人的見解

+ GraalVM ネイティブイメージアプリをコンテナ化する場合、ベースイメージによってはライブラリなどの依存関連の問題で、正常に Docker イメージにビルド出来ないエラーに遭遇しました。
+ 今回の例では試行錯誤を繰り返した後、結局 GraalVM でネイティブイメージビルドしたローカルの Ubuntu 22.04 と同じベースイメージを使用しました。
+ 改めて考えたら、**ネイティブイメージビルド**という仕組み的に当たり前だと思えたのですが、実際に稼働させるコンテナを作成する場合、そのコンテナと互換性のある専用のビルド環境が必要となる可能性があると思いました。(※ 公式の Dockerfile のサンプルを見たらどうやらそうではないらしい)

## 参考

+ [GraalVM Java Reference](https://www.graalvm.org/latest/reference-manual/java/)