
이 글은 Hart Hoover와 Ryan Walker가 공동으로 집필했습니다.
최근 Rackspace DevOps Automation 팀은 New Relic에서 발생한 알림을 Rackspace 지원 시스템으로 전달하는 서비스를 공개했습니다. 이 알림은 DevOps 엔지니어가 대응할 수 있도록 티켓을 자동 생성하며, 덕분에 고객은 새벽 3시에 울리는 알림 때문에 잠 못 이루는 일 없이 안심할 수 있습니다. 고객 환경에서 수집한 다른 데이터 포인트들과 결합하면, 엔지니어는 문제의 원인을 정확히 파악하고 적절한 조치를 신속하게 실행할 수 있습니다.
이 서비스의 인프라를 설계하는 과정에서 우리는 흔히 마주치지만 흥미로운 문제에 직면했습니다. 바로 보안상 Rackspace 내부 시스템에 대한 접근은 제한하면서도, 동시에 New Relic이 접속할 수 있는 공개 엔드포인트는 유지해야 한다는 점이었습니다. 우리의 해결책은 공개 API 엔드포인트와 비공개 워커(worker)를 완전히 분리된 구조로 설계하는 것이었습니다. 공개 API 엔드포인트는 New Relic에서 알림을 수신해 큐 역할을 하는 ObjectRocket Redis 인스턴스로 전달하고, 워커 서비스는 RackConnect 방화벽 뒤 내부망에서 실행되며 큐에서 메시지를 가져와 알림을 생성합니다.
이 방식은 환경을 매우 효과적으로 분리해 주지만, 로그 수집(log aggregation) 측면에서 새로운 과제를 낳았습니다. 우리는 비공개 환경 내부에 ElasticSearch/Kibana 스택을 운영하고 있습니다. 방화벽 안쪽에서는 fluentd를 사용해 로그를 ElasticSearch로 직접 전송하지만, 방화벽 바깥쪽에서는 EK 스택에 접근할 수 없습니다. 이를 해결하기 위해 우리는 fluentd로 공개 API 서비스의 로그를 ObjectRocket MongoDB 인스턴스로 전송하고, 내부에서는 다시 fluentd로 ObjectRocket에서 로그를 가져와 ElasticSearch에 적재하는 구조를 만들었습니다. 그 결과 환경 전체의 모든 활동 로그가 한곳에 모이게 되었습니다.
Fluentd란 무엇인가?
Fluentd는 데이터를 최대한 JSON 형태로 구조화하려는 오픈소스 데이터 수집기입니다. 덕분에 로그 데이터를 일관된 형식으로 맞추기 위해 여러 스크립트를 작성하고 관리할 필요가 없으며, 모든 데이터가 JSON으로 처리됩니다.
Fluentd의 진짜 강점은 다양한 소스(source)와 목적지(destination)를 지원한다는 점입니다. 예를 들어 Twitter 스트림에서 데이터를 수집해 IRC로 알림을 받는 식의 구성도 가능합니다. 커뮤니티에서 제공되는 플러그인 또한 매우 풍부합니다.
Docker와 함께 Fluentd 사용하기
MongoDB fluentd 플러그인을 사용하면 손쉽게 ObjectRocket으로 로그를 전송할 수 있습니다. 먼저 소스(source)를 정의해야 하는데, 우리의 모든 서비스가 Docker로 운영되기 때문에 컨테이너 로그를 fluentd로 넘겨야 합니다. Jason Wilder가 작성한 docker-gen과 fluentd를 활용한 Docker 로그 수집 가이드가 있으니 함께 참고하시면 좋습니다. fluentd 컨테이너가 실행되고 docker-gen이 fluentd 설정을 생성하면, 실행 중인 각 컨테이너마다 다음과 같은 섹션이 만들어집니다:
<source>
type tail
format json
time_key time
path /var/lib/docker/containers/abcdef/abcdef-json.log
pos_file /var/lib/docker/containers/abcdef/abcdef-json.log.pos
tag docker.container.abcdef
rotate_wait 5
</source>이 설정은 컨테이너 로그를 tail 방식으로 읽으면서, 위치 파일(pos_file)을 통해 어디까지 읽었는지 추적합니다. 주목할 점은 이 설정 섹션의 태그가 fluentd 태그라는 것입니다. 이 태그는 fluentd가 수집한 데이터를 어떻게 처리할지 결정하는 기준으로 사용됩니다.
MongoDB와 함께 Fluentd 사용하기
공개 쪽에서는 "match"를 통해 fluentd에게 데이터 처리 방식을 지시합니다. 같은 설정 파일에서 변수 부분을 실제 ObjectRocket 데이터베이스 정보로 교체하면 됩니다:
<match docker.**>
type mongo
database $DBNAME
collection prod
host $HOSTNAME
port $PORT
ssl
capped
capped_size 100m
user $MONGOUSER
password $MONGOPASS
include_tag_key true
</match>include_tag_key 설정은 fluentd가 MongoDB에 저장되는 로그 레코드에 태그를 포함하도록 합니다. 이렇게 하면 어떤 로그 항목이 어떤 컨테이너에 속하는지 정확히 추적할 수 있습니다. 이후 fluentd는 MongoDB에 데이터를 쌓기 시작하고, 우리는 애플리케이션의 비공개 쪽에서 이 데이터를 가져오면 됩니다.
비공개 쪽에서도 fluentd MongoDB 플러그인을 사용하지만, 이번에는 소스(source)로 설정합니다:
<source>
type mongo_tail
database $DBNAME
collection prod
host $HOSTNAME
port $PORT
user $MONGOUSER
password $MONGOPASS
ssl
time_key time
wait_time 5
tag prod
id_store_file /app/prod_last_id
</source>그다음, 로그를 ElasticSearch로 밀어 넣기 위한 "match"를 추가합니다:
<match **>
type forest
subtype elasticsearch
<template>
host elasticsearch.domain.com
port 9200
index_name fluentd
logstash_format true
buffer_type memory
type_name ${tag}
flush_interval 3
retry_limit 17
retry_wait 1.0
num_threads 1
</template>
</match>아울러 여러 환경에 걸친 태깅 설정을 단순화하기 위해 forest fluentd 플러그인도 함께 사용하고 있습니다.
Fluentd는 여러 호스트에 분산된 Docker 로그를 수집해 MongoDB 데이터베이스로 전송하기에 아주 훌륭한 도구입니다. 우리의 경우 ObjectRocket은 공개 환경과 비공개 환경 사이에서 로그를 중계하는 거점 역할을 합니다. 이 외에도 수집한 데이터를 실시간 분석하는 등 다양한 활용이 가능합니다. 우리 팀에게 가장 큰 장점은 ObjectRocket의 안정성과 전문성 덕분에 MongoDB를 직접 운영·관리할 필요가 없다는 점입니다.