ELK 로그 환경
요즈음 로그 시스템을 만들때 아마도 가장 많이 사용되는 기술은 ELK일 것입니다.
ELK에 대해서는 너무 잘 알려져있기 때문에 설명은 생략하겠습니다. (https://www.elastic.co/kr/what-is/elk-stack)
이번 포스팅에서는 Kafka + ELK 를 통해서 로그 시스템을 구축하는 예제를 정리하려 합니다.
예제에 사용될 서비스들은 모두 Docker를 통해 Local 환경에 스택들을 구성하여 사용할 예정입니다.
전체적인 서비스 환경은 아래와 같습니다.

Kafka 환경 구축하기
먼저, 로그 메세지를 통신하기 위한 Kafka를 띄워보겠습니다.
우선, kafka가 잘 구성되어있는 Docker image를 사용하기 위해 아래 git 을 통해 파일을 clone 합니다.
$ git clone https://github.com/wurstmeister/kafka-docker && cd kafka-docker
다음, docker-compose 파일을 수정합니다.
해당 예제 환경에서는 kafka single-node 만 사용할 예정으로 'docker-compose-single-broker.yml' 파일의 설정을 변경 합니다.
// docker-compose-single-broker.yml
// 서버 host name 설정
KAFKA_ADVERTISED_HOST_NAME: 10.x.x.x
// 사용할 topic 설정 (토픽은 Kafka 구성 이후 생성할수도 있음)
KAFKA_CREATE_TOPICS: "log-topic:1:1"
다음, docker-compose를 위에서 설정한'docker-compose-single-broker.yml' 파일 으로 실행 합니다.
$ docker-compose -f docker-compose-single-broker.yml up -d
kafka컨테이너가 정상 수행 되었는지 확인 합니다.
$ docker ps

ELK 환경 구축하기
이번에는 ELK stack을 마찬가지로 docker를 이용해 띄워보겠습니다.
ELK가 잘 구성되어있는 Docker image를 사용하기 위해 아래 git 을 통해 파일을 clone 합니다.
$ git clone https://github.com/deviantony/docker-elk.git && cd docker-elk
다음, Elasticsearch 파일을 수정합니다.
xpack 설정파일 주석처리
// elasticsearch/config/elasticsearch.yml
---
## Default Elasticsearch configuration from Elasticsearch base image.
## https://github.com/elastic/elasticsearch/blob/master/distribution/docker/src/docker/config/elasticsearch.yml
#
cluster.name: "docker-cluster"
network.host: 0.0.0.0
## X-Pack settings
## see https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-xpack.html
#
#xpack.license.self_generated.type: trial
#xpack.security.enabled: true
#xpack.monitoring.collection.enabled: true
nori 분석기 추가
// elasticsearch/Dockerfile
ARG ELK_VERSION
# https://www.docker.elastic.co/
FROM docker.elastic.co/elasticsearch/elasticsearch:${ELK_VERSION}
# Add your elasticsearch plugins setup here
# Example: RUN elasticsearch-plugin install analysis-icu
RUN elasticsearch-plugin install analysis-nori
다음, Logstash 파일을 수정합니다.
xpack 설정파일 주석처리
// logstash/config/logstash.yml
---
## Default Logstash configuration from Logstash base image.
## https://github.com/elastic/logstash/blob/master/docker/data/logstash/config/logstash-full.yml
#
http.host: "0.0.0.0"
xpack.monitoring.elasticsearch.hosts: [ "http://elasticsearch:9200" ]
## X-Pack security credentials
#
#xpack.monitoring.enabled: true
#xpack.monitoring.elasticsearch.username: elastic
#xpack.monitoring.elasticsearch.password: changeme
pipeline 설정 내용 세팅
// logstash/pipeline/logstash.config
input {
kafka {
bootstrap_servers => "10.x.x.x:9092" // kafka ip
group_id => "logstash"
topics => ["log-topic"]
consumer_threads => 1
}
tcp {
port => 5000
}
}
## Add your filters / logstash plugins configuration here
output {
elasticsearch {
hosts => "elasticsearch:9200"
index => "logs-%{+YYYY.MM.dd}"
user => "elastic"
password => "password"
}
}
다음, Kibana 파일을 수정합니다.
xpack 설정파일 주석처리
// kibana/config/kibana.yml
---
## Default Kibana configuration from Kibana base image.
## https://github.com/elastic/kibana/blob/master/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts
#
server.name: kibana
server.host: 0.0.0.0
elasticsearch.hosts: [ "http://elasticsearch:9200" ]
monitoring.ui.container.elasticsearch.enabled: true
## X-Pack security credentials
#
#elasticsearch.username: elastic
#elasticsearch.password: changeme
이제 ELK 각각의 구성설정을 마쳤으니 docker-compose 파일을 수정해줍니다.
elastic password 수정
// docker-compose
...
ELASTIC_PASSWORD: password
...
// docker-stack
...
ELASTIC_PASSWORD: password
...
다음, docker-compose를 실행 합니다.
$ docker-compose up -d
ELK 컨테이너가 정상 수행 되었는지 확인 합니다.
$ docker ps

Log 적재하기
이제 Kafka + ELK 환경이 갖추어졌으니 로그 시스템이 다 갖춰졌습니다.
다음과 같은 로직으로 위 시스템을 검증해보려 합니다.
- Kafka 에서 message 생성
- ES Index 생성 확인
- ES Document 생성 확인
- Kibana 데이터 조회
1. Kafka Message 생성
Kafka container bash 접속
// docker ps 를 통해 container id를 조회 후 bash를 실행
$ sudo docker exec -it 0c0af88851f0 /bin/bash
Kafka console producer 실행
$ kafka-console-producer.sh --broker-list 10.200.8.5:9092 --topic log-topic
log message 발행
> {"reg_dt":"2020-01-21 10:00:00","ldept_nm":"개발본부","mdept_nm":"개발실2","sdept_nm":"개발팀","reg_id":"test_id_1","reg_nm":"테스터1","emp_id":"202101010001","product_id":1000001,"product_name":"테스트 상품1","task_dtl":"상품 생성"}
> {"reg_dt":"2020-01-21 10:05:03","ldept_nm":"개발본부","mdept_nm":"개발실2","sdept_nm":"개발팀","reg_id":"test_id_1","reg_nm":"테스터1","emp_id":"202101010001","product_id":1000001,"product_name":"테스트 상품1","task_dtl":"상품 수정"}
> {"reg_dt":"2020-01-21 10:05:03","ldept_nm":"개발본부","mdept_nm":"개발실2","sdept_nm":"개발팀","reg_id":"test_id_1","reg_nm":"테스터1","emp_id":"202101010001","product_id":1000002,"product_name":"테스트 상품2","task_dtl":"상품 생성"}
> {"reg_dt":"2020-01-21 10:05:13","ldept_nm":"개발본부","mdept_nm":"개발실2","sdept_nm":"개발팀","reg_id":"test_id_1","reg_nm":"테스터1","emp_id":"202101010001","product_id":1000003,"product_name":"테스트 상품3","task_dtl":"상품 생성"}
> {"reg_dt":"2020-01-21 10:17:03","ldept_nm":"개발본부","mdept_nm":"개발실2","sdept_nm":"개발팀","reg_id":"test_id_1","reg_nm":"테스터1","emp_id":"202101010001","product_id":1000002,"product_name":"테스트 상품2","task_dtl":"상품 수정"}
2. Elasticsearch Index 생성 확인
Elasticsearch index 조회
$ curl localhost:9200/_cat/indices?v
결과
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
yellow open logs-2021.02.05 w4Oy85xWThaXT9nat6yFTg 1 1 5 0 15.8kb 15.8kb
green open .apm-custom-link vvCTctk7R--fI17vdW-QQg 1 0 0 0 208b 208b
green open .kibana_task_manager_1 2I5nX90PQlKWg2ExJIPd_Q 1 0 5 537 112.3kb 112.3kb
green open .apm-agent-configuration Hjmli6SFRtaRW8CyVWV1DA 1 0 0 0 208b 208b
green open .kibana_1 bUDjQkl7Skm7EL14oSa1sg 1 0 9 0 2.1mb 2.1mb
green open .kibana-event-log-7.10.2-000001 LPNt7Q0zSqS53vv4vURPVQ 1 0 1 0 5.6kb 5.6kb
로그스태시에 설정한대로 logs-2021.02.05 인덱스가 생성된것을 확인 할 수 있습니다.
3. ES Document 생성 확인
이제 index 내부에 Document가 실제로 생성되었는지 확인할 차례입니다.
$ curl localhost:9200/logs-2021.02.05/_search?pretty
결과
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 5,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "logs-2021.02.05",
"_type" : "_doc",
"_id" : "vyCRcHcBRkK_WRzTT99_",
"_score" : 1.0,
"_source" : {
"@version" : "1",
"message" : "{"reg_dt":"2020-01-21 10:00:00","ldept_nm":"개발본부","mdept_nm":"개발실2","sdept_nm":"개발팀","reg_id":"test_id_1","reg_nm":"테스터1","emp_id":"202101010001","product_id":1000001,"product_name":"테스트 상품1","task_dtl":"상품 생성"}",
"@timestamp" : "2021-02-05T05:01:46.492Z"
}
},
{
"_index" : "ogs-2021.02.05",
"_type" : "_doc",
"_id" : "xiCUcHcBRkK_WRzTqN8L",
"_score" : 1.0,
"_source" : {
"@version" : "1",
"message" : "{"reg_dt":"2020-01-21 10:05:03","ldept_nm":"개발본부","mdept_nm":"개발실2","sdept_nm":"개발팀","reg_id":"test_id_1","reg_nm":"테스터1","emp_id":"202101010001","product_id":1000001,"product_name":"테스트 상품1","task_dtl":"상품 수정"}",
"@timestamp" : "2021-02-05T05:05:26.431Z"
}
},
{
"_index" : "logs-2021.02.05",
"_type" : "_doc",
"_id" : "xyCUcHcBRkK_WRzTt9_3",
"_score" : 1.0,
"_source" : {
"@version" : "1",
"message" : "{"reg_dt":"2020-01-21 10:05:03","ldept_nm":"개발본부","mdept_nm":"개발실2","sdept_nm":"개발팀","reg_id":"test_id_1","reg_nm":"테스터1","emp_id":"202101010001","product_id":1000002,"product_name":"테스트 상품2","task_dtl":"상품 생성"}",
"@timestamp" : "2021-02-05T05:05:30.510Z"
}
},
{
"_index" : "logs-2021.02.05",
"_type" : "_doc",
"_id" : "yCCVcHcBRkK_WRzTxN8f",
"_score" : 1.0,
"_source" : {
"@version" : "1",
"message" : "{"reg_dt":"2020-01-21 10:05:13","ldept_nm":"개발본부","mdept_nm":"개발실2","sdept_nm":"개발팀","reg_id":"test_id_1","reg_nm":"테스터1","emp_id":"202101010001","product_id":1000003,"product_name":"테스트 상품3","task_dtl":"상품 생성"}",
"@timestamp" : "2021-02-05T05:06:39.157Z"
}
},
{
"_index" : "logs-2021.02.05",
"_type" : "_doc",
"_id" : "1SC6cHcBRkK_WRzTk9-G",
"_score" : 1.0,
"_source" : {
"@version" : "1",
"message" : "{"reg_dt":"2020-01-21 10:17:03","ldept_nm":"개발본부","mdept_nm":"개발실2","sdept_nm":"개발팀","reg_id":"test_id_1","reg_nm":"테스터1","emp_id":"202101010001","product_id":1000002,"product_name":"테스트 상품2","task_dtl":"상품 수정"}",
"@timestamp" : "2021-02-05T05:46:51.547Z"
}
}
]
}
}
위 검증로직을 통해 ES에 정상적으로 로그 메세지가 적재되는것을 확인하였습니다.
검색 가능한 ES 적재
위 상태에서는 넘겨준 message가 ES 에서 단순히 message 에 적재되고 있습니다. 검색이 가능하도록 필드를 구성하여 적재시켜보도록 하겠습니다.
현재 log 메세지가 Json 형태로 넘어오고 있기 때문에 단순히 logstash 에서 json 을 파싱해주는 filter만 추가해주면 됩니다.
// logstash/pipeline/logstash.config
input {
kafka {
bootstrap_servers => "10.200.8.5:9092"
group_id => "logstash"
topics => ["log-topic"]
consumer_threads => 1
}
tcp {
port => 5000
}
}
## Add your filters / logstash plugins configuration here
filter {
json {
source => "message"
}
}
output {
elasticsearch {
hosts => "elasticsearch:9200"
index => "logs-%{+YYYY.MM.dd}"
user => "elastic"
password => "password"
}
}
이후 메세지를 다시 생성해 ES 를 조회해보면 다음과같은 결과를 확인 할 수 있습니다.
{
"took" : 385,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 5,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "logs-2021.02.05",
"_type" : "_doc",
"_id" : "mXlUcXcBQOlug89tq04l",
"_score" : 1.0,
"_source" : {
"product_name" : "테스트 상품1",
"reg_id" : "test_id_1",
"message" : "{\"reg_dt\":\"2020-01-21 10:00:00\",\"ldept_nm\":\"개발본부\",\"mdept_nm\":\"개발실2\",\"sdept_nm\":\"개발팀\",\"reg_id\":\"test_id_1\",\"reg_nm\":\"테스터1\",\"emp_id\":\"202101010001\",\"product_id\":1000001,\"product_name\":\"테스트 상품1\",\"task_dtl\":\"상품 생성\"}",
"@timestamp" : "2021-02-05T08:35:09.744Z",
"ldept_nm" : "개발본부",
"product_id" : 1000001,
"emp_id" : "202101010001",
"task_dtl" : "상품 생성",
"@version" : "1",
"mdept_nm" : "개발실2",
"reg_nm" : "테스터1",
"reg_dt" : "2020-01-21 10:00:00",
"sdept_nm" : "개발팀"
}
},
{
"_index" : "logs-2021.02.05",
"_type" : "_doc",
"_id" : "mnlVcXcBQOlug89tWk7l",
"_score" : 1.0,
"_source" : {
"product_name" : "테스트 상품1",
"reg_id" : "test_id_1",
"message" : "{\"reg_dt\":\"2020-01-21 10:05:03\",\"ldept_nm\":\"개발본부\",\"mdept_nm\":\"개발실2\",\"sdept_nm\":\"개발팀\",\"reg_id\":\"test_id_1\",\"reg_nm\":\"테스터1\",\"emp_id\":\"202101010001\",\"product_id\":1000001,\"product_name\":\"테스트 상품1\",\"task_dtl\":\"상품 수정\"}",
"@timestamp" : "2021-02-05T08:35:55.128Z",
"ldept_nm" : "개발본부",
"product_id" : 1000001,
"emp_id" : "202101010001",
"task_dtl" : "상품 수정",
"@version" : "1",
"mdept_nm" : "개발실2",
"reg_nm" : "테스터1",
"reg_dt" : "2020-01-21 10:05:03",
"sdept_nm" : "개발팀"
}
},
{
"_index" : "-logs-2021.02.05",
"_type" : "_doc",
"_id" : "m3lVcXcBQOlug89tc07y",
"_score" : 1.0,
"_source" : {
"product_name" : "테스트 상품2",
"reg_id" : "test_id_1",
"message" : "{\"reg_dt\":\"2020-01-21 10:05:03\",\"ldept_nm\":\"개발본부\",\"mdept_nm\":\"개발실2\",\"sdept_nm\":\"개발팀\",\"reg_id\":\"test_id_1\",\"reg_nm\":\"테스터1\",\"emp_id\":\"202101010001\",\"product_id\":1000002,\"product_name\":\"테스트 상품2\",\"task_dtl\":\"상품 생성\"}",
"@timestamp" : "2021-02-05T08:36:01.545Z",
"ldept_nm" : "개발본부",
"product_id" : 1000002,
"emp_id" : "202101010001",
"task_dtl" : "상품 생성",
"@version" : "1",
"mdept_nm" : "개발실2",
"reg_nm" : "테스터1",
"reg_dt" : "2020-01-21 10:05:03",
"sdept_nm" : "개발팀"
}
},
{
"_index" : "logs-2021.02.05",
"_type" : "_doc",
"_id" : "nHlVcXcBQOlug89tp04A",
"_score" : 1.0,
"_source" : {
"product_name" : "테스트 상품3",
"reg_id" : "test_id_1",
"message" : "{\"reg_dt\":\"2020-01-21 10:05:13\",\"ldept_nm\":\"개발본부\",\"mdept_nm\":\"개발실2\",\"sdept_nm\":\"개발팀\",\"reg_id\":\"test_id_1\",\"reg_nm\":\"테스터1\",\"emp_id\":\"202101010001\",\"product_id\":1000003,\"product_name\":\"테스트 상품3\",\"task_dtl\":\"상품 생성\"}",
"@timestamp" : "2021-02-05T08:36:14.613Z",
"ldept_nm" : "개발본부",
"product_id" : 1000003,
"emp_id" : "202101010001",
"task_dtl" : "상품 생성",
"@version" : "1",
"mdept_nm" : "개발실2",
"reg_nm" : "테스터1",
"reg_dt" : "2020-01-21 10:05:13",
"sdept_nm" : "개발팀"
}
},
{
"_index" : "logs-2021.02.05",
"_type" : "_doc",
"_id" : "nXlVcXcBQOlug89twk4g",
"_score" : 1.0,
"_source" : {
"product_name" : "테스트 상품2",
"reg_id" : "test_id_1",
"message" : "{\"reg_dt\":\"2020-01-21 10:17:03\",\"ldept_nm\":\"개발본부\",\"mdept_nm\":\"개발실2\",\"sdept_nm\":\"개발팀\",\"reg_id\":\"test_id_1\",\"reg_nm\":\"테스터1\",\"emp_id\":\"202101010001\",\"product_id\":1000002,\"product_name\":\"테스트 상품2\",\"task_dtl\":\"상품 수정\"}",
"@timestamp" : "2021-02-05T08:36:21.559Z",
"ldept_nm" : "개발본부",
"product_id" : 1000002,
"emp_id" : "202101010001",
"task_dtl" : "상품 수정",
"@version" : "1",
"mdept_nm" : "개발실2",
"reg_nm" : "테스터1",
"reg_dt" : "2020-01-21 10:17:03",
"sdept_nm" : "개발팀"
}
}
]
}
}
원하던대로 필드가 구성되어 ES에 적재된것을 확인 할 수 있습니다.
이제 Elasticsearch에 원하는 검색 조건을 추가하여 조회 할 수 있습니다.
$ curl localhost:9200/logs-2021.02.05/_search?q=product_id:1000001
{
"took":1,
"timed_out":false,
"_shards":{
"total":1,
"successful":1,
"skipped":0,
"failed":0
},
"hits":{
"total":{
"value":2,
"relation":"eq"
},
"max_score":1.0,
"hits":[
{
"_index":"logs-2021.02.05",
"_type":"_doc",
"_id":"mXlUcXcBQOlug89tq04l",
"_score":1.0,
"_source":{
"product_name":"테스트 상품1",
"reg_id":"test_id_1",
"message":"{\"reg_dt\":\"2020-01-21 10:00:00\",\"ldept_nm\":\"개발본부\",\"mdept_nm\":\"개발실2\",\"sdept_nm\":\"개발팀\",\"reg_id\":\"test_id_1\",\"reg_nm\":\"테스터1\",\"emp_id\":\"202101010001\",\"product_id\":1000001,\"product_name\":\"테스트 상품1\",\"task_dtl\":\"상품 생성\"}",
"@timestamp":"2021-02-05T08:35:09.744Z",
"ldept_nm":"개발본부",
"product_id":1000001,
"emp_id":"202101010001",
"task_dtl":"상품 생성",
"@version":"1",
"mdept_nm":"개발실2",
"reg_nm":"테스터1",
"reg_dt":"2020-01-21 10:00:00",
"sdept_nm":"개발팀"
}
},
{
"_index":"logs-2021.02.05",
"_type":"_doc",
"_id":"mnlVcXcBQOlug89tWk7l",
"_score":1.0,
"_source":{
"product_name":"테스트 상품1",
"reg_id":"test_id_1",
"message":"{\"reg_dt\":\"2020-01-21 10:05:03\",\"ldept_nm\":\"개발본부\",\"mdept_nm\":\"개발실2\",\"sdept_nm\":\"개발팀\",\"reg_id\":\"test_id_1\",\"reg_nm\":\"테스터1\",\"emp_id\":\"202101010001\",\"product_id\":1000001,\"product_name\":\"테스트 상품1\",\"task_dtl\":\"상품 수정\"}",
"@timestamp":"2021-02-05T08:35:55.128Z",
"ldept_nm":"개발본부",
"product_id":1000001,
"emp_id":"202101010001",
"task_dtl":"상품 수정",
"@version":"1",
"mdept_nm":"개발실2",
"reg_nm":"테스터1",
"reg_dt":"2020-01-21 10:05:03",
"sdept_nm":"개발팀"
}
}
]
}
}