Dapr と Express と Spring Boot でマイクロサービスを構成する：Azure 編

# Dapr と Express と Spring Boot でマイクロサービスを構成する：Azure 編

こんにちは、@studio_meowtoon です。今回は、Azure Container Apps にて、Dapr と Express と Spring Boot を利用してマイクロサービスを構成する方法を紹介します。
![dapr_on_azure-container-apps.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/b5067f62-a4a9-efd6-f126-efc840347df4.png)

## 目的

分散アプリケーションの為のランタイム [Dapr](https://docs.dapr.io/concepts/overview/) の理解を深めます。  

## 実現すること

Microsoft Azure Container Apps 環境にて、Dapr、Express、Spring Boot で構成する、シンプルなマイクロサービスを実装します。  

こちらの記事の続きになります。  

https://qiita.com/studio_meowtoon/items/0d28fc54b395520d880b

## 開発環境

+ Windows 11 Home 22H2 を使用しています。  

:::note info
[WSL](https://learn.microsoft.com/ja-jp/windows/wsl/about) の Ubuntu を操作していきますので macOS の方も参考にして頂けます。  
::: 

<details>

WSL (Microsoft Store アプリ版) [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/2aaa6579cb8e4fa1d874)

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

Ubuntu [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/2aaa6579cb8e4fa1d874)

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

npm [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/111f556d8fb11a76430a)

```sh
$ node -v
v19.8.1
```

```sh
$ npm -v
9.5.1
```

Java JDK [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/4d11e94a2389758759cd)

```sh
$ java -version
openjdk version "11.0.18" 2023-01-17
OpenJDK Runtime Environment (build 11.0.18+10-post-Ubuntu-0ubuntu122.04)
OpenJDK 64-Bit Server VM (build 11.0.18+10-post-Ubuntu-0ubuntu122.04, mixed mode, sharing)
```

Maven [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/f4cdc4039158153365fb)

```sh
$ mvn -version
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 11.0.18, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
```

Docker [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/3bceee25ac67cc73ebbe)

```sh
$ docker --version
Docker version 23.0.1, build a5ee5b1
```

Dapr [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/a00f2802a171959b5728)

```sh
$ dapr --version
CLI version: 1.11.0
Runtime version: 1.11.2
```

Azure CLI [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/9721041f9e6e3e987cf0)

```sh
$ az --version
azure-cli                         2.45.0
core                              2.45.0
telemetry                          1.0.8
```

</details>

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

## 作成する Web アプリケーションの仕様

|No|エンドポイント|HTTPメソッド|MIME タイプ|
|:--|:--|:--|:--|
|1|/api/data|GET|application/json|

<details>
<summary>説明を開きます。</summary>

:::note info
**/api/data** というエンドポイントを実装する**バックエンドサービス**と、**HTTP GET** リクエストを送信して、JSON データを取得するフロントエンドの**クライアントアプリ**を実装します。
:::

</details>

## バックエンドサービスを実装

前回の記事からご確認いただけます。

https://qiita.com/studio_meowtoon/items/0d28fc54b395520d880b

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

プロジェクトフォルダに移動します。
※ ~/tmp/hello-spring-dapr をプロジェクトフォルダとします。

```sh
$ cd ~/tmp/hello-spring-dapr
```

### Java アプリをビルド

Java アプリをビルドします。

```sh
$ mvn clean package
```

:::note info
ここまでの手順で、target/app.jar Java アプリが作成されました。
:::

### コンテナイメージをビルド

コンテナイメージをビルドします。

```sh
$ docker build -t app-hello-spring-boot .
```

### コンテナイメージを Docker Hub にプッシュ

こちらの関連記事で手順がご確認いただけます。 

https://qiita.com/studio_meowtoon/items/cbcfe1433885bae514db

Docker Hub にログインします。  

```sh
$ docker login
Login Succeeded
```

コンテナイメージにタグを付けます。

:::note warn
$USER の部分はご自身のコンテナリポジトリに読み替えて下さい。  
:::

```sh
$ docker tag app-hello-spring-boot $USER/app-hello-spring-boot:latest
```

Docker Hub にコンテナイメージをプュシュします。

```sh
$ docker push $USER/app-hello-spring-boot:latest
```

:::note info
ここまでの手順で、Docker Hub に Spring Boot アプリのコンテナイメージを公開することができました。
:::

## フロントエンドアプリを実装

:::note alert
Azure Container Apps の構成ではフロントエンドアプリの React コンテナからバックエンドサービスの Spring Boot コンテナに直接 Dapr API を通してアクセスすることはできません。代わりにこの記事ではサーバーサイドで動作する Express を使用してフロントエンドアプリを実装します。
:::

### プロジェクトフォルダの作成

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

```sh
$ mkdir -p ~/tmp/hello-express
$ cd ~/tmp/hello-express
```

### JS ファイルの作成

server.js JS ファイルを作成します。

```sh
$ mkdir -p src
$ vim src/server.js
```

ファイルの内容

```js:src/server.js
import express from 'express';
import fetch from 'node-fetch';

const app = express();
const port = 3000;

const fetchData = async () => {
    try {
        const response = await fetch('http://localhost:3500/v1.0/invoke/app-hello-spring/method/api/data');
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('データの取得中にエラーが発生しました:', error);
        throw error;
    }
};

app.get('/', async (req, res) => {
    try {
        res.send(`
            <!DOCTYPE html>
            <html>
                <head>
                <title>Hello Express</title>
                </head>
                <body>
                <div id="message"><h2>Loading...</h2></div>
                <button id="fetchButton" onclick="fetchData()">データを取得</button>
                <script>
                    const messageDiv = document.getElementById('message');
                    const fetchButton = document.getElementById('fetchButton');
                    const fetchData = () => {
                    fetch('/api/data')
                        .then(response => response.json())
                        .then(data => {
                        messageDiv.innerHTML = '<h1>' + data.message + '</h1>';
                        fetchButton.style.display = 'none';
                        })
                        .catch(error => {
                        console.error('Error fetching data:', error);
                        });
                    };
                </script>
                </body>
            </html>
        `);
    } catch (error) {
        console.error('エラーが発生しました:', error);
        res.status(500).send('エラーが発生しました');
    }
});

app.get('/api/data', async (req, res) => {
    try {
        const data = await fetchData();
        res.json(data);
    } catch (error) {
        console.error('データの取得中にエラーが発生しました:', error);
        res.status(500).json({ error: 'データの取得中にエラーが発生しました' });
    }
});

app.listen(port, () => {
    console.log(`サーバーは http://localhost:${port} で実行されています`);
});
```

### webpack 設定の作成

**webpack.config.js** ファイルを作成します。

```sh
$ vim webpack.config.js
```

ファイルの内容

```js:webpack.config.js
const path = require('path');

module.exports = {
  mode: 'development',
  target: 'node',
  entry: './src/server.js',
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'server.bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
};
```

### プロジェクトの初期化

プロジェクトの初期化を行います。

```sh
$ npm init -y
```

package.json を修正します。

```sh
$ vim package.json
```

ファイルの内容

```sh:package.json
{
  "name": "hello-express",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "scripts": {
    "start": "node build/server.bundle.js",
    "build": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
```

ライブラリをインストールします。

```sh
$ npm install express node-fetch --save
$ npm install babel-loader @babel/core @babel/preset-env \
    webpack webpack-cli --save-dev
```

ローカルでビルド・実行してエラーが出ないか確認します。

```sh
$ npm run build
$ npm start
```

### コンテナイメージをビルド

Dockerfile を作成します。

```sh
$ vim Dockerfile
```

ファイルの内容

```sh:Dockerfile
FROM node:lts-alpine as build-env

WORKDIR /app

COPY package.json package-lock.json /app/
COPY src /app/src
COPY webpack.config.js /app/

RUN npm install
RUN npm run build

FROM node:lts-alpine

WORKDIR /app

COPY --from=build-env /app/build /app/build

RUN npm install express

EXPOSE 3000

CMD ["node", "build/server.bundle.js"]
```

コンテナイメージをビルドします。

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

コンテナイメージを確認します。

```sh
$ docker images | grep app-hello-express
app-hello-express                 latest    58d8acb77435   21 minutes ago   184MB
```

### コンテナイメージを Docker Hub にプッシュ

コンテナイメージにタグを付けます。

:::note warn
$USER の部分はご自身のコンテナリポジトリに読み替えて下さい。  
:::

```sh
$ docker tag app-hello-express $USER/app-hello-express:latest
```

Docker Hub にコンテナイメージをプュシュします。

```sh
$ docker push $USER/app-hello-express:latest
```

:::note info
ここまでの手順で、Docker Hub に Express アプリのコンテナイメージを公開することができました。
:::

## Azure 環境へのデプロイ

### シェル変数の作成

:::note alert
**シェル変数**として、**Ubuntu** に以下の値を作成します。
:::

```sh
location_name=japaneast
resource_group_name=rg-hello
containerapp_env_name=cae-hello
```

### Azure 環境にサインイン

こちらの関連記事からご確認いただけます。

https://qiita.com/studio_meowtoon/items/9721041f9e6e3e987cf0

Azure CLI でログインします。  

```sh
$ az login
```

### Azure 環境の作成

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

リソースグループを作成します。

```sh:Microsoft.Resources/resourceGroups
$ az group create \
    --name $resource_group_name \
    --location $location_name
```

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

コンテナ アプリ環境を作成します。    
※ Kubernetes 基盤の環境がベースとなっているので少し時間がかかります。  

```sh:Microsoft.App/managedEnvironments
$ az containerapp env create \
    --resource-group $resource_group_name \
    --name $containerapp_env_name \
    --location $location_name
```

Azure Portal での表示
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/3a57caf4-0700-294f-f96f-69bb1ee6cadf.png)

:::note info
ここまでの手順で、Azure に Container Apps 環境を作成することができました。
:::

## バックエンドサービスをデプロイ

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

プロジェクトフォルダに移動します。

```sh
$ cd ~/tmp/hello-spring-dapr
```

### シェル変数の作成

:::note alert
**シェル変数**として、**Ubuntu** に以下の値を作成します。状況により、コンテナ アプリの名前を変える必要があります。
:::

```sh
resource_group_name=rg-hello
containerapp_env_name=cae-hello
containerapp_name=ca-hello-spring-boot
container_image_name=app-hello-spring-boot:latest
```

### コンテナ アプリ

コンテナ アプリの作成とデプロイを行います。    

```sh:Microsoft.App/containerApps
$ az containerapp create \
    --resource-group $resource_group_name \
    --environment $containerapp_env_name \
    --name $containerapp_name \
    --image $USER/$container_image_name \
    --target-port 8080 \
    --ingress 'external' \
    --min-replicas 1 \
    --enable-dapr true \
    --dapr-app-id app-hello-spring \
    --dapr-app-port 8080 \
    --dapr-app-protocol http
```
Azure Portal での表示
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/b2138931-98ce-9611-c472-edacbb39f8ab.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/4a6dea2a-27fb-68f5-1ca0-6f092fca6ae5.png)

### コンテナに接続

バックエンドサービスの Dapr が有効になっているか確認します。

:::note alert
Container Apps のデフォルト構成では、Dapar API はクラウド環境の外部に公開されていません。
:::

コンテナ アプリに接続します。
```sh
$ az containerapp exec \
    --resource-group $resource_group_name \
    --name $containerapp_name
```

コンテナ アプリに curl をインストールします。
※ コンテナから出るときは ctrl + D を押します。  

```
# apk update
# apk add curl
# curl --version
curl 8.0.1 (x86_64-alpine-linux-musl) libcurl/8.0.1 OpenSSL/1.1.1k zlib/1.2.11 brotli/1.0.9 nghttp2/1.43.0
```

コンテナ アプリから Dapr API をリクエストして確認します。

```
# curl http://localhost:3500/v1.0/invoke/app-hello-spring/method/api/data -w '\n'
```

出力

```
{"message":"Hello World!"}
```

:::note info
ここまでの手順で、Container Apps に、Dapr サイドカーを有効にしたバックエンドサービスをデプロイすることができました。
:::

## フロントエンドアプリをデプロイ

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

プロジェクトフォルダに移動します。

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

### シェル変数の作成

:::note alert
**シェル変数**として、**Ubuntu** に以下の値を作成します。状況により、コンテナ アプリの名前を変える必要があります。
:::

```sh
resource_group_name=rg-hello
containerapp_env_name=cae-hello
containerapp_name=ca-hello-express
container_image_name=app-hello-express:latest
```

### コンテナ アプリ

コンテナ アプリの作成とデプロイを行います。    

```sh:Microsoft.App/containerApps
$ az containerapp create \
    --resource-group $resource_group_name \
    --environment $containerapp_env_name \
    --name $containerapp_name \
    --image $USER/$container_image_name \
    --target-port 3000 \
    --ingress 'external' \
    --min-replicas 1 \
    --enable-dapr true \
    --dapr-app-protocol http
```

:::note alert
Container Apps で構成する場合、フロントエンドアプリのコンテナでも Dapr サイドカーの設定が必須です。
:::

:::note warn
コンテナ アプリを削除する場合は以下のコマンドを実行します。

```sh
$ az containerapp delete \
    --resource-group $resource_group_name \
    --name $containerapp_name
```
:::

Azure Portal での表示
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/ef91fa69-470c-6282-b3f2-4daf755e384e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/c62e4319-aa31-c561-e106-e733f8e7f8bf.png)

### コンテナ動作の確認

コンテナ アプリの FQDN を Ubuntu のシェル変数 **containerapp_fqdn** に取得します。

```sh
$ containerapp_fqdn=$(az containerapp show \
    --resource-group $resource_group_name \
    --name $containerapp_name \
    --query 'properties.configuration.ingress.fqdn' \
    --output tsv)
```

コマンドから Web ブラウザを立ち上げます。

```sh
$ sudo apt update
$ sudo apt install wslu
$ wslview https://$containerapp_fqdn/
```

フロントエンドアプリからバックエンドサービスのデータを取得します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/ab3a23e3-8bb8-b424-2df6-4e6fee343c1a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/66e1e678-ee4d-0d54-f018-97e2cfbee299.png)

:::note info
ここまでの手順で、フロントエンドアプリ、バックエンドサービスのそれぞれの Dapr サイドカーを通して連携動作させることができました。
:::

## まとめ

+ Dapr、Express、Spring Boot で構成する、シンプルなマイクロサービスを実装することが出来ました。
+ この記事の例ではまだ Spring Boot 側のアプリに Dapr への依存関係はありません。
+ 今後さらに Spring Boot から Dapr クライアントオブジェクトを操作する方法を学ぶ必要があります。

どうでしたか？ WSL Ubuntu で Dapr を使用したマイクロサービス開発環境が手軽に構築できます。ぜひお試しください。今後も Ubuntu の開発環境などを紹介していきますので、ぜひお楽しみにしてください。

## 参考資料

+ [Dapr SDK Java](https://docs.dapr.io/developing-applications/sdks/java/)