배경: Docker Swarm 환경에서의 로그 수집
회사에서 서버 이전 작업을 진행하면서 기존 서비스들을 Docker Swarm 위에 올리는 작업을 수행하게 되었습니다.
동시에 구성해 놨던 Loki + Grafana 환경에 전체 컨테이너 로그를 모니터링할 수 있도록 연동하는 과제가 주어졌습니다.
fluent-bit란?
fluent-bit는 lightweight 로그 수집 도구로, 다양한 로그 소스를 수집한 뒤 태그를 붙이고,
필터링하여 원하는 로그 저장소로 전송할 수 있도록 도와주는 log collector입니다.
공식 사이트에서는 다음과 같이 소개하고 있습니다.
로그를 수집하고, tag를 붙이고, 필터를 통해 가공한 뒤 Loki, Elasticsearch, Kafka, Splunk 등 다양한 저장소로 전송할 수 있습니다.
이 모든 기능을 단일 binary로 제공하며, 메모리 사용량도 적어 리소스가 한정된 환경에서도 사용하기 적합합니다.
시스템 구성
시스템 구성은 다음과 같습니다.
- Docker Swarm은 다음과 같이 구성되어 있습니다.
- manager, node1, node2
목표
- 모든 Docker 노드의 로그를 Loki로 수집
- 로그에 서비스 이름 라벨 부여
- fluent-bit 컨테이너는 각 노드당 1개만 실행
작업 설명을 들을때도, 이전 springboot -> promtail -> loki를 예제로 다뤄봤기 때문에 별 어려움이 없을 거라 생각했습니다만...
문제 발생
Docker 로그는 다음 경로에 저장됩니다.
/var/lib/docker/containers/<container_id>/<container_id>-json.log
하지만 다음과 같은 문제가 존재합니다.
- 컨테이너 ID는 알 수 있지만, docker swarm 서비스 이름은 알 수 없음
- Docker API를 통해 swarm의 서비스 이름을 조회할 수 없음
- 로그 내용에도 서비스 이름 정보가 없음
해결방법
fluent-bit의 경우, 지정된 config 파일을 읽고 파일내 지정된 대로 pipeline을 구성해 데이터를 수집 및 전달하게 됩니다.
각 설정은 input, filter, output으로 나뉘고, 각 단계별로 document에 적힌 대로 시스템 내 built-in 된 api를 이용하거나
lua 언어로 전처리를 할수 있게 되어 있습니다.
제가 작성한 fluent-bit.conf를 같이 보자면
[SERVICE]
Flush 1
Log_Level info
[INPUT]
Name tail
Path /var/lib/docker/containers/*/*.log
Parser docker
Docker_Mode On
Tag docker.<file_name>
Tag_Regex (?<file_name>[a-z0-9]*)-json.log
[FILTER]
Name lua
Match docker.*
Script docker-metadata.lua
Call enrich_with_docker_metadata
[OUTPUT]
Name loki
Match docker.*
Host <loki_server_ip>
Port <loki_server_port>
Labels job=fluent-bit
label_keys $service_name
input으로 tail을 이용해 실제 container별 로그에 접근하고, 값을 읽어오게끔 합니다. 이때 수집되는 로그에 tag를 붙여주는데
양식은 docker.container_id가 되게끔 구성해 줍니다.
filter로 lua를 사용하고, 사용될 lua는 docker-metadata.lua입니다.
원하는 작업은 docker container_id => service_name으로 바꾸는 작업이 됩니다.
(물론 lua 언어는 굉장히 생소하고, 들어본 적도 없기 때문에 아래 정보를 참고했고, gpt에게 부탁했습니다;;)
- https://lannex.github.io/blog/2020/EFK-Fluentd-Fluentbit/
- https://github.com/fluent/fluent-bit/issues/1499
lua 코드
local DOCKER_VAR_DIR = '/var/lib/docker/containers/'
local CONFIG_SUFFIX = '/config.v2.json'
local DOCKER_CONTAINER_METADATA = {
['docker.container_name'] = '"Name"%s*:%s*"/?(.-)"',
['docker.container_image'] = '"Image"%s*:%s*"/?(.-)"',
['docker.container_started']= '"StartedAt"%s*:%s*"(.-)"',
['service_name'] = '"cohttp://m.docker.swarm.service.name"%s*:%s*"(.-)"'
}
local cache = {}
local function read_meta(container_id)
local path = DOCKER_VAR_DIR .. container_id .. CONFIG_SUFFIX
local fh = io.open(path, 'r')
if not fh then return nil end
local meta = {}
for line in fh:lines() do
for key, regex in pairs(DOCKER_CONTAINER_METADATA) do
local v = line:match(regex)
if v then meta[key] = v end
end
end
fh:close()
if next(meta) then
cache[container_id] = meta
return meta
end
return nil
end
function enrich_with_docker_metadata(tag, ts, record)
local container_id = tag:match('docker%.([0-9a-f]+)$')
if not container_id then return 0 end
local out = {}
for k,v in pairs(record) do out[k] = v end
out['docker.container_id'] = container_id
local meta = cache[container_id] or read_meta(container_id)
if meta then
for k,v in pairs(meta) do
out[k] = v
end
end
return 2, ts, out
end
output으로는 로그를 보낼 loki 서버를 지정해 줍니다. loki 내 service 별로 구분하기 위해 label_keys를 지정해 적어줍니다.
실제로 회사 내 loki+grafana 확인 시 동작하는 걸 확인했고, 다 적용하고 나니 요새 고민하던 것에 적용해야겠다고 생각이 들었습니다.
실제 적용 예시: 개인 Spring Boot 프로젝트
기존 Spring Boot 프로젝트의 로그도 Loki로 연동하고 싶었던 참에, 이번 구성이 적용 가능했습니다.
기존 logback 기반에서 log4j로 변경할 필요 없이, 외부 로그 파일만 잘 수집해 주면 되는 구조 덕분에
모든 Docker 컨테이너의 로그를 일관되게 수집할 수 있었습니다.
구성 순서 요약
- Loki 컨테이너 실행 (MinIO 연동 포함)
- 주의사항: config.yml에서 S3 연동 설정 시 MinIO endpoint, access key, bucket 설정이 정확히 일치해야 함
- 실패 시: Loki가 부팅되자마자 종료된다면 volume mount 경로나 credentials 문제일 수 있음
- Grafana 컨테이너 실행 및 Loki와 연동
- 주의사항: Grafana 첫 로그인 시 기본 계정(admin/admin) 변경 필요
- 실패 시: Data Source에 Loki URL이 잘못되어 있거나 Loki가 아직 부팅되지 않은 상태에서 Grafana가 먼저 붙으려 한 것일 수 있음
- Fluent-bit 구성 및 로그 수집 확인
- 주의사항: fluent-bit.conf의 Tag와 Filter 매칭 규칙이 실제 로그 경로 및 Lua 필터와 맞는지 반드시 확인
- 실패 시: 아무 로그도 나타나지 않는 경우, Tag_Regex나 Lua 필터의 정규식 오류일 수 있음 연동 포함)
- Grafana 컨테이너 실행 및 Loki와 연동
- Fluent-bit 구성 및 로그 수집 확인
docker-compose.yml은 다음과 같습니다.
services:
loki:
image: grafana/loki:2.9.3
container_name: loki
restart: always
ports:
- "3100:3100"
command: -config.file=/etc/loki/config.yml
volumes:
- ./loki/config.yml:/etc/loki/config.yml
depends_on:
- minio
networks:
- hoonzi_network
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: always
ports:
- "3000:3000"
depends_on:
- loki
networks:
- hoonzi_network
fluent-bit:
image: fluent/fluent-bit:4.0.3
container_name: fluent-bit
restart: always
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./fluent-bit:/fluent-bit/etc
ports:
- "2020:2020"
depends_on:
- loki
networks:
- hoonzi_network
잘 올라왔는지 확인
grafana에서 loki 랑 연결
fluent-bit.conf도 마찬가지로 작성한 뒤,
설정하고 docker-compose up -d 실행 시 loki로 인입 및 grafana에 표시되는지 확인하면 됩니다.
마무리
이 구성을 통해 Docker Swarm 환경에서도 컨테이너별 로그를 서비스 단위로 분류하고, Loki + Grafana를 활용해 시각화 및 검색까지 가능해졌습니다. 설정은 다소 복잡하지만, 일단 구축하면 운영 편의성이 크게 향상됩니다.
실제 운영 중에는 다음과 같은 개선 효과가 있었습니다.
- 기존 potainer의 경우, 로그 확인 시 여러 노드의 로그를 시간정렬 없이 가져오기 때문에 보기 복잡했다면 서비스에서 간헐적으로 발생하는 오류 로그를 빠르게 탐지하고, 시간대별로 필터링하여 분석하는 데 큰 도움이 되었습니다.
- 한 노드에서만 발생하는 에러도 쉽게 확인할 수 있었고, 컨테이너 ID가 아닌 서비스 단위로 구분되다 보니 원인 파악이 훨씬 빨라졌습니다.
'text > common' 카테고리의 다른 글
Docker & Registry 설치 및 문제 해결 후기 (0) | 2025.02.12 |
---|---|
Docker Registry를 이용해 사설 private image서버 설정 (0) | 2025.01.11 |
PLG stack 찍먹! (3) | 2024.11.21 |
mysql order by equal? (2) | 2023.12.05 |
maven multi binding 문제 시 해결 (1) | 2023.10.12 |
댓글