Elasticsearch에 Java Low Level REST Client를 통해 요청 보내기

허석진·2023년 12월 17일
0

Elasticsearch

목록 보기
2/5
post-thumbnail
  1. 사용하는 elasticsearch와 kibana의 버전은 8.11.1입니다
    1-1. 8.x 버전 끼리는 크게 차이 없을 것으로 예상되나 이외의 버전에서 진행에 문제가 생긴다면 반드시 검색을 통해 확인해 봐야합니다
  2. 사용하는 Spring Boot의 버전은 3.1.2입니다
  3. 사용하는 Java의 버전은 17입니다

Java Low Level REST Client의 특징

링크: Java Low Level REST Client
링크: Introduction

  1. 최소한의 의존성
  2. 모든 사용 가능한 노드 간의 부하 분산
  3. 노드 장애 시 장애 극복 및 특정 응답 코드에 대한 재시도
  4. 실패한 연결에 대한 penalization
    • 실패한 노드를 재시도할지 여부는 연속적으로 실패한 횟수에 따라 달라지며, 실패 시도가 늘어날수록 클라이언트는 동일한 노드를 다시 시도하기 전에 더 긴 대기 시간을 갖게 됨
  5. 지속적인 연결
  6. 요청 및 응답의 추적 로깅
  7. 클러스터 노드의 자동 검색
  8. 강력한 형식화된 요청 및 응답
  9. 모든 API의 블로킹 및 비동기 버전
  10. 복잡한 중첩 구조를 생성할 때 간결하면서도 가독성 있는 코드를 작성할 수 있도록 하는 플루언트 빌더 및 함수 패턴 사용
  11. Jackson 또는 JSON-B와 같은 객체 매퍼를 사용하여 애플리케이션 클래스의 원활한 통합
  12. HTTP 연결 풀링, 재시도, 노드 검색 등과 같은 모든 전송 수준 문제를 처리하는 Java Low Level REST Client와 같은 HTTP 클라이언트에 프로토콜 처리를 위임

위는 Elasticsearch 공식문서에 적혀있는 Java Low Level REST Client 특징들이다.
그냥 한번 쓱보고 그런갑다하고 넘어가겠다.
이런 내용은 사실 관련 툴을 여러개 사용할 사람들이 툴 선택 하기전에 고려해는 사항이기에

요구사항

링크: Elasticsearch Java API Client [8.11] - Getting started

  • Java 버전 8 이상
  • Elasticsearch API와 원활하게 통합할 수 있게 해주는 JSON 객체 매핑 라이브러리

Installation (의존성 설치)

Installation in a Gradle project by using Jackson

dependencies {
    implementation 'co.elastic.clients:elasticsearch-java:8.11.1'
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.7.1'
}

Installation in a Maven project by using Jackson

<project>
  <dependencies>

    <dependency>
      <groupId>co.elastic.clients</groupId>
      <artifactId>elasticsearch-java</artifactId>
      <version>8.11.1</version>
    </dependency>

    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.12.7.1</version>
    </dependency>

  </dependencies>
</project>

Spring 사용시 발생할 수 있는 오류

링크: Spring Boot 3.1 cannot use Elasticsearch Client 8.9.0
링크: Caused by: java.lang.NoSuchMethodError: ‘void co.elastic.clients.transport.rest_client.RestClientTransport.
Spring Boot 3.xElasticsearch 8.9.x+를 함께 사용하면 다음과 같은 오류가 발생한다.

***************************
APPLICATION FAILED TO START
***************************

Description:

An attempt was made to call a method that does not exist. The attempt was made from the following location:

    org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchClientConfigurations$ElasticsearchTransportConfiguration.restClientTransport(ElasticsearchClientConfigurations.java:91)

The following method did not exist:

    'void co.elastic.clients.transport.rest_client.RestClientTransport.<init>(org.elasticsearch.client.RestClient, co.elastic.clients.json.JsonpMapper, co.elastic.clients.transport.TransportOptions)'

The calling method's class, org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchClientConfigurations$ElasticsearchTransportConfiguration, was loaded from the following location:

    jar:file:/C:/Users/User/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/3.1.2/spring-boot-autoconfigure-3.1.2.jar!/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchClientConfigurations$ElasticsearchTransportConfiguration.class

The called method's class, co.elastic.clients.transport.rest_client.RestClientTransport, is available from the following locations:

    jar:file:/C:/Users/User/.m2/repository/co/elastic/clients/elasticsearch-java/8.9.0/elasticsearch-java-8.9.0.jar!/co/elastic/clients/transport/rest_client/RestClientTransport.class

The called method's class hierarchy was loaded from the following locations:

    co.elastic.clients.transport.rest_client.RestClientTransport: file:/C:/Users/User/.m2/repository/co/elastic/clients/elasticsearch-java/8.9.0/elasticsearch-java-8.9.0.jar
    co.elastic.clients.transport.ElasticsearchTransportBase: file:/C:/Users/User/.m2/repository/co/elastic/clients/elasticsearch-java/8.9.0/elasticsearch-java-8.9.0.jar


Action:

Correct the classpath of your application so that it contains compatible versions of the classes org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchClientConfigurations$ElasticsearchTransportConfiguration and co.elastic.clients.transport.rest_client.RestClientTransport

Spring Boot에서 ElasticsearchClientConfigurations를 자동 설정하는 도중, void co.elastic.clients.transport.rest_client.RestClientTransport.<init>(org.elasticsearch.client.RestClient, co.elastic.clients.json.JsonpMapper, co.elastic.clients.transport.TransportOptions) 메소드가 2곳에서 호출되어 충돌을 일으킨다.
한 곳은 spring boot과 한 곳은 elastic client인데 둘 다 사용해야하는 사람들에게는 머리아픈 얘기가 될 수 밖에 없다.

거기서 해결책을 위에 링크 중 GitHub issue에서 발견할 수 있는데 아예 사용자가 해당 생성자를 새로 정의하여 우선권을 가져가, 충돌을 막아버리는 것이다.
해당 이슈에서 2명의 기여자 중 한 분이 제공한 코드를 아래에 적어두겠다.

@Bean
RestClientTransport restClientTransport(RestClient restClient, ObjectProvider<RestClientOptions> restClientOptions) {
    return new RestClientTransport(restClient, new JacksonJsonpMapper(), restClientOptions.getIfAvailable());
}

elasticseasrch API key 생성하기

Java Low Level REST Client를 이용하기 위해서는 이용할 clusterindex, 권한 범위등을 정의한 API key 생성하고, 이용해야한다.
생성하는 방법은 2가지인데 아래중에 하나를 선택해서 하면된다.

API 요청으로 생성하기

링크: Create API key API
위에 링크를 보면 API key를 생성하는 API에 대해 설명이되어 있는데 그 문서를 참고하면 될 것 같다.
필자는 books라는 인덱스에 모든 권한을 사용하길 원해서 아래와 같은 요청을 보냈다.

PUT /_security/api_key
{
  "name": "my-api-key",
  "role_descriptors": {
    "superuser": {	# 역할 명
      "cluster": [	# 적용할 클러스터
        "all"
      ],
      "indices": [	# 적용할 인덱스
        {
          "names": [
            "books"
          ],
          "privileges": [	# 적용할 권한
            "all"
          ],
          "allow_restricted_indices": false
        }
      ],
      "run_as": [
        "*"
      ]
    }
  }
}

요청을 보내면 응답이 아래와 같이 오게된다.

{
  "id": "Z2qzd4wBAmBbbc-_wi0F",
  "name": "my-api-key",
  "api_key": "NveKRHFqSGeK1yBqUF5aDg",
  "encoded": "WjJxemQ0d0JBbUJiYmMtX3dpMEY6TnZlS1JIRnFTR2VLMXlCcVVGNWFEZw=="
}

여기서 api_keyencoded는 나중에 확인이 불가능하니 잘 적어둬야한다.

Kibana를 통해 생성하기

링크: API Keys
Main Menu에서 Management > Stack Management > Security > API Keys 순서로 들어가주면 바로 확인할 수 있다.
Kibana API Keys Menu

Kibana Create API Key Button

Kibana Create Api Key

입력한 json은 API 요청을 통해 생성할 때와 거의 동일하다.

{
  "superuser": {
    "cluster": [
      "all"
    ],
    "indices": [
      {
        "names": [
          "books"
        ],
        "privileges": [
          "all"
        ],
        "allow_restricted_indices": false
      }
    ],
    "run_as": [
      "*"
    ]
  }
}

그러면 아래와 같은 화면으로 넘어가는데 Encoded는 다시는 볼 수 없으니 반드시 어디에 메모해둬한다.

Kibana API Keys encoded

Java Low Level REST Client 연결 설정하기

API Key 설정하기

링크: Elasticsearch Java API Client [8.11] > Setup > Connecting
링크: Elasticsearch Java API Client [8.11] › Java Low Level REST Client › Common configuration > Other authentication methods

공식 문서가 사원의 구슬 조각 마냥 퍼져있다. 또, 공식에서는 별말 없이 API KeyAuthorization Header에 넣어 추가해주면 된다고 나와있는데 정말 순수하게 API Key를 아래 코드에 추가한다면 아래와 같은 에러를 볼 수 있을 것이다.

failed: [security_exception] unable to authenticate with provided credentials and anonymous access is not allowed for this request

그러니 필자처럼 몇날 며칠을 헤메지말고 반드시 encoded API Key를 아래 코드에 넣길 바란다.

String host = "localhost";
int port = 9200;

String encodedApiKey = "Change me to encoded API Key";

BasicCredentialsProvider credsProv = new BasicCredentialsProvider();
credsProv.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("elastic", "Change me elasticsearch password"));

try {
    // Create the low-level client
    RestClient restClient = RestClient
            .builder(new HttpHost(host, port, "https"))
            .setDefaultHeaders(new Header[]{
                    new BasicHeader("Authorization", "ApiKey " + encodedApiKey)
            })
            .setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder
                    .setDefaultCredentialsProvider(credsProv)
            )
            .build();

    // Create the transport with a Jackson mapper
    ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());

    // And create the API client
    ElasticsearchClient esClient = new ElasticsearchClient(transport);

    /*
        Write something to do here
    */
} catch (Exception e) {
    log.error("Elasticsearch rest client error", e);
}

추가로 만약 당신이 Docker 공식 이미지를 통해 elasticsearch를 실행했거나, elasticsearch.ymlTLS/SSL 보안설정을 추가했다면 아래의 과정을 추가로 참고하길 바란다.

TLS/SSL 인증 코드 추가

링크: Elasticsearch Java API Client [8.11] > Setup > Connecting
링크: Elastic Docs ›Elasticsearch Java API Client [8.11] › Java Low Level REST Client › Common configuration > Encrypted communication
보안 연결 설정은 elsticsearch.yml에서 xpack.security를 통해서 할 수 있다.
아래는 필자의 보안 설정이다.

# Enable security features
xpack.security.enabled: true

xpack.security.enrollment.enabled: true

# Enable encryption for HTTP API client connections, such as Kibana, Logstash, and Agents
xpack.security.http.ssl:
  enabled: true
  keystore.path: certs/http.p12

# Enable encryption and mutual authentication between cluster nodes
xpack.security.transport.ssl:
  enabled: true
  verification_mode: certificate
  keystore.path: certs/transport.p12
  truststore.path: certs/transport.p12

필자는 Docker에 있는 elasticsearch 공식 이미지를 사용해서 자동으로 설정된 것이기 때문에 스스로 보안 설정하려면 관련 문서를 찾아보고 하는 것을 추천한다.

만약 당신이 위와 같이 보안 설정을 했는데 다음 코드에 TLS/SSL 인증관련 코드를 추가하지 않았다면 아래와 같은 오류가 발생할 것이다.

javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at org.elasticsearch.client.RestClient.extractAndWrapCause(RestClient.java:925)
	at org.elasticsearch.client.RestClient.performRequest(RestClient.java:300)
	at org.elasticsearch.client.RestClient.performRequest(RestClient.java:288)
	at co.elastic.clients.transport.rest_client.RestClientHttpClient.performRequest(RestClientHttpClient.java:91)
	at co.elastic.clients.transport.ElasticsearchTransportBase.performRequest(ElasticsearchTransportBase.java:144)
	at co.elastic.clients.elasticsearch.ElasticsearchClient.search(ElasticsearchClient.java:1897)
	at co.elastic.clients.elasticsearch.ElasticsearchClient.search(ElasticsearchClient.java:1914)
Caused by: javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:131)
	at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:378)
	at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:321)
	at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:316)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1357)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.onConsumeCertificate(CertificateMessage.java:1232)
Caused by: javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.consume(CertificateMessage.java:1175)
	at java.base/sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:396)
	at java.base/sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:480)
	at java.base/sun.security.ssl.SSLEngineImpl$DelegatedTask$DelegatedAction.run(SSLEngineImpl.java:1277)
	at java.base/sun.security.ssl.SSLEngineImpl$DelegatedTask$DelegatedAction.run(SSLEngineImpl.java:1264)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:712)
	at java.base/sun.security.ssl.SSLEngineImpl$DelegatedTask.run(SSLEngineImpl.java:1209)
	at org.apache.http.nio.reactor.ssl.SSLIOSession.doRunTask(SSLIOSession.java:289)
	at org.apache.http.nio.reactor.ssl.SSLIOSession.doHandshake(SSLIOSession.java:357)
	at org.apache.http.nio.reactor.ssl.SSLIOSession.isAppInputReady(SSLIOSession.java:545)
	at org.apache.http.impl.nio.reactor.AbstractIODispatch.inputReady(AbstractIODispatch.java:120)
	at org.apache.http.impl.nio.reactor.BaseIOReactor.readable(BaseIOReactor.java:162)
	at org.apache.http.impl.nio.reactor.AbstractIOReactor.processEvent(AbstractIOReactor.java:337)
	at org.apache.http.impl.nio.reactor.AbstractIOReactor.processEvents(AbstractIOReactor.java:315)
	at org.apache.http.impl.nio.reactor.AbstractIOReactor.execute(AbstractIOReactor.java:276)
	at org.apache.http.impl.nio.reactor.BaseIOReactor.execute(BaseIOReactor.java:104)
	at org.apache.http.impl.nio.reactor.AbstractMultiworkerIOReactor$Worker.run(AbstractMultiworkerIOReactor.java:591)
	at java.base/java.lang.Thread.run(Thread.java:840)
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at java.base/sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:439)
	at java.base/sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:306)
	at java.base/sun.security.validator.Validator.validate(Validator.java:264)
	at java.base/sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:285)
	at java.base/sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:144)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1335)
	... 19 common frames omitted
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at java.base/sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:148)
	at java.base/sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:129)
	at java.base/java.security.cert.CertPathBuilder.build(CertPathBuilder.java:297)
	at java.base/sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:434)
	... 24 common frames omitted
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

해당 문제를 해결하는 가장 손 쉬운 방법은 다음과 fingerprint를 이용하는 방법이다.
여기서 fingerprintelasticsearch를 처음 시작했을 때 출력되는 블럭에서 확인할 수 있으며, 재발급이 불가능하다.
elasticsearch 내부에 있는 인증서인 http_ca.crtopenssl명령어를 통해서 확인할 수 있다는데 필자는 해보진 않았으니 필요한 사람은 아래의 링크를 참고해서 시도해보길 바란다.
링크: Where can I see my Certificate Fingerprint?

String host = "localhost";
int port = 9200;

String encodedApiKey = "Change me to encoded API Key";

String fingerprint = "Change me elasticsearch HTTP CA certificate SHA-256 fingerprint";	// 추가된 부분

SSLContext sslContext = TransportUtils.sslContextFromCaFingerprint(fingerprint); // 추가된 부분

BasicCredentialsProvider credsProv = new BasicCredentialsProvider();
credsProv.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("elastic", "Change me elasticsearch password"));

try {
    // Create the low-level client
    RestClient restClient = RestClient
            .builder(new HttpHost(host, port, "https"))
            .setDefaultHeaders(new Header[]{
                    new BasicHeader("Authorization", "ApiKey " + encodedApiKey)
            })
            .setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder
                    .setSSLContext(sslContext) // 추가된 부분
                    .setDefaultCredentialsProvider(credsProv)
            )
            .build();

    // Create the transport with a Jackson mapper
    ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());

    // And create the API client
    ElasticsearchClient esClient = new ElasticsearchClient(transport);

    /*
        Write something to do here
    */
} catch (Exception e) {
    log.error("Elasticsearch rest client error", e);
}

필자는 이것 이외의 방법 시도해본적이 없으나 아래의 링크에는 여러 방법이 설명되어 있으니 다른 방법을 시도해보고 싶은 사람들은 참고하면된다.
링크: Elastic Docs ›Elasticsearch Java API Client [8.11] › Java Low Level REST Client › Common configuration > Encrypted communication
참고로 해당 문서에서 말하는 http.p12http_ca.crt, transport.p12이 3개의 인증서는 모두 elasticsearchconfig/certs에 들어있다.


마무리

그저 연결하기를 몇날 며칠 붙잡고 있던 Java Low Level REST Client이다.
공식문서는 설명이 부족하고, 심지어 이와 관련된 메소드들에 대한 설명을 Java Docs형태로 정리했다고 하는데 예문하나 적혀있지 않다.
평소에 오라클에서 제공하는 Java 8 Docs 같은 잘 정리된 문서만 애용하다보니 모든 과정이 너무너무 힘들었다.
이 글을 읽고 사용하려는 사람이 있다면, 부디 Java Docs와 인터넷 서칭(반드시 영어로)를 사용해 사용법을 알아내고 그럼에도 불구하고 못 찾겠다, 머리가 깨질 것 같다 하면 elasitc dicuss에 가서 물어보면 친절하게 알려주실 것이다.

Java Low Level REST Client를 사용하는데 연결 설정에서부터 막히는 사람이 있다면, 부디 이 글을 찾아내서 이 어질어질한 고생들을 겪질 않길 진정으로 빈다..ㅠ

0개의 댓글