ElasticsearchをソースからビルドしてIntelliJを使ってデバッグ実行する

どうも、Elasticのコンサルタントの杉森といいます。Qiitaでははじめての記事です。お手柔らかにお願いします。

この記事ではエンジニアならば誰しもがやってみようかと思っているけど面倒で後回しになっているであろう作業、つまりElasticsearchをソースからビルドしてIntelliJでデバッグする、というところを紹介したいと思います。難しいことは何もないですが、初回ビルドにはそこそこ時間かかりますので、そこはご承知ください。

特にIntelliJについてはあんまり使い慣れていない私のような人を想定した記事です。ベテランの人は暖かく見守って下さい。

## 前提

この記事の前提は以下とします。

- Macのローカルでビルド（IntelMac, macOS Montereyで確認しています）
- JavaはSDKManでインストールしたJDK17をつかう
- IntelliJ IDEA 2022.1.2 (Community Edition)
- mainブランチのHEADからビルド（執筆時点ではElasticsearchバージョン8.4）

[2024-02-27 追記]

以下の環境でも確認しています。

- Apple M3 Pro, macOS 14.3.1（23D60）
- openjdk 17.0.9 2023-10-17 LTS
- IntelliJ IDEA 2023.3.2 (Community Edition)
- 8.14.0-SNAPSHOT (b4b32aa53a53975d1540dfc37c985729d622c6b6)

## JDKインストール

今回はJDKのインストールにはSDKManを利用します。[SDKManの公式サイトのインストラクション](https://sdkman.io/install)に従うと:

```sh
$ curl -s "https://get.sdkman.io" | bash
```

でインストールできます。

Elasticsearch 8系ではJDK 17以降が必要です。SDKManでインストールできるJavaを探します。

```sh
$ sdk list java
```

個人的におすすめのディストリビューションというのはないので、今回は単に私が使ったことがあるものということで17.0.3-zuluをインストールします。

```sh
$ sdk install java 17.0.3-zulu
```

きちんとインストールできたか確認します。

```sh
$ which java
/Users/daixque/.sdkman/candidates/java/current/bin/java
$ java -version
openjdk version "17.0.3" 2022-04-19 LTS
OpenJDK Runtime Environment Zulu17.34+19-CA (build 17.0.3+7-LTS)
OpenJDK 64-Bit Server VM Zulu17.34+19-CA (build 17.0.3+7-LTS, mixed mode, sharing)
```

大丈夫そうですね。


## Elasticsearchのビルド

そうしたら[Elasticsearchのコード](https://github.com/elastic/elasticsearch)をダウンロードしてきてビルドします。ビルド方法などは[README.asciidoc](https://github.com/elastic/elasticsearch/blob/master/README.asciidoc)や[CONTRIBUTING.md](https://github.com/elastic/elasticsearch/blob/master/CONTRIBUTING.md)に書いてあります。

まずはコードをGithubからcloneします。大きいコードベースなのでそれなりに時間かかります（環境にもよるでしょうが10分くらい？）。
```sh
$ git clone git@github.com:elastic/elasticsearch.git
```

Elasticでは少し前にmasterからmainに開発のメインブランチを移行しました。cloneできたらmainブランチをチェックアウトしましょう。

```sh
$ cd elasticsearch
$ git checkout -b main origin/main
```

ちなみに本記事執筆時点でのmainブランチの先頭コミットは d7b6a32d666f6232d8c4a0ce4e089b270e84e0c1 でした。

ではビルドします。お分かりのとおり初回は依存関係のライブラリをかき集めるためかなり時間がかかります（30分くらい？）ので、気長に行きましょう。

（ただ、後の説明するようにIntelliJを使ってもビルド可能です。ライブラリのダウンロード時間などを短くしたい場合、[IntelliJにインポートしてデバッグ実行](#IntelliJにインポートしてデバッグ実行)までスキップしても大丈夫です。）

README.asciidocにある通り、以下のコマンドを実行します。

```sh
$ ./gradlew localDistro
```

ビルドできたら以下のコマンドで起動します。

```sh
$ ./gradlew run
```

別のターミナルから以下のコマンドで接続を確認しましょう。gradleで起動するとBASIC認証がかかっていますが、ユーザー名`elastic`、パスワード`password`でアクセスできます。

```sh
$ curl elastic:password@localhost:9200
```

うまく行っていればレスポンスが以下のように出力されるはずです。

```json
{
  "name" : "runTask-0",
  "cluster_name" : "runTask",
  "cluster_uuid" : "ZaPn71FLQbyu3HpbCgW3YQ",
  "version" : {
    "number" : "8.4.0-SNAPSHOT",
    "build_flavor" : "default",
    "build_type" : "tar",
    "build_hash" : "d7b6a32d666f6232d8c4a0ce4e089b270e84e0c1",
    "build_date" : "2022-07-08T14:25:14.712327Z",
    "build_snapshot" : true,
    "lucene_version" : "9.3.0",
    "minimum_wire_compatibility_version" : "7.17.0",
    "minimum_index_compatibility_version" : "7.0.0"
  },
  "tagline" : "You Know, for Search"
}
```

無事にElasticsearchがビルドできました。特に難しいところはないですね。


## IntelliJにインポートしてデバッグ実行

それでは今ビルドしたElasticsearchのコードベースをIntelliJにインポートしますが、詳しい手順は[Elasticが公式のブログ記事](https://www.elastic.co/jp/blog/how-to-debug-elasticsearch-source-code-in-intellij-idea)を公開しているのでそちらも参照してください。

IntelliJのOpenでelasticsearchのフォルダを選択して開いてください。

![プロジェクトを開く](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628554/9a27bdc5-5880-1837-08bf-802d679154f9.png)

すると自動的に依存関係のインポート、コンパイルの処理が始まります。またバイナリのビルドについてもコマンドラインから直接gradlewでのビルドはせず、IntelliJ側のGradleでlocalDistroタスクを実行することももちろん可能です。

![インポートが始まる](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628554/00d42b6f-0d3d-3903-671c-00a10d2cf07b.png)

インポートが終わったら、Run/Debug Configurationで、デバッグ実行の設定をします。

![デバッグ設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628554/8f6bccb3-8134-f055-0a89-7151bcb80b4c.png)

基本的にプロジェクトをインポートしたタイミングでIntelliJが適切に設定してくれているはずです。私の環境では以下のような状態になっていて、このままでOKでした。ポイントはDebugger Modeが「Listen to remote JVN」、Auto restartにチェックが入っている状態になっていることです。

![ビルド設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628554/072a1f6a-30a9-64c6-1ee9-62593d7dbea1.png)

そうしたら、まずはIntelliJ側でデバッグ実行します。

![デバッグ実行](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628554/0c89f285-0d80-1b5e-0091-37a122311cac.png)

この時点ではIntelliJ側では特に何も起こりません。

次にコマンドラインからElasticsearchをデバッグモードで起動します。

```sh
$ ./gradlew run --debug-jvm
```

IntelliJ側のデバッグが実行されていないと、この起動に失敗しますのでIntelliJ側をデバッグ開始してから再度gradlewを実行してください。うまく行けば以下のようなメッセージがIntelliJのデバッグコンソールに出力されます。

```
Connected to the target VM, address: 'localhost:5007', transport: 'socket'
```

それではブレークポイントを打って実際に処理の流れを追ってみましょう。
今回は`org.elasticsearch.rest.action.RestMainAction#convertMainResponse`メソッドにブレークポイントを打ってみます。

![Break Point](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628554/eb95f975-19b7-e609-bc72-02e18941d9e7.png)

ここで再度以下のAPIを叩いてみましょう。

```sh
curl elastic:password@localhost:9200
```

![Break Point](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628554/b06415ad-499f-1d78-8be3-f216d2c90977.png)

おお、実際に処理が止まって内部の変数の状態などが参照できますね。ここまでできれば、多くのケースで十分内部の挙動を追えるようになったのではないでしょうか。


## ビルド済みのモジュールをIntelliJにアタッチ

これでElasticsearchの挙動をソースレベルで確認したい場合、大体のことはOKだと思います。ただ、設定ファイル周りについて調べているとき、デバッグ実行で読み込んでいるelasticsearch.ymlをどうやって変更するか現時点で理解していなかったので、今回はビルド済みのモジュールをデバッグ実行で起動してIntelliJにアタッチしてみましょう。

ここからは通常のElasticsearchのバイナリ版と同じ操作になりますので、[公式のインストール手順](https://www.elastic.co/guide/en/elasticsearch/reference/current/targz.html)なども参考に進めてください。

ビルドしたモジュールは以下に作成されます。

```
build/distribution/local/elasticsearch-8.4.0-SNAPSHOT
```

フォルダの内容を確認します。

```sh
$ ls
LICENSE.txt     NOTICE.txt      README.asciidoc bin             config          jdk.app         lib             logs            modules         plugins
```

設定ファイルはconfig/elasticsearch.ymlですね。必要に応じて修正して下さい。今回はローカルデバッグを目的としているので、セキュリティには気を使わず簡単に動作確認できるように、HTTPSの暗号化なしで接続できるようにします。

```yml
xpack.security.http.ssl:
  enabled: false
```

さて、IntelliJからアタッチするためにJVMの起動オプションを追加する必要があります。Run/Debug Configurationのダイアログに表示されているサンプルを参照して、`config/jvm.options`ファイルに以下のような行を追記します。アドレスは各環境によって違いますので注意してください。

```ini
-agentlib:jdwp=transport=dt_socket,server=n,address=192.168.1.8:5007,suspend=y
```

そうしたら以下のコマンドでElasticsearchを起動しましょう。

```
$ bin/elasticsearch
```

うまく起動できると、以下のような情報がログに出力されます。このビルドを使うにあたっては必要な情報なのでメモしておいて下さい。（なくしても[bin以下のツール](https://www.elastic.co/guide/en/elasticsearch/reference/current/commands.html)を使って復旧はできます。）

```
✅ Elasticsearch security features have been automatically configured!
✅ Authentication is enabled and cluster connections are encrypted.

ℹ️  Password for the elastic user (reset with `bin/elasticsearch-reset-password -u elastic`):
  YX91nt7z*pp4LhTJy36R

ℹ️  HTTP CA certificate SHA-256 fingerprint:
  10c57bd1987ab2f9144358e34456007b6fd08f3cd9614cd05b9d06df901c785a

ℹ️  Configure Kibana to use this cluster:
• Run Kibana and click the configuration link in the terminal when Kibana starts.
• Copy the following enrollment token and paste it into Kibana in your browser (valid for the next 30 minutes):
  eyJ2ZXIiOiI4LjQuMCIsImFkciI6WyIxOTIuMTY4LjE0Ny4zNzo5MjAwIl0sImZnciI6IjEwYzU3YmQxOTg3YWIyZjkxNDQzNThlMzQ0NTYwMDdiNmZkMDhmM2NkOTYxNGNkMDViOWQwNmRmOTAxYzc4NWEiLCJrZXkiOiJIWi1POVlFQk9Ga18tUl8zdkwzczpPQXZOSkltUlRZR2RWeGFjZlJoN3RRIn0=

ℹ️  Configure other nodes to join this cluster:
• On this node:
  ⁃ Create an enrollment token with `bin/elasticsearch-create-enrollment-token -s node`.
  ⁃ Uncomment the transport.host setting at the end of config/elasticsearch.yml.
  ⁃ Restart Elasticsearch.
• On other nodes:
  ⁃ Start Elasticsearch with `bin/elasticsearch --enrollment-token <token>`, using the enrollment token that you generated.
```

もちろんこの情報は本来セキュアなものです。今回は一時的なビルド用なのでオープンにしていますが、本番環境の情報は厳密に管理して下さいね。ではこのユーザーでAPIにアクセスしてみます。

```sh
$ curl -u "elastic:YX91nt7z*pp4LhTJy36R" localhost:9200
```

うまく動きますね。ブレークポイントが打ってあればそこできちんと処理をトレースできるはずです。`curl: (52) Empty reply from server`のようなエラーが出力される場合はHTTPSを無効化しているか確認してください。

お疲れ様でした！　これでいつでもElasticsearchをデバッグ実行できるので、バグを踏んでも直してPRが投げれますね！　ちなみに本当にPR投げる際は[CONTRIBUTING.md](https://github.com/elastic/elasticsearch/blob/master/CONTRIBUTING.md)を参照して下さい。


## Trialライセンスの設定

デバッグ実行すると、ElasticsearchはデフォルトでBasicライセンスを適用して起動します。上位のライセンスで利用する機能を開発したい場合は、トライアルライセンスで起動させることができます。以下のように` -Dtests.es.xpack.license.self_generated.type=trial`オプションを指定してください。

```
$ ./gradlew run --debug-jvm -Dtests.es.xpack.license.self_generated.type=trial
```

_licenseエンドポイントで正しくライセンスが適用されたか確認しましょう。

```
$ curl -s elastic:password@localhost:9200/_license
```

正しく設定できていれば以下のようにレスポンスのtypeがtrialになります。
```
{
  "license" : {
    "status" : "active",
    "uid" : "722cb3e9-8f9d-4898-a1c1-5b59e12aae46",
    "type" : "trial",
    "issue_date" : "2024-02-27T09:22:47.833Z",
    "issue_date_in_millis" : 1709025767833,
    "expiry_date" : "2024-03-28T09:22:47.833Z",
    "expiry_date_in_millis" : 1711617767833,
    "max_nodes" : 1000,
    "max_resource_units" : null,
    "issued_to" : "runTask",
    "issuer" : "elasticsearch",
    "start_date_in_millis" : -1
  }
}
```

すでに立ち上げてしまっているサーバーについては、[Start trial API](https://www.elastic.co/guide/en/elasticsearch/reference/current/start-trial.html)で後から適用することもできます。

```
$ curl -u "elastic:password" -X POST "localhost:9200/_license/start_trial?acknowledge=true"
```

## TIPS

その他どちらかと言うと個人的なメモですが、GradleでのUnitテスト実行など開発チームから教わったコマンドなどを残しておきます。参考まで。

```sh
# Precommit and spotless apply ensures the code is correctly formatted
$ ./gradlew :x-pack:plugin:core:spotlessApply :x-pack:plugin:core:precommit :x-pack:plugin:ml:spotlessApply :x-pack:plugin:ml:precommit

# Run the XPack core unit tests. Covers ml configuration classes
$ ./gradlew :x-pack:plugin:core:test

# And the ml unit tests
$ ./gradlew :x-pack:plugin:ml:test

# Checkstyle test
$ ./gradlew :x-pack:plugin:core:checkstyleTest
```

Gitでコードベースをクリーンするコマンド

```sh
$ git clean -dXf
```


## (参考) Trouble shooting

一度ビルド中に以下のようなエラーが出るようになってしまう現象がありました。

```
Execution failed for task ':build-tools-internal:jar'.
> Entry org/elasticsearch/gradle/internal/AntFixtureStop$_setFixture_closure1.class is a duplicate but no duplicate handling strategy has been set. Please refer to https://docs.gradle.org/7.0.2/dsl/org.gradle.api.tasks.Copy.html#org.gradle.api.tasks.Copy:duplicatesStrategy for details.
...
```

こうなった時は`build-tools-internal/build`ディレクトリを削除するとまたビルドできるようになるようです。