저번에 설계했던 시스템은 검색엔진 서버와 이벤트 서버를 중심으로 한 데이터 색인 및 검색 시스템이었습니다. 주요 기술 스택으로는 Elasticsearch, Redis, MongoDB, Object Storage, Spring Boot, Kibana를 사용하여 데이터 색인 및 장애 대응을 포함한 다양한 시나리오를 처리할 수 있도록 설계되었습니다.
이번 단계에서는 위 설계에서 설치 작업을 진행합니다. Docker Compose를 활용하여 Elasticsearch, Kibana, Redis, MongoDB 환경을 설정하고, Elasticsearch와 관련된 Nori 분석기 설치 및 AWS S3 연결 작업을 수행합니다.
.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
명령어를 실행하여 서비스를 시작합니다.
실행 중인 Elasticsearch 컨테이너를 확인합니다:
docker ps
Elasticsearch 컨테이너에 접속합니다:
docker exec -it {컨테이너_ID} /bin/bash
Nori
분석기를 설치합니다:
cd /usr/share/elasticsearch/bin/
./elasticsearch-plugin install analysis-nori
Elasticsearch 버전 8.x부터는 S3 플러그인이 내장되어 있습니다. 아래 명령어로 S3 설정을 추가합니다.
Elasticsearch 컨테이너에 접속합니다:
docker exec -it {컨테이너_ID} /bin/bash
IAM 계정 정보를 설정합니다:
./elasticsearch-keystore add s3.client.default.access_key
./elasticsearch-keystore add s3.client.default.secret_key
Elasticsearch에 연결할 때 발생하는 PKIX path building failed
오류를 해결하기 위해 SSL 인증서를 등록합니다.
Elasticsearch 컨테이너에서 인증서를 호스트로 복사합니다
docker cp {컨테이너_ID}:/usr/share/elasticsearch/config/certs/ca/ca.crt C:\elasticsearch
Java 설치 경로에서 cacerts
파일에 인증서를 등록합니다
keytool.exe -import -alias es -keystore "%JAVA_HOME%\lib\security\cacerts" -storepass changeit -file "C:\elasticsearch\ca.crt"
인증서 등록 여부를 확인합니다
등록된 인증서 목록에서 es
별칭이 있는지 확인합니다.
keytool.exe -list -keystore "%JAVA_HOME%\lib\security\cacerts" -storepass changeit
// 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}
@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;
}
}
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