ELK Stack으로 Nginx 로그 수집하기

kindof

·

2024. 3. 15. 23:17

이번 글에서는 ELK Stack을 활용해서 애플리케이션의 Nginx 로그를 수집해보는 프로젝트를 실습해보려고 합니다.
 

ELK Stack

ElasticSearch(es)는 Elastic Stack의 핵심인 분산 검색, 분석 엔진입니다. 고성능에 스키마 없는 JSON 기반 document로 다양한 데이터에 대한 검색과 분석에 용이합니다.
 
Logstash는 데이터를 수집, 집계하고 원하는 곳에 전송할 수 있도록 하는 도구입니다. 특히 es에 데이터를 로드할 때 편리하고 우수한 성능의 인덱싱을 가능하게 하여 es를 사용한다면 Logstash를 동시에 사용하는 것이 보편적입니다.
 
Kibana는 데이터 시각화 및 탐색 도구입니다. 이 역시 es에 저장된 데이터를 시각화할 때 기본적으로 많이 사용되고 있습니다.
 

https://www.geeksforgeeks.org/what-is-elastic-stack-and-elasticsearch/

 
결국 ELK Stack은 [1] Logstash를 통해 데이터를 수집 및 변환해서 es에 전송하고, [2] es는 데이터를 인덱싱하고 검색하는 기능을 제공, [3] Kibana는 데이터의 분석 결과를 시각화하는 일련의 프로세스를 스택화한 것이라고 볼 수 있습니다.
 

개발 환경 구축하기 - Springboot + Nginx

Spring Initializer를 통해 간단한 Springboot 프로젝트를 생성합니다. Springboot 3.2.3, Java17 환경에서 Lombok, Spring Web 의존성 정도만 추가했습니다. 환경은 크게 중요하지 않습니다.

 

아래와 같이 TestController를 작성하고 동작을 확인합니다.

@RestController
public class TestController {

    @GetMapping("/v1/elk/test")
    public String test() {
        return "OK!";
    }
}
$ curl http://localhost:8080/v1/elk/test

OK!

 

 

Nginx는 여러 용도로 사용될 수 있지만 Reverse Proxy 서버 역할을 하는 데 많이 사용됩니다. 

간단한 예시로 /api/xxx와 같은 요청에서 /api를 제거하고 서버에 요청을 보내는 기능을 nginx.conf에 작성하고 실행해보겠습니다.

# nginx.conf

// .. 생략
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    access_log /Users/josh/Desktop/projects/elk/logs/nginx/access.log;
    error_log  /Users/josh/Desktop/projects/elk/logs/nginx/error.log;

    server {
        listen       80;
        server_name  localhost;

        location /api/v1/ {
            rewrite ^/api/(.*) /$1 break;
	        proxy_pass http://localhost:8080/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_redirect off;
        }

    }
}
$ nginx -t
$ brew services start nginx
$ brew services list
nginx              started josh ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist

 

이제 크롬을 켜고 Nginx가 떠 있는 localhost:80/api/v1/elk/test에 요청을 보내면 Springboot 애플리케이션의 /v1/elk/test API 요청으로 프록시되고, 아래와 같이 nginx 로그가 남습니다.

$ cat /Users/josh/Desktop/projects/elk/logs/nginx/access.log
127.0.0.1 - - [15/Mar/2024:21:53:17 +0900] "GET /api/v1/elk/test HTTP/1.1" 200 3 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"

 

 

이러한 Nginx 로그는 서버의 상태를 모니터링하는 데 중요합니다. 또한 클라이언트의 요청에 대한 세부 정보를 포함하기 때문에 보안 측면에서도 수집할 가치가 매우 큽니다.

 

위에서 설명한 ELK Stack을 이용해서 Nginx 로그를 수집해보겠습니다.

 

개발 환경 구축하기 - ElasticSearch, Logstash, Kibana 설치

개발 환경은 Mac M1 Monterey 12.5 입니다.

구글에 download elasticsearch, download logstash, download kibana를 검색하고 macOS aarch64 플랫폼의 압축 파일을 다운받으시면 됩니다.

download elasticsearch

 
프로젝트 실습 폴더에 해당 파일을 옮기고, 압축을 해제합니다.

~/Desktop/projects/elk > ls -l
total 0
drwxr-xr-x@ 12 josh  staff  384  2 19 19:08 elasticsearch-8.12.2
drwxr-xr-x@ 17 josh  staff  544  2 20 00:57 kibana-8.12.2
drwxr-xr-x@ 19 josh  staff  608  2 20 00:04 logstash-8.12.2

 


ElasticSearch 실행

elasticsearch를 실행합니다.

$ ~/Desktop/projects/elk/elasticsearch-8.12.2 > ./bin/elasticsearch

 
만약 아래와 같이 "received plaintext http traffic on an https channel, closing connection Netty4HttpChannel" 로그가 출력되면서 localhost:9200에 연결되지 않는다면 config > elasticsearch.yml 파일의 security 옵션을 false로 수정해줍니다.

received plaintext http traffic on an https channel, closing connection Netty4HttpChannel
elasticsearch.yml

 
정상적으로 실행되면 아래와 같이 Password 등에 대한 정보가 로그로 출력됩니다.

elasticsearch 실행

 

Kibana 실행

kibana를 실행해줍니다.

~/Desktop/projects/elk/kibana-8.12.2 > ./bin/kibana
// .. 생략
[2024-03-10T21:11:39.911+09:00][INFO ][root] Holding setup until preboot stage is completed.


i Kibana has not been configured.

Go to http://localhost:5601/?code=444859 to get started.

로그에 'Kibana has not been configured.' 라는 메시지가 보이며 'http://localhost:5601/?code=444859'에 접속하라는 말이 보입니다.
 

http://localhost:5601/?code=444859

 
 
es를 실행했을 때 로그에 출력됐던 토큰을 입력해줍니다.

Kibana erollment token

 

http://localhost:5601/app/home#/

 
 

Logstash 실행

logstash를 실행합니다.

$ ~/Desktop/projects/elk/logstash-8.12.2 > ./bin/logstash

//.. 생략
ERROR: Pipelines YAML file is empty. Location: /Users/josh/Desktop/projects/elk/logstash-8.12.2/config/pipelines.yml
usage:
  bin/logstash -f CONFIG_PATH [-t] [-r] [] [-w COUNT] [-l LOG]
  bin/logstash --modules MODULE_NAME [-M "MODULE_NAME.var.PLUGIN_TYPE.PLUGIN_NAME.VARIABLE_NAME=VALUE"] [-t] [-w COUNT] [-l LOG]
  bin/logstash -e CONFIG_STR [-t] [--log.level fatal|error|warn|info|debug|trace] [-w COUNT] [-l LOG]
  bin/logstash -i SHELL [--log.level fatal|error|warn|info|debug|trace]
  bin/logstash -V [--log.level fatal|error|warn|info|debug|trace]
  bin/logstash --help
[2024-03-11T20:36:59,999][FATAL][org.logstash.Logstash    ] Logstash stopped processing because of an error: (SystemExit) exit

하지만 "ERROR: Pipelines YAML file is empty. Location: .../logstash-8.12.2/config/pipelines.yml" 라는 에러 로그를 찍으며 실행에 실패합니다.
 
logstash에서 사용할 파이프라인이 정의되지 않았다는 에러입니다. 아래와 같이 logstash.conf 파일을 작성해주고 실행합니다. 

# ~/Desktop/projects/elk/logstash-8.12.2/config/logstash.conf

input {
  file {
    path => "/Users/josh/Desktop/projects/elk/logs/nginx/access.log"
    start_position => "beginning"
    sincedb_path => "/dev/null"
  }
}

filter {
  grok {
    match => { "message" => "%{IPORHOST:client_ip} - %{DATA:user_name} \[%{HTTPDATE:timestamp}\] \"%{WORD:http_method} %{URIPATHPARAM:request} HTTP/%{NUMBER:http_version}\" %{NUMBER:response_code} %{NUMBER:body_bytes_sent} \"%{DATA:referrer}\" \"%{DATA:user_agent}\"" }
  }

  date {
    match => [ "timestamp", "dd/MMM/yyyy:HH:mm:ss Z" ]
    remove_field => [ "timestamp" ]
  }
}

output {
  elasticsearch {
    hosts => ["localhost:9200"]
    index => "nginx_logs"
  }

  stdout { codec => rubydebug }
}

Input에는 Nginx 액세스 로그 파일의 경로를 지정하고, 파일의 처음부터 로그를 읽습니다. grok을 이용해서 Nginx 기본 로그 형식을 파싱합니다. Output에는 파싱된 로그를 ES에 인덱싱하는 내용을 지정합니다. 현재 로컬에서 ES를 띄운 상태이기 때문에 localhost:9200입니다. stdout은 파싱된 로그를 터미널에 출력합니다.

 

아래와 같이 logstash를 실행하고 localhost에 요청을 보내면 로그가 출력됩니다.

$ ~/Desktop/projects/elk/logstash-8.12.2 > ./bin/logstash -f ./config/logstash.conf
// ..

        "http_method" => "GET",
          "client_ip" => "127.0.0.1",
              "event" => {
        "original" => "127.0.0.1 - - [15/Mar/2024:22:04:22 +0900] \"GET /api/v1/elk/test HTTP/1.1\" 200 3 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36\""
    },
            "request" => "/api/v1/elk/test",
       "http_version" => "1.1",
            "message" => "127.0.0.1 - - [15/Mar/2024:22:04:22 +0900] \"GET /api/v1/elk/test HTTP/1.1\" 200 3 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36\"",
      "response_code" => "200",
           "referrer" => "-",
           "@version" => "1",
                "log" => {
        "file" => {
            "path" => "/Users/josh/Desktop/projects/elk/logs/nginx/access.log"
        }
    },
         "@timestamp" => 2024-03-15T13:04:22.000Z,
    "body_bytes_sent" => "3",
          "user_name" => "-",
               "host" => {
        "name" => "AL02284225.local"
    },
         "user_agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"

 


 

Logstash -> ElasticSearch -> Kibana 플로우 확인

Kibana로 돌아와서 Stack Management > Index Management를 보면 로그가 정상적으로 수집되는 것을 확인할 수 있습니다.

Kibana Index Management
nginx_access_log

위 화면에서 Discover index를 통해 실제 데이터를 확인할 수 있습니다.

Discover index

 

 

Reference

https://medium.com/jimmyberg-kim/spring-boot-nginx-c8e87f7f8376

https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/