NGINX + ELK 스택을 통한 로그시스템 구축

백종현·2023년 7월 29일
0

NGINX에서 ACCESS LOG 떨구기

기본적으로 서비스에서 로그 시스템을 모으고 있지 않았다. 특히 애플리케이션 단에서 모든 로그를 떨구기에는 코드 변경을 너무 많이 일으켜야하는 구조였다.

해결책

모든 요청에서 공통으로 사용되는 nginx의 설정을 고쳐 로그를 모으기로 하였다. nginx.conf 설정을 통해 request_body 값을 모았다.

# nginx.conf http 블록에 추가
# 일반 포멧
log_format post_logs escape=none '$remote_addr - $remote_user [$time_local] $request '
                             '$status $body_bytes_sent $http_referer '
                             ' $http_user_agent $http_x_forwarded_for '
                             ' $request_body ';

# json 포멧
log_format access_logs escape=json
 '{'
   '"time_local":"$time_local",'
   '"remote_addr":"$remote_addr",'
   '"http_x_forwarded_for":"$http_x_forwarded_for",'
   '"request":"$request",'
   '"status": "$status",'
   '"body_bytes_sent":"$body_bytes_sent",'
   '"request_time":"$request_time",'
   '"http_referrer":"$http_referer",'
   '"http_user_agent":"$http_user_agent",'
   '"connection":"$connection",'
   '"request_body":"$request_body"'
 '}';
# location 블록에 추가
access_log {로그를 떨굴 위치}/$time_local-access.log access_logs;

escape=none 옵션을 주지 않게 되면, default로 설정되게 되는데, 이는 아스키코드 기준 32보다 작거나 126보다 큰 문자를 \xXX 형태로 처리하기 때문에 설정해야 한다.(즉, 문자열이 이상하게 log에 찍힐 수 있다.) 추가적으로 json으로 하게되면, json으로 log를 변경하여 떨어뜨릴 수 있다. 우리 시스템은 json으로 저장하였다.

location 블록에서 $time_local설정을 넣게 되면, 로그 파일명은 날짜별로 "2023-07-29-15-access.log"와 같이 구성되며, 매 시간마다 새로운 로그 파일이 생성된다.

nginx -s reload, service nginx restart

이제 nginx 재시작을 해야하는데 여기서 유의할 점이 있다. 기존 시스템에서 운영중이라면, nginx -s reload를 사용해야한다. 또한 반드시 config 파일이 유효한지 체크하고 reload해야한다.

nginx -s reload

기존에 로드된 웹 서버의 프로세스를 중단하지 않고 새로운 설정을 적용

# config 파일이 유효한지 테스트
nginx -t

# Nginx 웹 서버의 설정 파일을 다시 불러오는 작업을 수행.
nginx -s reload

service nginx restart

이 명령어는 Nginx 웹 서버를 완전히 중단하고 다시 시작. 현재 실행 중인 Nginx 프로세스를 종료하고 새로운 프로세스를 시작하여 새로운 설정을 적용하기 때문에, 잠시 웹 서버가 중단되는 downtime이 발생하기 때문에, nginx -s reload를 사용해야 한다.

service nginx restart

따라서 운영중이라면 nginx -s reload를 사용해야한다.

Elasticsearch, Kibana 설치

elasticsearch, kibana, cerebro 를 Docker로 올려보자!
이 블로그를 참조하여 Docker 위에 elasticsearch와 kibana를 띄웠다. 컴퓨터 한대를 사용하여 elasticsearch와 kibana를 실행하는 서버를 한대 두었다.

로그 데이터 이동

Logstash → filebeat로 대체하여 사용하였다. 왜나하면, Logstash는 필터링 기능 등의 다양한 기능들을 해주는데, 사실상 이러한 기능까지 필요하지는 않았고, 또한 Logstash는 경량 프로세스가 아니였다. 따라서 이를 운영서버에 설치하는 것은 운영환경에 어느정도의 위험성을 줄 수 있다고 생각하였다. 따라서 filbeat를 설치하여 로그 데이터를 옮기려고 하였다.

# filebeat 설치
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
sudo apt-get install apt-transport-https

# Filebeat 7.6.2 버전으로 변경 (deb URL 수정)
echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list

sudo apt-get update
sudo apt-get install filebeat=7.6.2

# 아래 명령어를 통해 잘 깔아졌는지 확인
filebeat version
# Filebeat의 구성 파일(/etc/filebeat/filebeat.yml)을 편집
filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /var/log/nginx/*.log # 로그가 저장된 위치. 모을 log
  # 24시간 넘은 log 삭제 -> 원한다면 추가
  #clean_inactive: 24h

output.elasticsearch:
  hosts: ["xxx.xxx.xx.xxx:9200"] # 본인의 elasticsearch ip를 입력
  protocol: "http"
# file beat config파일이 정상적인지 테스트
sudo filebeat test config -c /etc/filebeat/filebeat.yml


# filebeat 서비스 시작
sudo service filebeat start

# filebeat의 오류 메시지를 보고 싶은 경우 (서비스로 돌리지 않고 싶은 경우) 아래 명령어를 통해 시작
sudo filebeat -e -c /etc/filebeat/filebeat.yml -d "publish"

이렇게 한 후, kibana의 톱니바퀴 버튼 -> index management에서 데이터가 들어와진게 확인이 된다면 성공이다.

하지만, 여기서 본인의 경우는 오류가 발생했었는데, Ubuntu 22.04 이상의 버전에서 생기는 문제였다. 아래에 이를 정리해 두었다.
Filebeat Ubuntu 22.04 이상의 버전에서의 cgo: pthread_create failed 오류

Elasticsearch와 kibana에 암호 적용

이제 filebeat와 client가 Elasticsearch와 kibana에 접근을 하게 될텐데, 여기서 문제가 있었다. 로그 데이터는 아무나 함부로 보면 안되는데,
(1) 패킷을 탈취당할 시 암호화가 되어있지 않기 때문에 훔쳐볼 가능성이 있었다.
(2) 아이디/패스워드가 없을시 아무나 접근하여 Elasticsearch의 데이터를 볼 수 있다. 따라서 인증 체계를 만들어야했다.

HTTPS 적용

(1) 문제를 해결하기 위해 Elasticsearch와 kibana에 Https를 적용하기로 하였다. 아래 elasticsearch의 참조 문서를 통해 적용했다.
참조 : https://www.elastic.co/kr/blog/configuring-ssl-tls-and-https-to-secure-elasticsearch-kibana-beats-and-logstash

cd ~
mkdir -p tmp/cert_blog
cd tmp/cert_blog
echo "
instances:
  - name: docker-cluster
    dns:
      - xx.xx.xx.xx # DNS를 설정하지 않았으므로 elasticsearch IP를 입력
    ip:
      - xx.xx.xx.xx # elasticsearch가 실행되고 있는 서버IP 입력
  - name: kibana
    dns:
      - xx.xx.xx.xx # DNS를 설정하지 않았으므로 kibana IP를 입력
    ip:
      - xx.xx.xx.xx # kibana가 실행되고 있는 서버IP 입력
" >> instance.yml

cd /usr/share/elasticsearch
bin/elasticsearch-certutil cert --keep-ca-key --pem --in ~/tmp/cert_blog/instance.yml --out ~/tmp/cert_blog/certs.zip

cd ~/tmp/cert_blog
unzip certs.zip -d ./certs

cd /usr/share/elasticsearch/config/
mkdir certs
cp ~/tmp/cert_blog/certs/ca/ca* ~/tmp/cert_blog/certs/docker-cluster/* certs

vi elasticsearch.yml

cluster.name: "docker-cluster"
network.host: 0.0.0.0
xpack.security.enabled: true # 비밀번호를 위한 설정
xpack.security.http.ssl.enabled: true
xpack.security.transport.ssl.enabled: true
xpack.security.http.ssl.key: certs/docker-cluster.key
xpack.security.http.ssl.certificate: certs/docker-cluster.crt
xpack.security.http.ssl.certificate_authorities: certs/ca.crt
xpack.security.transport.ssl.key: certs/docker-cluster.key
xpack.security.transport.ssl.certificate: certs/docker-cluster.crt
xpack.security.transport.ssl.certificate_authorities: certs/ca.crt

# 이후 도커 재시작
docker elasticsearch restart

비밀번호 설정을 진행한다.

bin/elasticsearch-setup-passwords interactive -u "https://xx.xx.xx.xx:9200"

# 이 인증서 파일을 옮긴다. (꺼낸 후, kibana에 넣어주기)
docker cp elasticsearch:/usr/share/elasticsearch/config/certs C:\Users\username\Desktop
docker cp C:\Users\username\Desktop\certs kibana:/usr/share/kibana/config 

# Kibana도 변경
vi kibana.yml 
server.name: kibana
server.host: "0"
server.ssl.enabled: true
server.ssl.certificate: /usr/share/kibana/config/certs/docker-cluster.crt
server.ssl.key: /usr/share/kibana/config/certs/docker-cluster.key
elasticsearch.hosts: [ "https://xx.xx.xx.xx:9200" ]
elasticsearch.username: "아이디"
elasticsearch.password: "비밀번호"
elasticsearch.ssl.certificateAuthorities: [ "/usr/share/kibana/config/certs/ca.crt" ]

이제 filebeat의 설정을 편집한다.

# filebeat.yml 편집
mkdir /etc/filebeat/certs
chmod 777 /etc/filebeat/certs
sudo scp /home/username/ca.crt username@xx.xx.xx.xx:/etc/filebeat/certs

# filebeat.yml
filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /var/log/nginx/*.log
  ignore_older: 48h  # 2일 (48시간) 이후의 데이터를 무시

output.elasticsearch:
  hosts: ["https://xx.xx.xx.xx:9200"]
  ssl.certificate_authorities:
    - /etc/filebeat/certs/ca.crt
  username:
    유저명
  password:
  	비밀번호

또한 이 ca.crt 인증서를 크롬에서 사용할 수 있도록 등록해야한다. 루트 도메인 인증서로 등록하였다.

아래 메시지가 뜨지 않으면 정상이다.
{"type": "server", "timestamp": "2013-01-28T02:56:48,922Z", "level": "WARN", "component": "o.e.x.s.t.n.SecurityNetty4HttpServerTransport", "message": "received plaintext http traffic on an https channel, closing connection Netty4HttpChannel"}

추가

추가적으로 더 해야할 부분은 kibana의 위험감지시 메일을 보내주는 부분, 등등을 진행할 필요성을 느낀다.

여담 (실패-타협한 부분)

현재 서비스의 코드에서는 HTTP STATUS CODE로 오류 상황을 처리하는 것이 아니라, response body에 “result_code“를 담아 처리하고 있었다. 즉 오류 상황이 일어나더라도, HTTP STATUS CODE는 200으로 잘 나왔기 때문에 분류가 불가능했다.

{
    "result_code": "101"
}

해결책

lua script를 통해 response body에 있는 result_code를 추가한다. ⇒ 실패

기존의 돌아가고 있는 nginx에서 luaScript를 읽기 위해서는 LuaJIT를 추가해야하는데, 기본적으로 기존 시스템을 건들여야 했으므로 하지 않았다.

그리고 추가적으로 result_code를 입력하는 것이 굳이 큰 의미가 없을 것으로 결론 냈다. 왜냐하면 사실상 result_code를 보내준다는건, 우리가 예상한 범위 내에서 프로그램이 작동한다는 결론을 내렸기 때문이다. 또한, lua script를 실행하면서, 어쨌든 메모리/CPU를 조금이라도 아주 소량이지만 더 쓰게 될 텐데, 운영환경을 건드리는걸 굳이 원하지 않았다.

profile
노력하는 사람

0개의 댓글