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

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

こんにちは、@studio_meowtoon です。今回は、WSL Ubuntu 22.04 で Dapr と React と Spring Boot を利用してマイクロサービスを構成する方法を紹介します。
![dapr_on_Docker.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/99e4d001-a922-d003-780a-e09a9fe6d80a.png)

## 目的

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

## 実現すること

Ubuntu の Docker 環境にて、Dapr、React、Spring Boot で構成する、シンプルなマイクロサービスを実装します。  

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

https://qiita.com/studio_meowtoon/items/38d2632f3851cb2f89e1

## 開発環境

+ 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
```

</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/2141cfd290b526a3bd39

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

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

CORS への対応のため、アプリケーションクラスを修正します。

```sh
$ vim src/main/java/com/example/springdapr/Application.java
```

ファイルの内容

```java:Application.java
package com.example.springdapr;

import java.util.Map;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
@RequestMapping("/api")
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @CrossOrigin
    @GetMapping("/data")
    public Map<String, String> getData() {
        Map<String, String> map = Map.of("message", "Hello World!");
        return map;
    }
}
```

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

```sh
$ mvn clean package
```

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

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

Dockerfile を作成します。

```sh
$ vim Dockerfile
```

ファイルの内容

```sh: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"]
```

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

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

確認します。

```sh
$ docker images | grep app-hello-spring-boot
app-hello-spring-boot   latest    2ee39bcf1346   11 seconds ago   278MB
```

### コンテナを起動する

バックエンドサービスのコンテナを実行します。

```sh
$ docker run --rm --name app-backend \
    -p 8080:8080 app-hello-spring-boot
```

別ターミナルからリクエストします。 

```sh
$ curl http://localhost:8080/api/data -w '\n'
```

出力

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

:::note info
ここまでの手順で、バックエンドのサービスを作成することができました。
:::

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

こちらの以前の記事をご参照いただけます。

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

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

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

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

### JS ファイルの作成

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

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

ファイルの内容

```jsx:src/index.js
import React, { useState } from 'react';
import ReactDOM from 'react-dom';

const App = () => {
    const [data, setData] = useState(null);
    // ボタンがクリックされたときにデータを取得する関数
    const fetchData = () => {
        fetch('http://localhost:8080/api/data')
            .then(response => response.json())
            .then(data => {
                setData(data); // レスポンスのデータを state にセット
            })
            .catch(error => {
                console.error('Error fetching data:', error);
            });
    };

    return (
        <div>
            {data ? (
                <h1>{data.message}</h1>
            ) : (
                <div>
                    <h2>Loading...</h2>
                    <button onClick={fetchData}>データを取得</button>
                </div>
            )}
        </div>
    );
};

ReactDOM.render(<App />, document.getElementById("app"));
```

### HTML ファイルの作成

**index.html** ファイルを作成します。

```sh
$ vim src/index.html
```

ファイルの内容

```html:src/index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>React</title>
    </head>
    <body>
        <div id="app"></div>
    </body>
</html>
```

### webpack 設定の作成

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

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

ファイルの内容

```js:webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

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

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

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

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

package.json を修正します。

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

ファイルの内容

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

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

```sh
$ npm install \
    react react-dom \
    babel-loader @babel/core @babel/preset-env @babel/preset-react \
    webpack webpack-cli html-webpack-plugin \
    --save-dev
```

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

Dockerfile を作成します。

```sh
$ vim Dockerfile
```

ファイルの内容

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

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

FROM nginx:alpine

COPY --from=build-env /app/build /usr/share/nginx/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
```

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

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

確認します。

```sh
$ docker images | grep app-hello-react
app-hello-react         latest    e98d06760672   8 seconds ago    42.6MB
```

### コンテナを起動する

フロントエンドアプリのコンテナを実行します。

```sh
$ docker run --rm --name app-frontend \
    -p 80:80 app-hello-react
```

WEBアプリにアクセスします。

```sh
http://localhost
```

フロントエンドアプリからバックエンドサービスのデータを取得します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/e669f03f-5a91-c5c9-aae8-2196a9d7457d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/868166a0-a9a1-9935-8199-d402dc3b586f.png)

:::note info
ここまでの手順で、バックエンドサービス、フロントエンドアプリの各コンテナを直接連携させて動作させることができました。
:::

## Dapr を通してコンテナを連携動作させる

### フロントエンドアプリの修正

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

ファイルの内容※一部分

```jsx:src/index.js
// ボタンがクリックされたときにデータを取得する関数
const fetchData = () => {
    fetch('http://localhost:3500/v1.0/invoke/app-hello-spring/method/api/data')
        .then(response => response.json())
        .then(data => {
            setData(data); // レスポンスのデータを state にセット
        })
        .catch(error => {
            console.error('Error fetching data:', error);
        });
};
```

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

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

### docker-compose.yml の作成

docker-compose.yml ファイルを作成します。

```sh
$ vim docker-compose.yml
```

ファイルの内容

```yaml
version: '3'
services:
  ############################
  # React app
  app-react:
    image: app-hello-react
    depends_on:
      - redis
      - placement
    ports:
      - "80:80"
    networks:
      - net-hello
  ############################
  # Spring Boot app + Dapr sidecar
  app-spring:
    image: app-hello-spring-boot
    depends_on:
      - redis
      - placement
    ports:
      - "3500:3500"
    networks:
      - net-hello
  app-spring-dapr:
    image: daprio/daprd:edge
    command: [
      "./daprd",
      "--app-id", "app-hello-spring",
      "--app-port", "8080",
      "--dapr-http-port", "3500",
      "--dapr-grpc-port", "50051",
      "--placement-host-address", "placement:50006",
      "--components-path", "/components"
    ]
    volumes:
      - "./components/:/components"
    depends_on:
      - app-spring
    network_mode: "service:app-spring"
  ############################
  # Dapr placement service
  placement:
    image: daprio/dapr
    command: ["./placement", "-port", "50006"]
    ports:
      - "50006:50006"
    networks:
      - net-hello
  ############################
  # Redis state store
  redis:
    image: redis:alpine
    ports:
      - "6380:6379"
    networks:
      - net-hello
networks:
    net-hello:
```

:::note info
Dapr を React、Spring Boot アプリの**サイドカー**として設定しており、Spring Boot アプリのコンテナは 3500 番ポートでリッスンしています。  
:::

### Docker 環境で起動する

コンテナを起動します。

```sh
$ docker-compose up -d
Creating hello-react_placement_1 ... done
Creating hello-react_redis_1     ... done
Creating hello-react_app-react_1  ... done
Creating hello-react_app-spring_1 ... done
Creating hello-react_app-spring-dapr_1 ... done
```

コンテナを確認します。

```sh
$ docker ps | grep hello
2ed6b9cdb78d   daprio/daprd:edge       "./daprd --app-id ap…"   47 seconds ago   Up 47 seconds                                                         hell -react_app-spring-dapr_1
c9b60a6a1fe8   app-hello-react         "/docker-entrypoint.…"   48 seconds ago   Up 47 seconds   0.0.0.0:80->80/tcp, :::80->80/tcp                     hell -react_app-react_1
7c2c941d6bf5   app-hello-spring-boot   "java -jar app.jar"       48 seconds ago   Up 47 seconds   0.0.0.0:3500->3500/tcp, :::3500->3500/tcp, 8080/tcp   hello-react_app-spring_1
d4a8f2c769ad   redis:alpine            "docker-entrypoint.s…"   49 seconds ago   Up 47 seconds   0.0.0.0:6380->6379/tcp, :::6380->6379/tcp             hell -react_redis_1
216a0d2c9efe   daprio/dapr             "./placement -port 5…"   49 seconds ago   Up 47 seconds   0.0.0.0:50006->50006/tcp, :::50006->50006/tcp         hell -react_placement_1
```

フロントエンドアプリからバックエンドサービスのデータを取得します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/e669f03f-5a91-c5c9-aae8-2196a9d7457d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/868166a0-a9a1-9935-8199-d402dc3b586f.png)

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

## まとめ

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

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

## 推奨コンテンツ

https://qiita.com/studio_meowtoon/items/0609130d905a46acd546

## 参考資料

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