엘라스틱 서치 검색 구현 (2) Elasticsearch 8.13.4, Kibana 설치 및 S3 연결 (docker-compose)

오형상·2024년 12월 28일
0

Ficket

목록 보기
21/27

저번에 설계했던 시스템은 검색엔진 서버와 이벤트 서버를 중심으로 한 데이터 색인 및 검색 시스템이었습니다. 주요 기술 스택으로는 Elasticsearch, Redis, MongoDB, Object Storage, Spring Boot, Kibana를 사용하여 데이터 색인 및 장애 대응을 포함한 다양한 시나리오를 처리할 수 있도록 설계되었습니다.

이번 단계에서는 위 설계에서 설치 작업을 진행합니다. Docker Compose를 활용하여 Elasticsearch, Kibana, Redis, MongoDB 환경을 설정하고, Elasticsearch와 관련된 Nori 분석기 설치 및 AWS S3 연결 작업을 수행합니다.


설치 작업

1. Docker Compose 환경 구성

.env 파일과 docker-compose.yml 파일을 작성하여 Elasticsearch, Kibana, Redis, MongoDB를 실행할 수 있는 환경을 설정합니다.

.env 작성

ELASTIC_PASSWORD=elastic
KIBANA_PASSWORD=kibana_system
STACK_VERSION=8.13.4
CLUSTER_NAME=docker-cluster
LICENSE=basic
ES_PORT=9200
KIBANA_PORT=5601

docker-compose.yml 작성

services:
  redis-lock:
    image: redis
    container_name: redis
    ports:
      - "6379:6379"
    command: ["redis-server", "--appendonly", "yes"]

  mongodb:
    image: mongo
    container_name: mongodb
    ports:
      - "27017:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: password
    volumes:
      - mongo-data:/data/db

  setup:
    image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
    volumes:
      - certs:/usr/share/elasticsearch/config/certs
    user: "0"
    command: >
      bash -c '
        if [ x${ELASTIC_PASSWORD} == x ]; then
          echo "Set the ELASTIC_PASSWORD environment variable in the .env file";
          exit 1;
        elif [ x${KIBANA_PASSWORD} == x ]; then
          echo "Set the KIBANA_PASSWORD environment variable in the .env file";
          exit 1;
        fi;
        if [ ! -f config/certs/ca.zip ]; then
          echo "Creating CA";
          bin/elasticsearch-certutil ca --silent --pem -out config/certs/ca.zip;
          unzip config/certs/ca.zip -d config/certs;
        fi;
        if [ ! -f config/certs/certs.zip ]; then
          echo "Creating certs";
          echo -ne \
          "instances:\n"\  
          "  - name: es01\n"\  
          "    dns:\n"\  
          "      - es01\n"\  
          "      - localhost\n"\  
          "    ip:\n"\  
          "      - 127.0.0.1\n"\  
          "  - name: kibana\n"\  
          "    dns:\n"\  
          "      - kibana\n"\  
          "      - localhost\n"\  
          "    ip:\n"\  
          "      - 127.0.0.1\n"\  
          > config/certs/instances.yml;
          bin/elasticsearch-certutil cert --silent --pem -out config/certs/certs.zip --in config/certs/instances.yml --ca-cert config/certs/ca/ca.crt --ca-key config/certs/ca/ca.key;
          unzip config/certs/certs.zip -d config/certs;
        fi;
        echo "Setting file permissions"
        chown -R root:root config/certs;
        find . -type d -exec chmod 750 \{\} \; ;
        find . -type f -exec chmod 640 \{\} \; ;
        echo "Waiting for Elasticsearch availability";
        until curl -s --cacert config/certs/ca/ca.crt https://es01:9200 | grep -q "missing authentication credentials"; do sleep 30; done;
        echo "Setting kibana_system password";
        until curl -s -X POST --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_security/user/kibana_system/_password -d "{\"password\":\"${KIBANA_PASSWORD}\"}" | grep -q "^{}"; do sleep 10; done;
        echo "All done!";
      '
    healthcheck:
      test: ["CMD-SHELL", "[ -f config/certs/es01/es01.crt ]"]
      interval: 1s
      timeout: 5s
      retries: 120

  es01:
    depends_on:
      setup:
        condition: service_healthy
    image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
    labels:
      co.elastic.logs/module: elasticsearch
    volumes:
      - certs:/usr/share/elasticsearch/config/certs
      - esdata01:/usr/share/elasticsearch/data
    ports:
      - ${ES_PORT}:9200
    environment:
      - node.name=es01
      - cluster.name=${CLUSTER_NAME}
      - discovery.type=single-node
      - ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
      - bootstrap.memory_lock=true
      - xpack.security.enabled=true
      - ES_JAVA_OPTS=-Xms1g -Xmx1g
      - xpack.security.http.ssl.enabled=true
      - xpack.security.http.ssl.key=certs/es01/es01.key
      - xpack.security.http.ssl.certificate=certs/es01/es01.crt
      - xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt
      - xpack.security.transport.ssl.enabled=true
      - xpack.security.transport.ssl.key=certs/es01/es01.key
      - xpack.security.transport.ssl.certificate=certs/es01/es01.crt
      - xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt
      - xpack.security.transport.ssl.verification_mode=certificate
      - xpack.license.self_generated.type=${LICENSE}
    ulimits:
      memlock:
        soft: -1
        hard: -1
    healthcheck:
      test:
        [
          "CMD-SHELL",
          "curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'",
        ]
      interval: 10s
      timeout: 10s
      retries: 120

  kibana01:
    depends_on:
      es01:
        condition: service_healthy
    image: docker.elastic.co/kibana/kibana:${STACK_VERSION}
    labels:
      co.elastic.logs/module: kibana
    volumes:
      - certs:/usr/share/kibana/config/certs
      - kibanadata:/usr/share/kibana/data
    ports:
      - ${KIBANA_PORT}:5601
    environment:
      - SERVERNAME=kibana
      - ELASTICSEARCH_HOSTS=https://es01:9200
      - ELASTICSEARCH_USERNAME=kibana_system
      - ELASTICSEARCH_PASSWORD=${KIBANA_PASSWORD}
      - ELASTICSEARCH_SSL_CERTIFICATEAUTHORITIES=config/certs/ca/ca.crt
    healthcheck:
      test:
        [
          "CMD-SHELL",
          "curl -s -I http://localhost:5601 | grep -q 'HTTP/1.1 302 Found'",
        ]
      interval: 10s
      timeout: 10s
      retries: 120
  
volumes:
  mongo-data:
  certs:
  esdata01:
  kibanadata:

이후 docker-compose up -d 명령어를 실행하여 서비스를 시작합니다.


2. Nori 분석기 설치

  1. 실행 중인 Elasticsearch 컨테이너를 확인합니다:

    docker ps
    
  2. Elasticsearch 컨테이너에 접속합니다:

    docker exec -it {컨테이너_ID} /bin/bash
    
  3. Nori 분석기를 설치합니다:

    cd /usr/share/elasticsearch/bin/
    ./elasticsearch-plugin install analysis-nori
    

3. AWS S3 연결

Elasticsearch 버전 8.x부터는 S3 플러그인이 내장되어 있습니다. 아래 명령어로 S3 설정을 추가합니다.

  1. Elasticsearch 컨테이너에 접속합니다:

    docker exec -it {컨테이너_ID} /bin/bash
    
  2. IAM 계정 정보를 설정합니다:

    ./elasticsearch-keystore add s3.client.default.access_key
    ./elasticsearch-keystore add s3.client.default.secret_key
    

4. SSL 인증서 문제 해결

Elasticsearch에 연결할 때 발생하는 PKIX path building failed 오류를 해결하기 위해 SSL 인증서를 등록합니다.

인증서 복사

  1. Elasticsearch 컨테이너에서 인증서를 호스트로 복사합니다

    docker cp {컨테이너_ID}:/usr/share/elasticsearch/config/certs/ca/ca.crt C:\elasticsearch
    

Java 인증서 등록

  1. Java 설치 경로에서 cacerts 파일에 인증서를 등록합니다

    keytool.exe -import -alias es -keystore "%JAVA_HOME%\lib\security\cacerts" -storepass changeit -file "C:\elasticsearch\ca.crt"
    
  2. 인증서 등록 여부를 확인합니다

    등록된 인증서 목록에서 es 별칭이 있는지 확인합니다.

    keytool.exe -list -keystore "%JAVA_HOME%\lib\security\cacerts" -storepass changeit
    

5. Spring 프로젝트 설정

의존성 추가

// Redis
implementation "org.redisson:redisson-spring-boot-starter:3.21.1"

// MongoDB
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'

// Elasticsearch
implementation 'co.elastic.clients:elasticsearch-java:8.13.4'

환경 변수 설정

elasticsearch 컨테이너의 /usr/share/elasticsearch/config/certs/ca/ca.crt를 열면 다음과 같은 내용이 있습니다.

-----BEGIN CERTIFICATE-----
MIIDTTCCAjWgAwIBAgIUHY0MSYQEiZBeWaI9br1jQf9BZ7FQDQYJKoZIhvcNAQEL
(생략)
OL01wpHXE+jAFz4i/75pQRjL6Gvq8vsrjYJc2Wp=
-----END CERTIFICATE-----

첫 번째 줄(BEGIN)과 마지막 줄(END)를 제외하고 복사하여 application.yml에 환경 변수로 추가합니다.

spring:
  elasticsearch:
    host: ${ELASTIC_HOST}
    username: ${ELASTIC_USER}
    password: ${ELASTIC_PASSWORD}
    ca-certificate-path: ${ca-certificate}

ElasticsearchConfig 파일 작성

@Configuration
public class ElasticsearchConfig {

    @Value("${spring.elasticsearch.username}")
    private String username;

    @Value("${spring.elasticsearch.password}")
    private String password;

    @Value("${spring.elasticsearch.host}")
    private String host;

    @Value("${spring.elasticsearch.port:9200}")
    private int port;

    @Value("${elasticsearch.ca-certificate-path}")
    private String certificateBase64;

    /**
     * ElasticsearchClient Bean 생성
     */
    @Bean
    public ElasticsearchClient elasticsearchClient() throws Exception {
        // RestClient 설정
        RestClient restClient = restClient();

        // ObjectMapper 생성
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule()); // Java8 시간 관련 클래스 처리

        // RestClientTransport를 사용하여 ElasticsearchTransport로 변환
        ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper(objectMapper));

        // ElasticsearchClient를 생성
        return new ElasticsearchClient(transport);
    }

    /**
     * Elasticsearch RestClient Bean 생성
     */
    @Bean
    public RestClient restClient() throws Exception {
        // CA 인증서 로드
        SSLContext sslContext = getSSLContext(); // getSSLContext()를 호출하여 SSLContext 객체 생성

        RestClientBuilder builder = RestClient.builder(new HttpHost(host, port, "https"))
                .setHttpClientConfigCallback(httpAsyncClientBuilder -> httpAsyncClientBuilder
                        .setSSLContext(sslContext)  // getSSLContext()에서 반환된 SSLContext 설정
                        .setDefaultCredentialsProvider(credentialsProvider()));

        return builder.build();
    }

    private CredentialsProvider credentialsProvider() {
        CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password));
        return credentialsProvider;
    }

    private SSLContext getSSLContext() throws Exception {
        byte[] decodedCertificate = Base64.getMimeDecoder().decode(certificateBase64);

        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        X509Certificate ca;
        try (InputStream certificateInputStream = new ByteArrayInputStream(decodedCertificate)) {
            ca = (X509Certificate) certificateFactory.generateCertificate(certificateInputStream);
        }

        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null, null);
        keyStore.setCertificateEntry("ca", ca);

        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(keyStore);

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, tmf.getTrustManagers(), new java.security.SecureRandom());
        return sslContext;
    }
}

S3 저장소 설정

    public CreateRepositoryResponse registerS3Repository() {
        try {

            Repository repository = new Repository.Builder()
                    .s3(builder -> builder
                            .settings(settings -> settings
                                    .bucket(SNAPSHOT_S3_BUCKET)
                                    .basePath("elasticsearch/snapshot")
                            ))
                    .build();

            CreateRepositoryRequest repositoryRequest = new CreateRepositoryRequest.Builder()
                    .repository(repository)
                    .name(SNAPSHOT_STORAGE_NAME)
                    .build();


            // S3 저장소 등록
            CreateRepositoryResponse response = elasticsearchClient.snapshot().createRepository(repositoryRequest);

            log.info("S3 저장소가 성공적으로 등록되었습니다.");

            return response;
        } catch (Exception e) {
            log.error("S3 저장소 등록에 실패했습니다: {}", e.getMessage(), e);
            throw new RuntimeException("S3 저장소 등록에 실패했습니다.");
        }
    }

이로써 Elasticsearch, Redis, MongoDB, Kibana의 설치와 설정이 완료됩니다. 다음 단계에서는 검색엔진 서버와 관련된 색인 로직을 구현합니다.

Reference

0개의 댓글