CLOVER🍀

That was when it all began.

Amazon S3互換のオブジェクトストレージ、MinIOを試す

これは、なにをしたくて書いたもの?

前々から1度試しておきたかった、Amazon S3互換のオブジェクトストレージMinIOを扱ってみようかなと。

MinIOとは

MinIOは、Amazon S3互換のオブジェクトストレージです。

MinIO | High Performance, Kubernetes Native Object Storage

Goで実装されています。

GitHub - minio/minio: High Performance, Kubernetes Native Object Storage

3つのライセンス形態があるようですが、サポートに差があるみたいですね。

Pricing

ドキュメントは、ベアメタル環境向け、Kubernetes環境向け、VMware Cloud Fundation環境向けのものがありますが、
今回はベアメタル環境向けのものを参照します。

MinIO High Performance Object Storage — MinIO Baremetal Documentation

MinIO Object Storage for Hybrid Cloud — MinIO Hybrid Cloud Documentation

機能については、こちら。

MinIO Server Features — MinIO Baremetal Documentation

Notification、バージョニング、耐障害性、レプリケーションを備えます。

ちなみに、これらのドキュメントは現在作成中で、レガシー扱いとなったドキュメントもあります。現在のドキュメントに
記載がないものについては、レガシードキュメントの方を参照する必要があるようです。

MinIO | The MinIO Quickstart Guide

たとえば、AWS CLIや各種プログラミング言語向けのクライアントライブラリなどはレガシードキュメントにしか
ありません。

MinIO | AWS CLI with MinIO - Cookbook/Recipe

MinIO | Java Client Quickstart Guide

Amazon S3互換ということで、AWS CLIやAWS SDKが使えるわけですが、専用のクライアントコマンドや管理コマンド、
クライアントライブラリも備えています。

MinIO Client (mc) — MinIO Baremetal Documentation

MinIO Admin (mc admin) — MinIO Baremetal Documentation

今回は、とりあえずスタンドアロンでMinIOサーバーを立てて、クライアントコマンドやAWS CLIでアクセスして
みたいと思います。

環境

今回の環境は、こちら。Ubuntu Linux 20.04 LTSです。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.3 LTS
Release:    20.04
Codename:   focal


$ uname -srvmpio
Linux 5.4.0-84-generic #94-Ubuntu SMP Thu Aug 26 20:27:37 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

AWS CLIもインストールしておきました。

$ aws --version
aws-cli/2.2.36 Python/3.8.8 Linux/5.4.0-84-generic exe/x86_64.ubuntu.20 prompt/off

MinIOサーバーをインストールする

まずは、MinIOサーバーをインストールします。

MinIO | Code and downloads to create high performance object storage

インストール方法は実行バイナリ、rpmパッケージ、debパッケージ、そしてDockerイメージがありますが、今回は
debパッケージのものを使うことにします。

debパッケージをダウンロードして

$ curl -O https://dl.min.io/server/minio/release/linux-amd64/minio_20210903035613.0.0_amd64.deb

インストール。

$ sudo dpkg -i minio_20210903035613.0.0_amd64.deb

バージョン。

$ minio --version
minio version RELEASE.2021-09-03T03-56-13Z

実行方法についてはこちらに記載がありますが、

MinIO Server — MinIO Baremetal Documentation

ヘルプも確認。

$ minio --help
NAME:
  minio - High Performance Object Storage

DESCRIPTION:
  Build high performance data infrastructure for machine learning, analytics and application data workloads with MinIO

USAGE:
  minio [FLAGS] COMMAND [ARGS...]

COMMANDS:
  server   start object storage server
  gateway  start object storage gateway
  
FLAGS:
  --certs-dir value, -S value  path to certs directory (default: "/home/vagrant/.minio/certs")
  --quiet                      disable startup information
  --anonymous                  hide sensitive information from logging
  --json                       output server logs and startup information in json format
  --help, -h                   show help
  --version, -v                print the version
  
VERSION:
  RELEASE.2021-09-03T03-56-13Z

起動方法にserverとgatewayがあるようです。

MinIO Gatewayは、AWSやAzureのオブジェクトストレージをバックエンドにするプロセスです。

今回は、serverの方を使います。

$ minio server --help
NAME:
  minio server - start object storage server

USAGE:
  minio server [FLAGS] DIR1 [DIR2..]
  minio server [FLAGS] DIR{1...64}
  minio server [FLAGS] DIR{1...64} DIR{65...128}

DIR:
  DIR points to a directory on a filesystem. When you want to combine
  multiple drives into a single large system, pass one directory per
  filesystem separated by space. You may also use a '...' convention
  to abbreviate the directory arguments. Remote directories in a
  distributed setup are encoded as HTTP(s) URIs.

FLAGS:
  --address value              bind to a specific ADDRESS:PORT, ADDRESS can be an IP or hostname (default: ":9000")
  --console-address value      bind to a specific ADDRESS:PORT for embedded Console UI, ADDRESS can be an IP or hostname
  --certs-dir value, -S value  path to certs directory (default: "/home/vagrant/.minio/certs")
  --quiet                      disable startup information
  --anonymous                  hide sensitive information from logging
  --json                       output server logs and startup information in json format
  --help, -h                   show help
  
EXAMPLES:
  1. Start minio server on "/home/shared" directory.
     $ minio server /home/shared

  2. Start single node server with 64 local drives "/mnt/data1" to "/mnt/data64".
     $ minio server /mnt/data{1...64}

  3. Start distributed minio server on an 32 node setup with 32 drives each, run following command on all the nodes
     $ export MINIO_ROOT_USER=minio
     $ export MINIO_ROOT_PASSWORD=miniostorage
     $ minio server http://node{1...32}.example.com/mnt/export{1...32}

  4. Start distributed minio server in an expanded setup, run the following command on all the nodes
     $ export MINIO_ROOT_USER=minio
     $ export MINIO_ROOT_PASSWORD=miniostorage
     $ minio server http://node{1...16}.example.com/mnt/export{1...32} \
            http://node{17...64}.example.com/mnt/export{1...64}

データを保存するためのディレクトリが必要なようなので、作成して

$ mkdir data

起動。

$ minio server data
API: http://192.168.121.136:9000  http://192.168.33.10:9000  http://127.0.0.1:9000     
RootUser: minioadmin 
RootPass: minioadmin 

Console: http://192.168.121.136:42799 http://192.168.33.10:42799 http://127.0.0.1:42799   
RootUser: minioadmin 
RootPass: minioadmin 

Command-line: https://docs.min.io/docs/minio-client-quickstart-guide
   $ mc alias set myminio http://192.168.121.136:9000 minioadmin minioadmin

Documentation: https://docs.min.io

WARNING: Console endpoint is listening on a dynamic port (42799), please use --console-address ":PORT" to choose a static port.
WARNING: Detected default credentials 'minioadmin:minioadmin', we recommend that you change these values with 'MINIO_ROOT_USER' and 'MINIO_ROOT_PASSWORD' environment variables

特に指定しないと、Web Consoleのポートがランダムに決まるようなので、指定しておきます。ドキュメントに習って
9001にしておきました。

$ minio server data --console-address ":9001"
API: http://192.168.121.136:9000  http://192.168.33.10:9000  http://127.0.0.1:9000     
RootUser: minioadmin 
RootPass: minioadmin 

Console: http://192.168.121.136:9001 http://192.168.33.10:9001 http://127.0.0.1:9001   
RootUser: minioadmin 
RootPass: minioadmin 

Command-line: https://docs.min.io/docs/minio-client-quickstart-guide
   $ mc alias set myminio http://192.168.121.136:9000 minioadmin minioadmin

Documentation: https://docs.min.io
WARNING: Detected default credentials 'minioadmin:minioadmin', we recommend that you change these values with 'MINIO_ROOT_USER' and 'MINIO_ROOT_PASSWORD' environment variables

起動時に表示されているのは、MinIOの管理ユーザーの名前とパスワードでもあり、AWS SDKのクレデンシャルでも
あります。

これは、MINIO_ROOT_USERおよびMINIO_ROOT_PASSWORD環境変数で指定することができます。

$ MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=password minio server data --console-address ":9001"
API: http://192.168.121.136:9000  http://192.168.33.10:9000  http://127.0.0.1:9000     
RootUser: admin 
RootPass: password 

Console: http://192.168.121.136:9001 http://192.168.33.10:9001 http://127.0.0.1:9001   
RootUser: admin 
RootPass: password 

Command-line: https://docs.min.io/docs/minio-client-quickstart-guide
   $ mc alias set myminio http://192.168.121.136:9000 admin password

Documentation: https://docs.min.io

今回は、デフォルトのminioadmin / minioadminをそのまま使うことにします。

$ minio server data --console-address ":9001"

MinIOクライアントをインストールする

続いては、MinIOクライアントをインストールしましょう。

こちらも、debパッケージでインストールします。ダウンロードして

$ curl -O https://dl.min.io/client/mc/release/linux-amd64/mcli_20210902092127.0.0_amd64.deb

インストール。

$ sudo dpkg -i mcli_20210902092127.0.0_amd64.deb

バージョン。

$ mcli --version
mcli version RELEASE.2021-09-02T09-21-27Z

ちなみに、実行可能ファイルでインストールするとmcコマンドとなり、rpmパッケージやdebパッケージでインストール
するとmcliコマンドになります。

MinIO | Code and downloads to create high performance object storage

どちらにせよ、インストールされるのは単一バイナリだけですが。

$ dpkg -L mcli
/usr
/usr/local
/usr/local/bin
/usr/local/bin/mcli

ダウンロードページに記載されているコマンドに従い、myminioという名前にローカルのMinIOをエイリアスとして
設定します。この時、ユーザー名とパスワードも必要です。

$ mcli alias set myminio http://localhost:9000 minioadmin minioadmin

MinIOサーバーの情報を確認。

$ mcli admin info myminio
●  localhost:9000
   Uptime: 6 minutes 
   Version: 2021-09-03T03:56:13Z
   Network: 1/1 OK 

いくつかコマンドを試してみます。

バケットの作成。

$ mcli mb myminio/my-bucket
Bucket created successfully `myminio/my-bucket`.

確認。

$ mcli ls myminio
[2021-09-10 22:58:17 JST]     0B my-bucket/

ローカルでファイルを作成して

$ echo 'Hello World' > hello.txt

アップロード。

$ mcli cp hello.txt myminio/my-bucket/hello.txt

ダウンロード。

$ mcli cp myminio/my-bucket/hello.txt hello-copy.txt


$ cat hello-copy.txt
Hello World

ファイル削除。

$ mcli rm myminio/my-bucket/hello.txt
Removing `myminio/my-bucket/hello.txt`.

バケット削除。

$ mcli rb myminio/my-bucket
Removed `myminio/my-bucket` successfully.

もう1度バケットの作成とファイルアップロード、そしてバケットをもうひとつ作成します。

$ mcli mb myminio/my-bucket
$ mcli cp hello.txt myminio/my-bucket/hello.txt


$ mcli mb myminio/my-bucket2

起動時に--console-address ":9001"を指定したので、http://[MinIOサーバーのIPアドレス]:9001でWeb Consoleに
アクセスできます。

MinIO Console — MinIO Baremetal Documentation

ログインアカウントは、起動時に表示されたユーザー名とパスワードです。

Web Consoleでは、Minioサーバーの情報を参照したり、バケットへのアクセス、ユーザー管理などができたりします。

f:id:Kazuhira:20210910230530p:plain

f:id:Kazuhira:20210910230550p:plain

AWS CLIでアクセスする

続いて、AWS CLIでアクセスしてみましょう。

クレデンシャルとリージョンは、環境変数で指定することにします。

$ export AWS_ACCESS_KEY_ID=minioadmin
$ export AWS_SECRET_ACCESS_KEY=minioadmin
$ export AWS_DEFAULT_REGION=ap-northeast-1

AWS CLIでアクセスする際は、--endpoint-url http://[MinIOサーバーのIPアドレス]:9000を指定します。

$ aws --endpoint-url http://localhost:9000 s3 ls
2021-09-10 23:03:54 my-bucket
2021-09-10 23:04:19 my-bucket2

先ほど、MinIOのクライアントツールで作成したバケットが見えています。

ファイルのダウンロード。

$ aws --endpoint-url http://localhost:9000 s3 cp s3://my-bucket/hello.txt -
Hello World

こちらもOKそうですね。

systemdでMinIOサーバーを起動する

ところで、MinIOサーバーのパッケージを見ると、どうやらsystemdの定義ファイルが含まれているようです。

$ dpkg -L minio
/usr
/usr/local
/usr/local/bin
/usr/local/bin/minio
/etc
/etc/systemd
/etc/systemd/system
/etc/systemd/system/minio.service

systemdのユニット定義ファイルは、こんな感じでした。

/etc/systemd/system/minio.service

[Unit]
Description=MinIO
Documentation=https://docs.min.io
Wants=network-online.target
After=network-online.target
AssertFileIsExecutable=/usr/local/bin/minio

[Service]
WorkingDirectory=/usr/local

User=minio-user
Group=minio-user
ProtectProc=invisible

EnvironmentFile=-/etc/default/minio
ExecStartPre=/bin/bash -c "if [ -z \"${MINIO_VOLUMES}\" ]; then echo \"Variable MINIO_VOLUMES not set in /etc/default/minio\"; exit 1; fi"
ExecStart=/usr/local/bin/minio server $MINIO_OPTS $MINIO_VOLUMES

# Let systemd restart this service always
Restart=always

# Specifies the maximum file descriptor number that can be opened by this process
LimitNOFILE=65536

# Specifies the maximum number of threads this process can create
TasksMax=infinity

# Disable timeout logic and wait until process is stopped
TimeoutStopSec=infinity
SendSIGKILL=no

[Install]
WantedBy=multi-user.target

# Built for ${project.name}-${project.version} (${project.name})

有効化して

$ sudo systemctl enable minio
Created symlink /etc/systemd/system/multi-user.target.wants/minio.service → /etc/systemd/system/minio.service.

起動しようとすると、失敗します。

$ sudo systemctl start minio
Job for minio.service failed because the control process exited with error code.
See "systemctl status minio.service" and "journalctl -xe" for details.

ログを見ると、クレデンシャルに問題があるようです。

 9月 10 23:18:28 server systemd[1]: Starting MinIO...
 9月 10 23:18:28 server systemd[1989]: minio.service: Failed to determine user credentials: No such process
 9月 10 23:18:28 server systemd[1989]: minio.service: Failed at step USER spawning /bin/bash: No such process

ユニット定義のユーザーおよびグループ指定を見ると、存在しないユーザーとグループが指定されています。

User=minio-user
Group=minio-user

このままだと起動しないので、ユーザーを追加します。

$ sudo useradd minio-user

これでもまだ失敗します。

$ sudo systemctl start minio
Job for minio.service failed because the control process exited with error code.
See "systemctl status minio.service" and "journalctl -xe" for details.

エラーメッセージを確認すると、/etc/default/minioというファイルに環境変数の定義がないと言っています。

 9月 10 23:19:44 server systemd[1]: Starting MinIO...
 9月 10 23:19:44 server bash[2030]: Variable MINIO_VOLUMES not set in /etc/default/minio

これは、ExecStartPreおよびExecStartを見ると理由がわかります。

ExecStartPre=/bin/bash -c "if [ -z \"${MINIO_VOLUMES}\" ]; then echo \"Variable MINIO_VOLUMES not set in /etc/default/minio\"; exit 1; fi"
ExecStart=/usr/local/bin/minio server $MINIO_OPTS $MINIO_VOLUMES

起動時の設定をしなければいけないようです。

では、データを保存するディレクトリを作成して

$ sudo mkdir -p /var/lib/minio/data
$ sudo chown -R minio-user:minio-user /var/lib/minio

/etc/default/minioファイルも作成します。ここで、起動時のオプションとデータの保存先を指定します。

/etc/default/minio

MINIO_OPTS=--console-address :9001
MINIO_VOLUMES=/var/lib/minio/data

今度は起動するようになります。

$ sudo systemctl start minio

デフォルトのクレデンシャルだと警告が出るので

 9月 10 23:26:00 server minio[2273]: WARNING: Detected default credentials 'minioadmin:minioadmin', we recommend that you change these values with 'MINIO_ROOT_USER' and 'MINIO_ROOT_PASSWORD' environment variables

好みに応じて、MINIO_ROOT_USERおよびMINIO_ROOT_PASSWORDを設定してクレデンシャルを指定すると
よいでしょう。

/etc/default/minio

MINIO_OPTS=--console-address :9001
MINIO_VOLUMES=/var/lib/minio/data
MINIO_ROOT_USER=root-user
MINIO_ROOT_PASSWORD=password

まとめ

Amazon S3互換のオブジェクトストレージ、MinIOを試してみました。

MinIOのクライアントコマンドは最初は戸惑ったりしましたが、あとはそれほど困りませんでしたね。

お手軽に使えそうなので、Amazon S3の代わりが欲しい時はLocalStackと使い分けていこうかなと思います。

Infinispan Server 12.1で、Hot Rod Client(RemoteCacheManagerAdmin)からCacheを作成する

これは、なにをしたくて書いたもの?

InfinispanのHot Rod Clientには、Cacheの作成、削除などを行う管理APIがあります。

今までも何回か使っていたのですが、よく忘れるので単体でメモしておくことにしました。

RemoteCacheManagerAdminインターフェース

RemoteCacheManagerAdminというインターフェースを使うことで、Infinispan ServerにおけるCacheの作成、削除などが
できます。

RemoteCacheManagerAdmin (Infinispan JavaDoc 12.1.7.Final API)

クラスタ構成のInfinispan Serverであっても、各Nodeに一気にCacheを作れたりするので便利です。
各Nodeそれぞれに対して、XMLファイルを修正しなくてすみますからね。
もっともCLIで操作した場合も同じように各NodeにCacheの定義を反映してくれるのですが、APIで書けた方が
便利な時もあるかな、と。

ドキュメントとしては、このあたりに記載があります。

Deploying and Configuring Infinispan Servers / Creating Remote Caches with Hot Rod Clients

Hot Rod Java Clients / Creating Remote Caches with Hot Rod Clients

書かれている内容は同じなのですが。

RemoteCacheManagerAdminではなく、RemoteCacheConfigurationBuilder#configurationでCacheの定義を行う方法も
紹介されているのですが、こちらはパスします…。

RemoteCacheConfigurationBuilder (Infinispan JavaDoc 12.1.7.Final API)

環境

今回の環境は、こちらです。

$ java --version
openjdk 11.0.11 2021-04-20
OpenJDK Runtime Environment (build 11.0.11+9-Ubuntu-0ubuntu2.20.04)
OpenJDK 64-Bit Server VM (build 11.0.11+9-Ubuntu-0ubuntu2.20.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.2 (ea98e05a04480131370aa0c110b8c54cf726c06f)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.11, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-81-generic", arch: "amd64", family: "unix"

Infinispan Serverは12.1.7.Finalを使い、172.17.0.2〜4の3 Node用意します。

準備

各Nodeには、adminグループに属するユーザーと、applicationグループに属するユーザーをそれぞれ作成しておきます。

$ bin/cli.sh user create -g admin -p password admin-user
$ bin/cli.sh user create -g application -p password app-user

各グループは、ClusterRoleMapperを意識しています。

Deploying and Configuring Infinispan Servers / User Roles and Permissions

Cacheの定義などは行いません。

Maven依存関係などはこちら。

    <dependencies>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-client-hotrod</artifactId>
            <version>12.1.7.Final</version>
        </dependency>

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-core</artifactId>
            <version>12.1.7.Final</version>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.7.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.7.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.20.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>

今回のお題には、最低限infinispan-client-hotrodが必要です。infinispan-coreはなくてもRemoteCacheManagerAdminを
使うことはできるのですが、あった方がCacheの定義をするには便利かなと思います。理由は後述します。

動作確認はテストコードで行うので、テスト用のライブラリも追加しておきます。

テストコードの雛形

テストコードの雛形は、こちら。

src/test/java/org/littlewings/remote/admin/CreateCacheTest.java

package org.littlewings.remote.admin;

import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.RemoteCacheManagerAdmin;
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class CreateCacheTest {

    private static String createUri(String userName, String password) {
        return String.format("hotrod://%s:%s@172.17.0.2:11222,172.17.0.3:11222,172.17.0.4:11222", userName, password);
    }

    // ここに、テストコードを書く
}

createUriというメソッドは、接続するユーザー名とパスワードを使って、Infinispan Serverへ接続するためのURIを
作成するメソッドです。こちらを接続情報とします。

RemoteCacheManagerAdminを使ってCacheを作成してみる

では、RemoteCacheManagerAdminを使ってCacheを作成してみます。

使用例はこちら。

    @Test
    public void createCacheByAdminUser() {
        String uri = createUri("admin-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration cacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            RemoteCache<String, String> cache = admin.createCache("distCache", cacheConfiguration);

            IntStream.rangeClosed(1, 100).forEach(i -> cache.put("key" + i, "value" + i));
            assertThat(cache.size()).isEqualTo(100);

            admin.removeCache("distCache");
        }
    }

RemoteCacheManagerAdminは、RemoteCacheManager#administrationで取得できます。
なお、接続にはadminグループに属しているユーザーを使用しています。

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

Cacheの作成はRemoteCacheManagerAdmin#createCacheで行えばよいのですが

            RemoteCache<String, String> cache = admin.createCache("distCache", cacheConfiguration);

この時に作成するCacheの定義情報が必要です。

Cacheの定義情報はStringとXMLStringConfigurationから作成でき、依存関係もinfinispan-client-hotrodだけで済みますが
Cacheの定義を文字列で用意することになり、まあ面倒です。この作り方は、最後に載せたいと思います。

もうひとつは、Embedded CacheでのConfigurationを使う方法です。こちらを使うとAPIでCache定義を組み立てられるの
ですが、infinispan-coreが必要になります。依存関係にinfinispan-coreを追加したのはこれが理由ですね。

こんな感じでCacheの定義情報を作成します。infinispan-client-hotrodの範囲外であることがわかるようにFQCNで
クラス名を書いています。

            org.infinispan.configuration.cache.Configuration cacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

Cacheの種類としては、Distributed Cacheとしました。

これでクラスタに参加する各NodeにCacheが作成され、すぐに使うことができます。

            IntStream.rangeClosed(1, 100).forEach(i -> cache.put("key" + i, "value" + i));
            assertThat(cache.size()).isEqualTo(100);

RemoteCacheManagerAdminを使うと、Cacheの削除も可能です。

            admin.removeCache("distCache");

なお、Cacheの作成にはADMIN権限が必要です。このため、applicationグループに属したユーザーでは
RemoteCacheManagerAdmin#createCacheの呼び出しに失敗します。

    @Test
    public void createCacheByApplicationUser() {
        String uri = createUri("app-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration cacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            assertThatThrownBy(() -> admin.createCache("distCache", cacheConfiguration))
                    .hasMessageContaining("org.infinispan.commons.CacheException: java.lang.SecurityException: ISPN000287: Unauthorized access: subject 'Subject with principal(s): [app-user, RolePrincipal{name='application'},")
                    .hasMessageContaining("' lacks 'ADMIN' permission")
                    .isInstanceOf(HotRodClientException.class);
        }
    }

ClusterRoleMapperでADMIN権限が使えるのは、ALLが割り当てられているadminグループしかありません。

RemoteCacheManagerAdmin#getOrCreateCacheを使う

RemoteCacheManagerAdmin#createCacheでCacheの作成が可能なことはわかりましたが、作成対象のCacheがすでに
存在している場合は例外がスローされます。

    @Test
    public void recreateCache() {
        String uri = createUri("admin-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration cacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            assertThat(admin.createCache("distCache", cacheConfiguration))
                    .isNotNull()
                    .isInstanceOf(RemoteCache.class);

            assertThatThrownBy(() -> admin.createCache("distCache", cacheConfiguration))
                    .hasMessage("org.infinispan.commons.CacheConfigurationException: ISPN000507: Cache distCache already exists")
                    .isInstanceOf(HotRodClientException.class);

            admin.removeCache("distCache");
        }
    }

これを避ける場合は、RemoteCacheManagerAdmin#getOrCreateCacheを使えばOKです。

    @Test
    public void createOrGetCache() {
        String uri = createUri("admin-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration cacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            assertThat(admin.createCache("distCache", cacheConfiguration))
                    .isNotNull()
                    .isInstanceOf(RemoteCache.class);
            assertThat(admin.getOrCreateCache("distCache", cacheConfiguration))
                    .isNotNull()
                    .isInstanceOf(RemoteCache.class);

            admin.removeCache("distCache");

            assertThat(admin.getOrCreateCache("distCache", cacheConfiguration))
                    .isNotNull()
                    .isInstanceOf(RemoteCache.class);

            admin.removeCache("distCache");
        }
    }

対象のCacheがなければ作成し、すでに存在していればそれを使うという動作になります。

最初からこれを使ったらいいのでは、という話もありますね。

複数のCacheを作成してみる

ここまでDistributed Cacheばかり作っていましたが、さらにReplicated Cacheも作ってみましょう。

    @Test
    public void createVariousCache() {
        String uri = createUri("admin-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration distCacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            org.infinispan.configuration.cache.Configuration replCacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.REPL_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();


            RemoteCache<String, String> distCache = admin.getOrCreateCache("distCache", distCacheConfiguration);
            RemoteCache<String, String> replCache = admin.getOrCreateCache("replCache", replCacheConfiguration);

            IntStream.rangeClosed(1, 100).forEach(i -> distCache.put("key" + i, "value" + i));
            assertThat(distCache.size()).isEqualTo(100);

            IntStream.rangeClosed(1, 100).forEach(i -> replCache.put("key" + i, "value" + i));
            assertThat(replCache.size()).isEqualTo(100);

            admin.removeCache("distCache");
            admin.removeCache("replCache");
        }
    }

まあ、ちょっとしたオマケ的な感じですが…。

で、ここまでCacheを作成してきましたが、Infinispan Server側にどのように存在するかは特に書いてきませんでした。

Cacheを作成しても、infinispan.xmlそのものは変わりません。

server/conf/infinispan.xml

   <cache-container name="default" statistics="true">
      <transport cluster="${infinispan.cluster.name:cluster}" stack="${infinispan.cluster.stack:tcp}" node-name="${infinispan.node.name:}"/>
      <security>
         <authorization/>
      </security>
   </cache-container>

動的に作成したCacheは、server/data/caches.xmlファイルに記載されます。

server/data/caches.xml

<?xml version="1.0"?>
<infinispan xmlns="urn:infinispan:config:12.1">
    <cache-container>
        <replicated-cache mode="SYNC" remote-timeout="17500" name="replCache" statistics="true">
            <encoding>
                <key media-type="application/x-protostream"/>
                <value media-type="application/x-protostream"/>
            </encoding>
            <locking concurrency-level="1000" acquire-timeout="15000" striping="false"/>
            <state-transfer timeout="60000"/>
        </replicated-cache>
        <distributed-cache mode="SYNC" remote-timeout="17500" name="distCache" statistics="true">
            <encoding>
                <key media-type="application/x-protostream"/>
                <value media-type="application/x-protostream"/>
            </encoding>
            <locking concurrency-level="1000" acquire-timeout="15000" striping="false"/>
            <state-transfer timeout="60000"/>
        </distributed-cache>
    </cache-container>
</infinispan>

ConfigurationBuilderでCacheの定義を行った時はほぼ最低限の設定しか行っていなかったのに、いろいろと
デフォルト値が埋め込まれていますね。

文字列でCache定義を行う

最後に、Cacheの定義を文字列で作成します。ドキュメントに記載されていたのは、この方法ですね。

    @Test
    public void createCacheByString() {
        String uri = createUri("admin-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            String cacheConfigurationAsString =
                    "        <distributed-cache name=\"distCache\">\n" +
                            "            <encoding>\n" +
                            "                <key media-type=\"application/x-protostream\"/>\n" +
                            "                <value media-type=\"application/x-protostream\"/>\n" +
                            "            </encoding>\n" +
                            "        </distributed-cache>";

            org.infinispan.commons.configuration.XMLStringConfiguration cacheConfiguration =
                    new org.infinispan.commons.configuration.XMLStringConfiguration(cacheConfigurationAsString);

            RemoteCache<String, String> distCache = admin.getOrCreateCache("distCache", cacheConfiguration);

            IntStream.rangeClosed(1, 100).forEach(i -> distCache.put("key" + i, "value" + i));
            assertThat(distCache.size()).isEqualTo(100);

            admin.removeCache("distCache");

            assertThatThrownBy(() -> admin.getOrCreateCache("distributedCache", cacheConfiguration))
                    .hasMessageContaining("org.infinispan.commons.CacheConfigurationException: ISPN005031: The supplied configuration for cache 'distributedCache' is missing a named configuration for it:")
                    .isInstanceOf(HotRodClientException.class);
        }
    }

ポイントはこちらで、Cache定義の断片をXMLStringConfigurationとして作成します。

            String cacheConfigurationAsString =
                    "        <distributed-cache name=\"distCache\">\n" +
                            "            <encoding>\n" +
                            "                <key media-type=\"application/x-protostream\"/>\n" +
                            "                <value media-type=\"application/x-protostream\"/>\n" +
                            "            </encoding>\n" +
                            "        </distributed-cache>";

            org.infinispan.commons.configuration.XMLStringConfiguration cacheConfiguration =
                    new org.infinispan.commons.configuration.XMLStringConfiguration(cacheConfigurationAsString);

これをRemoteCacheManagerAdmin#createCacheやRemoteCacheManagerAdmin#getOrCreateCacheに渡せば
OKです。

            RemoteCache<String, String> distCache = admin.getOrCreateCache("distCache", cacheConfiguration);

この方法であればinfinispan-client-hotrodの依存関係に含まれる、infinispan-commonsがあれば依存関係としては
十分です。つまり、Hot Rod Clientのみで実行することができます。

ちなみに、XML定義のCache名と作成するCacheの名前が異なる場合は、例外がスローされるので注意しましょう。。

            assertThatThrownBy(() -> admin.getOrCreateCache("distributedCache", cacheConfiguration))
                    .hasMessageContaining("org.infinispan.commons.CacheConfigurationException: ISPN005031: The supplied configuration for cache 'distributedCache' is missing a named configuration for it:")
                    .isInstanceOf(HotRodClientException.class);

これでひととおり確認できた感じですね。

こちらのチュートリアルを見ていると、Configuration Templateも定義できそうですが、今回はパスします。

https://github.com/infinispan/infinispan-simple-tutorials/tree/main/infinispan-remote/cache-admin-api

Embedded CacheのConfigurationは、どう使われているの?

オマケ的にですが。

こちらがRemoteCacheManagerAdminインターフェースの実装です。

https://github.com/infinispan/infinispan/blob/12.1.7.Final/client/hotrod-client/src/main/java/org/infinispan/client/hotrod/impl/RemoteCacheManagerAdminImpl.java

Embedded CacheのConfigurationを渡した時にはどうなるのかな?と思ったのですが、結局文字列に変換されているので
最終的にはXML定義をInfinispan Serverに送ってCacheを作成するみたいですね。

https://github.com/infinispan/infinispan/blob/12.1.7.Final/client/hotrod-client/src/main/java/org/infinispan/client/hotrod/impl/RemoteCacheManagerAdminImpl.java#L61

まとめ

InfinispanのHot Rod ClientのRemoteCacheManagerAdminを使って、Infinispan Server上にCacheを動的に作成して
みました。

時々使っているのですが、本当によく忘れるのでいい機会かな、と。

今回作成したソースコードは、こちらに置いています。

https://github.com/kazuhira-r/infinispan-getting-started/tree/master/remote-create-cache