CLOVER🍀

That was when it all began.

Elasticsearchクラスタで、SSL/TLSの設定をする

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

以前、ElasticsearchのシングルノードでX-Pack Securityの簡単な設定をしてみました。

Elasticsearch(シングルノード)で、認証・認可設定をする - CLOVER🍀

今度は、クラスタとして構成したElasticsearchで行ってみたいと思います。

無料の暗号化とユーザー認証で、Elasticsearchクラスターを安全に保つ | Elastic Blog

SSL、TLS、HTTPSを設定してElasticsearch/Kibana/Beats/Logstashを安全に保つ | Elastic Blog

セキュリティ機能のはじめ方 | Elastic Blog

こちらの情報から、シングルノードで使っているか、クラスタで使っているかには差がありそうです。

This requirement applies to clusters with more than one node and to clusters with a single node that listens on an external interface. Single-node clusters that use a loopback interface do not have this requirement. For more information, see Encrypting communications.

Configuring security in Elasticsearch | Elasticsearch Reference [7.6] | Elastic

今回は、クラスタ構成のElasticsearchでX-Pack Securityの設定をしてみましょう。

環境

今回の環境は、こちら。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.3 LTS
Release:    18.04
Codename:   bionic


$ java --version
openjdk 11.0.6 2020-01-14
OpenJDK Runtime Environment (build 11.0.6+10-post-Ubuntu-1ubuntu118.04.1)
OpenJDK 64-Bit Server VM (build 11.0.6+10-post-Ubuntu-1ubuntu118.04.1, mixed mode, sharing)


$ curl localhost:9200
{
  "name" : "node-1",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "vl9L1QQeQiSUPzrYlFOMFw",
  "version" : {
    "number" : "7.6.0",
    "build_flavor" : "default",
    "build_type" : "deb",
    "build_hash" : "7f634e9f44834fbc12724506cc1da681b0c3b1e3",
    "build_date" : "2020-02-06T00:09:00.449973Z",
    "build_snapshot" : false,
    "lucene_version" : "8.4.0",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

Ubuntu Linux 18.04 LTS、OpenJDK 11、Elasticsearch 7.6.0です。Elasticsearchは、aptでインストールしました。

Elasticsearchは、以下の3ノードでクラスタを構成済みとしています。

$ curl localhost:9200/_cat/nodes?v
ip            heap.percent ram.percent cpu load_1m load_5m load_15m node.role master name
192.168.33.12           19          95  10    0.74    1.06     0.58 dilm      -      node-2
192.168.33.11           17          97  10    0.85    1.05     0.57 dilm      -      node-1
192.168.33.13           11          95  12    0.75    1.13     0.62 dilm      *      node-3

ノードは3つで、それぞれ以下とします。

  • node-1 … 192.168.33.11
  • node-2 … 192.168.33.12
  • node-3 … 192.168.33.13

設定は、こんな感じです。
※「node.name」はノードごとに異なります

$ sudo grep -vE '# *' /etc/elasticsearch/elasticsearch.yml
path.data: /var/lib/elasticsearch
path.logs: /var/log/elasticsearch
node.name: node-1  # 他ノードでは、node-2、node-3
network.host: 0.0.0.0
discovery.seed_hosts:
   - "192.168.33.11"
   - "192.168.33.12"
   - "192.168.33.13"
cluster.initial_master_nodes:
   - "192.168.33.11"
   - "192.168.33.12"
   - "192.168.33.13"

この環境に対して、X-Pack Securityを有効化して、最低限の設定をしていってみましょう。

X-Pack Securityを有効にする

とりあえず、X-Pack Securityを有効化しましょう。

基本的な流れは、こちら。

Configuring security in Elasticsearch | Elasticsearch Reference [7.6] | Elastic

X-Pack Securityに関する設定。

Security settings in Elasticsearch | Elasticsearch Reference [7.6] | Elastic

SSL/TLSに関する話。

Encrypting communications | Elasticsearch Reference [7.6] | Elastic

Setting up TLS on a cluster | Elasticsearch Reference [7.6] | Elastic

Encrypting communications in Elasticsearch | Elasticsearch Reference [7.6] | Elastic

まずは、node-1でelasticsearch.ymlを編集して

xpack.security.enabled: true

再起動してみます。

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

すると、なんと起動に失敗します。

ログを見てみます。

$ sudo journalctl -u elasticsearch
-- Logs begin at Sat 2020-02-15 09:19:00 UTC, end at Sat 2020-02-15 09:53:18 UTC. --
Feb 15 09:27:23 ubuntu1804.localdomain systemd[1]: Starting Elasticsearch...
Feb 15 09:27:23 ubuntu1804.localdomain elasticsearch[5845]: OpenJDK 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0 and will likely be removed
Feb 15 09:28:53 ubuntu1804.localdomain systemd[1]: Started Elasticsearch.
Feb 15 09:52:38 ubuntu1804.localdomain systemd[1]: Stopping Elasticsearch...
Feb 15 09:52:38 ubuntu1804.localdomain systemd[1]: Stopped Elasticsearch.
Feb 15 09:52:39 ubuntu1804.localdomain systemd[1]: Starting Elasticsearch...
Feb 15 09:52:40 ubuntu1804.localdomain elasticsearch[6223]: OpenJDK 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0 and will likely be removed
Feb 15 09:52:55 ubuntu1804.localdomain elasticsearch[6223]: ERROR: [1] bootstrap checks failed
Feb 15 09:52:55 ubuntu1804.localdomain elasticsearch[6223]: [1]: Transport SSL must be enabled if security is enabled on a [basic] license. Please set [xpack.security.transport
Feb 15 09:52:55 ubuntu1804.localdomain elasticsearch[6223]: ERROR: Elasticsearch did not exit normally - check the logs at /var/log/elasticsearch/elasticsearch.log
Feb 15 09:52:55 ubuntu1804.localdomain systemd[1]: elasticsearch.service: Main process exited, code=exited, status=78/n/a
Feb 15 09:52:55 ubuntu1804.localdomain systemd[1]: elasticsearch.service: Failed with result 'exit-code'.
Feb 15 09:52:55 ubuntu1804.localdomain systemd[1]: Failed to start Elasticsearch.
$ sudo cat /var/log/elasticsearch/elasticsearch.log
〜省略〜

[2020-02-15T01:52:46,687][INFO ][o.e.p.PluginsService     ] [node-1] no plugins loaded
[2020-02-15T01:52:51,564][INFO ][o.e.x.s.a.s.FileRolesStore] [node-1] parsed [0] roles from file [/etc/elasticsearch/roles.yml]
[2020-02-15T01:52:52,158][INFO ][o.e.x.m.p.l.CppLogMessageHandler] [node-1] [controller/6335] [Main.cc@110] controller (64 bit): Version 7.6.0 (Build 1c8cca13fa9631) Copyright (c) 2020 Elasticsearch BV
[2020-02-15T01:52:52,772][DEBUG][o.e.a.ActionModule       ] [node-1] Using REST wrapper from plugin org.elasticsearch.xpack.security.Security
[2020-02-15T01:52:52,946][INFO ][o.e.d.DiscoveryModule    ] [node-1] using discovery type [zen] and seed hosts providers [settings]
[2020-02-15T01:52:54,427][INFO ][o.e.n.Node               ] [node-1] initialized
[2020-02-15T01:52:54,427][INFO ][o.e.n.Node               ] [node-1] starting ...
[2020-02-15T01:52:54,639][INFO ][o.e.t.TransportService   ] [node-1] publish_address {192.168.33.11:9300}, bound_addresses {0.0.0.0:9300}
[2020-02-15T01:52:55,513][INFO ][o.e.b.BootstrapChecks    ] [node-1] bound or publishing to a non-loopback address, enforcing bootstrap checks
[2020-02-15T01:52:55,540][ERROR][o.e.b.Bootstrap          ] [node-1] node validation exception
[1] bootstrap checks failed
[1]: Transport SSL must be enabled if security is enabled on a [basic] license. Please set [xpack.security.transport.ssl.enabled] to [true] or disable security by setting [xpack.security.enabled] to [false]
[2020-02-15T01:52:55,549][INFO ][o.e.n.Node               ] [node-1] stopping ...
[2020-02-15T01:52:55,576][INFO ][o.e.n.Node               ] [node-1] stopped
[2020-02-15T01:52:55,577][INFO ][o.e.n.Node               ] [node-1] closing ...
[2020-02-15T01:52:55,597][INFO ][o.e.n.Node               ] [node-1] closed
[2020-02-15T01:52:55,600][INFO ][o.e.x.m.p.NativeController] [node-1] Native controller process has stopped - no new native processes can be starte

「Basicライセンスの場合、SSLを有効にしなさい」と言われていますね。

Transport SSL must be enabled if security is enabled on a [basic] license. Please set [xpack.security.transport.ssl.enabled] to [true] or disable security by setting [xpack.security.enabled] to [false]

というわけで、Basicライセンスでクラスタを構成している時には、SSL/TLSの設定は必須のようです。
※これ以上のライセンス(サブスクリプション)を選択すると、そうでもないみたいですが

とりあえず、各ノードのElasticsearchは停止しておきます。

$ sudo systemctl stop elasticsearch

SSL/TLSを設定する

では、ドキュメントに沿ってSSL/TLSの設定をしていきましょう。

Encrypting communications | Elasticsearch Reference [7.6] | Elastic

Tutorial: Encrypting communications | Elasticsearch Reference [7.6] | Elastic

どういうことをする必要があるかは、こちらに記載があります。

Setting up TLS on a cluster | Elasticsearch Reference [7.6] | Elastic

  • ノードごとに、秘密鍵、X.509証明書を作成する
  • トランスポート層でTLSを有効にする
    • オプションとして、HTTPレイヤーでもTLSを有効にできる(HTTPSを使える)

あとは、監視の設定や、Kibana、Logstash、Beatsなどからの接続設定を行います。このあたりは、上記ドキュメントにリンクがあるので
そちらからたどってください。今回はパスします。

まずは、CA証明書を作成(これはオプションらしいですが)。「/etc/elasticsearch」ディレクトリ配下に作成しましょう。
「--pass」は、CA証明書のパスワードです。

$ sudo /usr/share/elasticsearch/bin/elasticsearch-certutil ca --out /etc/elasticsearch/elastic-stack-ca.p12 --pass capass

「--out」および「--pass」を指定しない場合は、ファイル名とパスワードを聞かれます。

Please enter the desired output file [elastic-stack-ca.p12]: 
Enter password for elastic-stack-ca.p12 :

パスワードを指定したくない場合は、以下のように「--pass」に空文字を指定するか、対話時になにも入力せずにEnterを押します。

$ sudo /usr/share/elasticsearch/bin/elasticsearch-certutil ca --out /etc/elasticsearch/elastic-stack-ca.p12 --pass ''

ちなみに、「--out」で出力先ディレクトリを指定しない場合は、こちらにできます。

$ ls -l /usr/share/elasticsearch
total 544
drwxr-xr-x  2 root root   4096 Feb 15 09:26 bin
-rw-------  1 root root   2527 Feb 15 10:28 elastic-stack-ca.p12
drwxr-xr-x  9 root root   4096 Feb 15 09:26 jdk
drwxr-xr-x  3 root root   4096 Feb 15 09:26 lib
drwxr-xr-x 39 root root   4096 Feb 15 09:25 modules
-rw-rw-r--  1 root root 523209 Feb  6 00:20 NOTICE.txt
drwxr-xr-x  2 root root   4096 Feb  6 00:20 plugins
-rw-r--r--  1 root root   8164 Feb  6 00:19 README.asciidoc

これは微妙な場所なので、やめておこうかなと…。

その他の調整をしたかったら、ヘルプを見てオプションを設定しましょう。

$ sudo /usr/share/elasticsearch/bin/elasticsearch-certutil ca --help
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.bouncycastle.jcajce.provider.drbg.DRBG (file:/usr/share/elasticsearch/lib/tools/security-cli/bcprov-jdk15on-1.61.jar) to constructor sun.security.provider.Sun()
WARNING: Please consider reporting this to the maintainers of org.bouncycastle.jcajce.provider.drbg.DRBG
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
generate a new local certificate authority

Option               Description                                             
------               -----------                                             
-E <KeyValuePair>    Configure a setting                                     
--ca-dn              distinguished name to use for the generated ca. defaults
                       to CN=Elastic Certificate Tool Autogenerated CA       
--days <Integer>     number of days that the generated certificates are valid
-h, --help           Show help                                               
--keysize <Integer>  size in bits of RSA keys                                
--out                path to the output file that should be produced         
--pass               password for generated private keys                     
--pem                output certificates and keys in PEM format instead of   
                       PKCS#12                                               
-s, --silent         Show minimal output                                     
-v, --verbose        Show verbose output

続いて、ノードの証明書と秘密鍵を作成します。先ほど作成したCA証明書を「--ca」オプションで指定します。

$ sudo /usr/share/elasticsearch/bin/elasticsearch-certutil cert \
  --ca /etc/elasticsearch/elastic-stack-ca.p12 \
  --ca-pass capass \
  --out /etc/elasticsearch/elastic-certificates.p12 \
  --pass pass

「--ca-pass」はCA証明書のパスワード、「--out」と「--pass」は、作成する証明書のファイルパスとパスワードです。

CA証明書のパスワードと、作成する証明書の出力先のファイル名、パスワードを指定しない場合は、対話形式で聞かれます。

Enter password for CA (elastic-stack-ca.p12) : 
Please enter the desired output file [elastic-certificates.p12]: 
Enter password for elastic-certificates.p12 :

「--out」を指定しない場合の出力先は、デフォルトではやっぱりこちらです。

$ ls -l /usr/share/elasticsearch
total 548
drwxr-xr-x  2 root root   4096 Feb 15 09:26 bin
-rw-------  1 root root   3443 Feb 15 10:33 elastic-certificates.p12
-rw-------  1 root root   2527 Feb 15 10:28 elastic-stack-ca.p12
drwxr-xr-x  9 root root   4096 Feb 15 09:26 jdk
drwxr-xr-x  3 root root   4096 Feb 15 09:26 lib
drwxr-xr-x 39 root root   4096 Feb 15 09:25 modules
-rw-rw-r--  1 root root 523209 Feb  6 00:20 NOTICE.txt
drwxr-xr-x  2 root root   4096 Feb  6 00:20 plugins
-rw-r--r--  1 root root   8164 Feb  6 00:19 README.asciidoc

パスワードを指定したくない場合は、やっぱりこんな感じです(この例では、CA証明書のパスワードも空ですが)。

$ sudo /usr/share/elasticsearch/bin/elasticsearch-certutil cert \
  --ca /etc/elasticsearch/elastic-stack-ca.p12 \
  --ca-pass '' \
  --out /etc/elasticsearch/elastic-certificates.p12 \
  --pass ''

ヘルプ。

$ sudo /usr/share/elasticsearch/bin/elasticsearch-certutil cert --help
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.bouncycastle.jcajce.provider.drbg.DRBG (file:/usr/share/elasticsearch/lib/tools/security-cli/bcprov-jdk15on-1.61.jar) to constructor sun.security.provider.Sun()
WARNING: Please consider reporting this to the maintainers of org.bouncycastle.jcajce.provider.drbg.DRBG
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
generate X.509 certificates and keys

Option               Description                                             
------               -----------                                             
-E <KeyValuePair>    Configure a setting                                     
--ca                 path to an existing ca key pair (in PKCS#12 format)     
--ca-cert            path to an existing ca certificate                      
--ca-dn              distinguished name to use for the generated ca. defaults
                       to CN=Elastic Certificate Tool Autogenerated CA       
--ca-key             path to an existing ca private key                      
--ca-pass            password for an existing ca private key or the generated
                       ca private key                                        
--days <Integer>     number of days that the generated certificates are valid
--dns                comma separated DNS names                               
-h, --help           Show help                                               
--in                 file containing details of the instances in yaml format 
--ip                 comma separated IP addresses                            
--keep-ca-key        retain the CA private key for future use                
--keysize <Integer>  size in bits of RSA keys                                
--multiple           generate files for multiple instances                   
--name               name of the generated certificate                       
--out                path to the output file that should be produced         
--pass               password for generated private keys                     
--pem                output certificates and keys in PEM format instead of   
                       PKCS#12                                               
-s, --silent         Show minimal output                                     
-v, --verbose        Show verbose output             

このやり方(デフォルト)ではホスト名の情報が入っていないようなので、ホスト名を含めて検証したい場合はノードの数だけ
「--name」、「--dns」、「--ip」オプションを指定して実行するようです。

If you want to use hostname verification within your cluster, run the elasticsearch-certutil cert command once for each of your nodes and provide the --name, --dns and --ip options.

Encrypting communications in Elasticsearch | Elasticsearch Reference [7.6] | Elastic

例は、こちら。

Generate certificates | Elasticsearch Reference [7.6] | Elastic

こちらのブログエントリを見ていると、「instance.yml」というファイルを作成し、「--in」オプションでまとめて指定する方法も
あるみたいですね。

SSL、TLS、HTTPSを設定してElasticsearch/Kibana/Beats/Logstashを安全に保つ | Elastic Blog

こんな感じでファイルができたのですが、このままだとElasticsearchのプロセスから読めないので

$ sudo ls -l /etc/elasticsearch
total 48
-rw------- 1 root elasticsearch  3443 Feb 15 13:20 elastic-certificates.p12
-rw-rw---- 1 root elasticsearch   199 Feb 15 09:27 elasticsearch.keystore
-rw-rw---- 1 root elasticsearch  3338 Feb 15 10:44 elasticsearch.yml
-rw------- 1 root elasticsearch  2527 Feb 15 13:15 elastic-stack-ca.p12
-rw-rw---- 1 root elasticsearch  2373 Feb  6 00:20 jvm.options
-rw-rw---- 1 root elasticsearch 17545 Feb  6 00:20 log4j2.properties
-rw-rw---- 1 root elasticsearch   473 Feb  6 00:20 role_mapping.yml
-rw-rw---- 1 root elasticsearch   197 Feb  6 00:20 roles.yml
-rw-rw---- 1 root elasticsearch     0 Feb  6 00:20 users
-rw-rw---- 1 root elasticsearch     0 Feb  6 00:20 users_roles

読み取り権限くらい与えておきましょう。

$ sudo chmod g+r /etc/elasticsearch/elastic-certificates.p12

CA証明書の方は、Elasticsearchが直接読むことはありません。

この状態で、elasticsearch.ymlには以下のように設定します。

xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
xpack.security.transport.ssl.verification_mode: certificate
xpack.security.transport.ssl.keystore.path: elastic-certificates.p12
xpack.security.transport.ssl.keystore.password: pass
xpack.security.transport.ssl.truststore.path: elastic-certificates.p12
xpack.security.transport.ssl.truststore.password: pass

各設定の意味は、こちら。

Transport TLS/SSL Settings

Transport TLS/SSL Key and Trusted Certificate Settings

verification_modeを「certificate」にしていますが、「certificate」とするとホスト名の検証は行いません。「full」にするとホスト名の
検証まで行うようになるので、その場合は証明書にホスト名を適切に設定しましょう。

General TLS settings

「xpack.security.transport.ssl.keystore.path」とか「xpack.security.transport.ssl.truststore.path」にファイル名だけを書くと、
パッケージインストールだと「/etc/elasticsearch」ディレクトリ配下を指すみたいですね。

keystoreやtruststoreのパスワードは、平文で設定しました。

パスワードをセキュアに設定する場合は、「elasticsearch-keystore」というコマンドを使うようです。

Encrypting communications between nodes in a cluster

なお、パスワードを設定していない場合は、以下の設定自体が不要です。

xpack.security.transport.ssl.keystore.password: pass

xpack.security.transport.ssl.truststore.password: pass

今回は、パスワードを指定するパターンでいきます。

あとは、残りの2ノードの設定をします。先ほど作成した証明書(elastic-certificates.p12)を各ノードに配り、設定ファイルも同じように
設定します。
※CA証明書は不要です

$ sudo ls -l /etc/elasticsearch
total 44
-rw-r----- 1 root elasticsearch  3443 Feb 15 15:43 elastic-certificates.p12
-rw-rw---- 1 root elasticsearch   199 Feb 15 14:54 elasticsearch.keystore
-rw-rw---- 1 root elasticsearch  3446 Feb 15 15:44 elasticsearch.yml
-rw-rw---- 1 root elasticsearch  2373 Feb  6 00:20 jvm.options
-rw-rw---- 1 root elasticsearch 17545 Feb  6 00:20 log4j2.properties
-rw-rw---- 1 root elasticsearch   473 Feb  6 00:20 role_mapping.yml
-rw-rw---- 1 root elasticsearch   197 Feb  6 00:20 roles.yml
-rw-rw---- 1 root elasticsearch     0 Feb  6 00:20 users
-rw-rw---- 1 root elasticsearch     0 Feb  6 00:20 users_roles

で、Elasticsearchを起動。

$ sudo systemctl start elasticsearch

今度は、起動します。

クラスタを構成するノードの情報を見てみましょう。

$ curl localhost:9200/_cat/nodes?v
{"error":{"root_cause":[{"type":"security_exception","reason":"missing authentication credentials for REST request [/_cat/nodes?v]","header":{"WWW-Authenticate":"Basic realm=\"security\" charset=\"UTF-8\""}}],"type":"security_exception","reason":"missing authentication credentials for REST request [/_cat/nodes?v]","header":{"WWW-Authenticate":"Basic realm=\"security\" charset=\"UTF-8\""}},"status":401}

401です…。さて、認証の設定が必要ですね。

HTTPS化する

ところで、認証の設定を行う前に、先にクライアントとの通信もSSL/TLS化しておきましょう。
※こちらは、BasicライセンスでElasticsearchクラスタを構成するのに必須ではありません

基本的には、以下を見ればOKです。

Encrypting HTTP Client communications

設定は、こちら。

HTTP TLS/SSL Settings

これらの情報を元に、今回はelasticsearch.ymlに以下のように設定して
※最初、「xpack.security.http.ssl.verification_mode」を指定するのを忘れて、ホスト名の検証が有効になってしまってハマりました…

xpack.security.http.ssl.enabled: true
xpack.security.http.ssl.verification_mode: certificate
xpack.security.http.ssl.keystore.path: elastic-certificates.p12
xpack.security.http.ssl.keystore.password: pass
xpack.security.http.ssl.truststore.path: elastic-certificates.p12
xpack.security.http.ssl.truststore.password: pass

再起動します。

$ sudo systemctl restart elasticsearch

すると、HTTPでは通信できなくなりました。

$ curl localhost:9200/_cat/nodes?v
curl: (52) Empty reply from server

とりあえず、証明書を無視してHTTPSで確認。
※curlを使う場合は、証明書のフォーマットをPKCSではなくてCERTの方が良かったかも…

$ curl -k https://localhost:9200
{"error":{"root_cause":[{"type":"security_exception","reason":"missing authentication credentials for REST request [/]","header":{"WWW-Authenticate":["Bearer realm=\"security\"","ApiKey","Basic realm=\"security\" charset=\"UTF-8\""]}}],"type":"security_exception","reason":"missing authentication credentials for REST request [/]","header":{"WWW-Authenticate":["Bearer realm=\"security\"","ApiKey","Basic realm=\"security\" charset=\"UTF-8\""]}},"status":401}

Javaのクライアントで使う場合などは、証明書を設定しましょう。

Encrypted communication | Java REST Client [7.6] | Elastic

ビルトインユーザーの認証設定を行う

では、ビルドインユーザーの設定を行います。「elasticsearch-setup-passwords」コマンドを使うのですが、今回はパスワードは
自動生成するようにしてみましょう。

$ sudo /usr/share/elasticsearch/bin/elasticsearch-setup-passwords auto -u https://localhost:9200

「elastic」ユーザーのパスワードは、こんな感じになりました。

Changed password for user elastic
PASSWORD elastic = CgnAS1lSGZ3KmMwVTspZ

このパスワードを使って、elasticユーザーでアクセス。

$ curl -k -u elastic:CgnAS1lSGZ3KmMwVTspZ https://localhost:9200/_cat/nodes?v
ip            heap.percent ram.percent cpu load_1m load_5m load_15m node.role master name
192.168.33.12            9          95   0    0.16    0.19     0.11 dilm      -      node-2
192.168.33.11            9          92   1    0.23    0.24     0.14 dilm      -      node-1
192.168.33.13            8          96   1    0.22    0.23     0.13 dilm      *      node-3

今度は、アクセスできるようになりましたね。

あとは、このユーザーを使ってElasticsearchへユーザーを追加していったりすればいいので、こちらのロールやユーザーの作成方法に
したがっていけばいいでしょう。

Configuring security in Elasticsearch | Elasticsearch Reference [7.6] | Elastic

これについては、シングルノードの時にも行っています。

Elasticsearch(シングルノード)で、認証・認可設定をする - CLOVER🍀

とりあえず、今回はここまでかな、と。

まとめ

Elasticsearchクラスタに関して、ノード間の通信および、REST APIへアクセスするクライアントに対してSSL/TLSを有効にしてみました。

だいぶてこずったのですが、なんとか設定できるところまでたどりついて良かったです…。

MarshallingがリファクタリングされたInfinispanを、Hot Rodで試す

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

Infinispan 10.0で、Marshallingまわりがリファクタリングされ、ProtoStream(Protocol Buffers 2)がデフォルトのMarshalingの
仕組みになったということを、以前書きました。
※それまでは、JBoss Marshallingがデフォルトでした

Blog: Infinispan 10.0.0.Final - Infinispan

Infinispan 10.0でMarshallingがリファクタリングされたという話(Embedded Mode) - CLOVER🍀

なんですけど、この時はEmbeddedだけ試していて、Hot Rodの方を完全に忘れていたので、後にQuarkusで遊ぶ時に
「あれ?これやってなかったな?」という気になりました…。

ProtoStreamが使えるようになった、Quarkus+Infinispan Client(Hot Rod) Extensionを試す - CLOVER🍀

というわけで、今回はシンプルにHot Rodのみでやってみることにしました。

環境

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

$ java --version
openjdk 11.0.6 2020-01-14
OpenJDK Runtime Environment (build 11.0.6+10-post-Ubuntu-1ubuntu118.04.1)
OpenJDK 64-Bit Server VM (build 11.0.6+10-post-Ubuntu-1ubuntu118.04.1, mixed mode, sharing)


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

Infinispan Serverは10.1.1.Finalを使用し、動作しているIPアドレスは172.17.0.2とします。

お題

基本的には、この時と同じで。

Infinispan 10.0でMarshallingがリファクタリングされたという話(Embedded Mode) - CLOVER🍀

RemoteCacheのエントリとして、String、そしてユーザー定義クラスを使った時の差を見てみたいと思います。

ユーザー定義クラスの方のお題は、書籍で。

準備

Maven依存関係は、こちら。

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

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.6.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.6.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.11.1</version>
            <scope>test</scope>
        </dependency>

JUnit 5とAssertJは、テストコード用です。

JUnit 5があるので、Maven Surefire Pluginのバージョンも指定しておきます。

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

では、ソースコードを書いていきます。

サンプルコードを書きつつ、動作確認

まず、テストコードの雛形から。
src/test/java/org/littlewings/infinispan/marshalling/MarshallingTest.java

package org.littlewings.infinispan.marshalling;

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.configuration.Configuration;
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

public class MarshallingTest {
    @BeforeEach
    public void setUp() {
        Configuration configuration =
                new ConfigurationBuilder()
                        .addServers("172.17.0.2:11222")
                        .build();

        RemoteCacheManager manager = new RemoteCacheManager(configuration);

        manager.getCache("simpleCache").clear();
        manager.getCache("bookCache").clear();
    }

    <K, V> void withCache(String cacheName, Consumer<RemoteCache<K, V>> consumer) {
        Configuration configuration =
                new ConfigurationBuilder()
                        .addServers("172.17.0.2:11222")
                        .build();

        RemoteCacheManager manager = new RemoteCacheManager(configuration);

        RemoteCache<K, V> cache = manager.getCache(cacheName);

        try {
            consumer.accept(cache);
        } finally {
            cache.stop();
            manager.stop();
        }
    }

    // ここに、テストを書く!
}

Cacheは、「simpleCache」と「bookCache」という2つのものを使い、テストごとにデータを空っぽにします。

利用するCacheは、Infinispan Server側にあらかじめ作成しておきます。

ispn@infinispan-server:/opt/infinispan-server$ bin/cli.sh
[disconnected]> connect
[infinispan-server-17735@cluster//containers/default]> 
[infinispan-server-17735@cluster//containers/default]> create cache --template=org.infinispan.DIST_SYNC simpleCache
[infinispan-server-17735@cluster//containers/default]> create cache --template=org.infinispan.DIST_SYNC bookCache

では、テストケースを書きましょう。

まずは、Stringを使った単純なケース。

    @Test
    public void simpleCase() {
        this.<String, String>withCache("simpleCache", cache -> {
            cache.put("key1", "value1");
            cache.put("key2", "value2");
            cache.put("key3", "value3");

            assertThat(cache.get("key1")).isEqualTo("value1");
            assertThat(cache.get("key2")).isEqualTo("value2");
            assertThat(cache.get("key3")).isEqualTo("value3");
        });
    }

こちらは、特に問題なく動作します。

次に、ユーザー定義クラスを作成。お題は、書籍で。
src/test/java/org/littlewings/infinispan/marshalling/Book.java

package org.littlewings.infinispan.marshalling;

import java.io.Serializable;

public class Book implements Serializable {
    private static final long serialVersionUID = 1L;

    String isbn;

    String title;

    int price;

    public static Book create(String isbn, String title, int price) {
        Book book = new Book();

        book.setIsbn(isbn);
        book.setTitle(title);
        book.setPrice(price);

        return book;
    }

    // getter/setterは省略
}

Serializableなクラスとします。

で、このテストケースを実行すると、Marshallerがないと言われてエラーになります。

java.lang.IllegalArgumentException: No marshaller registered for Java type org.littlewings.infinispan.marshalling.Book
    at org.littlewings.infinispan.marshalling.MarshallingTest.lambda$useDefinedClassCase$1(MarshallingTest.java:72)
    at org.littlewings.infinispan.marshalling.MarshallingTest.lambda$useDefinedClassCase$2(MarshallingTest.java:72)
    at org.littlewings.infinispan.marshalling.MarshallingTest.withCache(MarshallingTest.java:41)
    at org.littlewings.infinispan.marshalling.MarshallingTest.useDefinedClassCase(MarshallingTest.java:63)

というわけでHot Rodを使った場合でも、ProtoStream用のMarshallerをなんとか用意しないといけないわけですね。

ドキュメントを見る

では、どうするか…はEmbeddedを見ているのでだいたいわかるのですが、ちょっとドキュメントを探しましょう。

EmbeddedのMarshallingまわりのドキュメント。

Marshalling

ProtoStream (Default)

StringがMarshallingでエラーにならないのは、デフォルトでStringやIntegerなどに対するMarsharinngが考慮されているからですね。

Usage

で、Embedded Modeの時はシリアライズ対象のクラスにProtoStreamが提供するアノテーションを付与し、
SerializationContextInitializerインターフェースのサブインターフェースを作成することになります。

結論から言うと、Hot Rodでも同じようなことをします。

Marshalling data

ProtoStream

まず、先ほどのBookクラスのフィールド(またはgetter)に、@ProtoFieldアノテーションを指定します。
src/test/java/org/littlewings/infinispan/marshalling/Book.java

package org.littlewings.infinispan.marshalling;

import org.infinispan.protostream.annotations.ProtoFactory;
import org.infinispan.protostream.annotations.ProtoField;

public class Book {
    @ProtoField(number = 1, required = true)
    String isbn;

    @ProtoField(number = 2, required = true)
    String title;

    @ProtoField(number = 3, required = true, defaultValue = "0")
    int price;

    @ProtoFactory
    public static Book create(String isbn, String title, int price) {
        Book book = new Book();

        book.setIsbn(isbn);
        book.setTitle(title);
        book.setPrice(price);

        return book;
    }

    // getter/setterは省略
}

@ProtoFactoryについては、必要に応じてコンストラクタやファクトリメソッドに付けましょう。

続いて、SerializationContextInitializerインターフェースのサブインターフェースを作成します。

Pluggable Annotation Processing APIを使うので、Maven依存関係を追加します。

        <dependency>
            <groupId>org.infinispan.protostream</groupId>
            <artifactId>protostream-processor</artifactId>
            <version>4.3.2.Final</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
            <optional>true</optional>
        </dependency>

両方ともoptionalで良いでしょう。

作成したSerializationContextInitializerインターフェースを拡張したインターフェース。
src/test/java/org/littlewings/infinispan/marshalling/BookContextInitializer.java

package org.littlewings.infinispan.marshalling;

import org.infinispan.protostream.SerializationContextInitializer;
import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder;

@AutoProtoSchemaBuilder(
        includeClasses = {Book.class},
        schemaFileName = "book.proto",
        schemaFilePath = "proto/",
        schemaPackageName = "sample")
public interface BookContextInitializer extends SerializationContextInitializer {
}

まあ、@AutoProtoSchemaBuilderアノテーションを使いたいがために存在するのですが…。

Protocol Buffersのスキーマ定義を生成する対象のクラスとパッケージ名、ファイルの情報を指定します。また、合わせてMarshallerの
実装や、SerializationContextInitializerの実装も生成されます。

ビルドすると、こんな感じでMarshallerやSerializationContextInitializerの実装、Protocol Buffersのスキーマ定義が生成されたことが
確認できます。

$ find target/test-classes -type f
target/test-classes/proto/book.proto
target/test-classes/org/littlewings/infinispan/marshalling/MarshallingTest.class
target/test-classes/org/littlewings/infinispan/marshalling/BookContextInitializer.class
target/test-classes/org/littlewings/infinispan/marshalling/Book$___Marshaller_abf7b4d69b46e47579a90635aad0268de3c98b409eef1e9d735fea0f6d115580.class
target/test-classes/org/littlewings/infinispan/marshalling/BookContextInitializerImpl.class
target/test-classes/org/littlewings/infinispan/marshalling/Book.class

ちなみに、生成されたProtocol Bufferesのスキーマ定義はこんな感じになります。
target/test-classes/proto/book.proto

// File name: book.proto
// Generated from : org.littlewings.infinispan.marshalling.BookContextInitializer

syntax = "proto2";

package sample;



message Book {
   
   required string isbn = 1;
   
   required string title = 2;
   
   required int32 price = 3 [default = 0];
}

あとは、RemoteCacheManagerを作成する際のConfigurationに対して、自動生成されたSerializationContextInitializerのインスタンスを
指定してあげればOKです。

    <K, V> void withCache(String cacheName, Consumer<RemoteCache<K, V>> consumer) {
        Configuration configuration =
                new ConfigurationBuilder()
                        .addServers("172.17.0.2:11222")
                        .addContextInitializer(new BookContextInitializerImpl())
                        .build();

        RemoteCacheManager manager = new RemoteCacheManager(configuration);

        RemoteCache<K, V> cache = manager.getCache(cacheName);

        try {
            consumer.accept(cache);
        } finally {
            cache.stop();
            manager.stop();
        }
    }

クラスのFQCNをStringで渡す、でも可です。

        Configuration configuration =
                new ConfigurationBuilder()
                        .addServers("172.17.0.2:11222")
                        //.addContextInitializer(new BookContextInitializerImpl())
                        .addContextInitializer("org.littlewings.infinispan.marshalling.BookContextInitializerImpl")
                        .build();

今回は使っていませんが、hotrod-client.propertiesファイルを使う場合は、以下のプロパティを使うことになるようです。

infinispan.client.hotrod.context-initializers=org.littlewings.infinispan.marshalling.BookContextInitializerImpl

https://github.com/infinispan/infinispan/blob/10.1.1.Final/client/hotrod-client/src/main/java/org/infinispan/client/hotrod/impl/ConfigurationProperties.java#L29

ここまで修正すれば、ユーザー定義クラスを使ったMarshallingが成功するようになります。

まとめ

今回は、InfinispanのデフォルトのMarshallingの仕組みが変わったことを、Hot Rod Clientを使って動作確認してみました。

1度Embeddedで試していたのでそれほど困らなかったのですが、あえて言えば自動生成されたSerializationContextInitializerインターフェースの
実装を、どうやって指定させるのか?くらいですね。

幸い、ドキュメントに記載がありましたが。

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

https://github.com/kazuhira-r/infinispan-getting-started/tree/master/remote-without-jboss-marshalling