23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
안녕하세요, dragonpipe입니다.
이 번에는 AKS(Azure Kubernetes Service) 상용 서비스에 적용한 후기 다섯 번째 내용으로, k8s 환경에서 로그를 통합 관리하게 해주는 EFK 기술에 대해 소개드리려고 합니다.
우선 EFK는 ElasticSearch, Fluentd, Kibana의 줄임말인데요. 이 세 가지 기술을 사용하는 아키텍처는 여러가지가 있습니다.
그 중에서 서비스에 맞는 아키텍처를 선정하여 진행하면 됩니다.
Use Case #1: 각 Node에 Fluentd를 데몬셋으로 설치하고 개별적으로 ElasticSearch로 전송
Use Case #2: 각 Pod에 Fluentbit을 Sidecar로 설치하고 Fluentd에서 통합 수집하여 ElasticSearch로 전송
Use Case #3: 각 Pod에 Fluentbit을 Sidecar로 설치하고 Kafka를 Fluentd와의 중간에 배치하여 Log손실을 방지
Use Case #4: 각 Application Server에 쉐어드 볼륨을 마운트하고 Fluentd에서 Log적재 파일을 직접 읽어 드림
그 중에서 일반적으로 Use Case 1번과 Use Case 2번을 많이 적용하는데요. Fluentd를 데몬셋으로 설치하는 방안(1번 케이스)의 장단점에 대해 알아보겠습니다.
장점
개별 Pod에 Sidecar로 설치하는 것 보다 리소스 경량화 가능 (Pod 마다 Sidecar를 설치하는데 소요되는 자원 절약)
fluentbit은 경량이긴 하나 fluentd에서 제공하는 다양한 필터링 기능 사용 불가 (특정 namespace 혹은 pod 로그만 수집 등)
단점
콘솔에 출력되는 stdout/stderr를 container 로그로 저장하므로 콘솔 로그를 수집에 맞게 Json 형식으로 출력해야 함
Daemonset resource 관리가 어려움. (Log생성이 많은 Pod가 특정 노드에 몰릴경우 발생.)
각 아키텍처의 장단점을 비교 분석한 결과, 저희 서비스는 아래와 같이 "Fluentd를 데몬셋으로 설치"하는 아키텍처를 구성하게 되었습니다.
저희는 ElasticSearch와 kubernetes에 맞춤형 image인 "fluentd-kubernetes-daemonset-debian-elasticsearch"를 사용하여 쿠버네티스에 데몬셋으로 설치하였습니다.
설치하는 방법은 아래와 같습니다.
yaml 파일 작성
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
k8s-app: fluentd
name: fluentd
namespace: kube-system
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: fluentd
rules:
- apiGroups:
- ""
resources:
- "namespaces"
- "pods"
verbs:
- "list"
- "get"
- "watch"
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: fluentd
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: fluentd
subjects:
- kind: ServiceAccount
name: fluentd
namespace: kube-systemServiceAccount, ClusterRole, ClusterRoleBinding 생성
k apply -f fluentd-service.yaml
ServiceAccount, ClusterRole, ClusterRoleBinding 생성 확인
k get serviceaccount fluentd -n kube-system
k get clusterrole fluentd -n kube-system
k get clusterrolebinding fluentd -n kube-system
yaml 파일 작성
namespace : kube-system으로 설정 필요
data : file 형태로 저장
data 하위에 key로 적은 문자열 (예 : fluent.conf) : 파일명
| 이후 내용 : txt 형태로 파일의 내용이 됨
<filter> 설정 : filter란 이벤트 처리를 위한 중간 pipeline으로서, 생성할 경우 정의한 문자열 규칙과 동일하게 tag를 지정한 이벤트가 들어오면 로직을 처리 (예 : kubernetes.var.log.containers.**이면 tag로 kubernetes.*로 지정할 경우 filter에 걸림)
type : parser로 설정 (json parse를 위해)
<parse> 설정
type : json으로 설정
key_name : log로 설정
key_name을 log로 설정하는 이유
데몬셋 parse 규칙 : /^(?<time>.+) (?<stream>stdout|stderr)( (?<logtag>.))? (?<log>.*)$/
마지막 (?<log>.*)$에 해당하는 부분이 실제 application에서 쌓는 로그이며 json 형태로 되어 있음
<filter></filter> 없이 ES로 발송할 경우 json 전체가 문자열로 넘어감
해당 json을 ES에서 관리하는 key:value로 인식시키려면 parse 해서 전달해야 함
kubernetes.conf를 보면 id가 "in_tail_container_logs"인 소스를 "kubernetes.xxx"로 보내고 있음 (설정하지 않을 경우 기본값이며 기본값이 "kubernetes.*"임)
filter로 받아서 log라는 key_name으로 json 문자열이 넘어오므로 key_name : log를 넣어서 json 문자열을 parsing 할 수 있도록 함
환경변수 주입하지 않고 config-map으로 생성한 이유
filter를 환경변수 주입으로 생성할 수는 없으므로 config map에 정의하여 config 파일을 생성
fluent.conf 파일에서 conf.d/xxx.conf 파일을 include 하도록 설정되어 있어서 daemonset.yaml 에서 volume mountpath를 conf.d/*.conf로 지정
filter 조건을 다른 문자열이 아닌 kubernetes.var.log.containers.** 로 설정한 이유
source의 tag를 임의의 환경변수로 주입하고 (예 : spring_log) 그 것으로 filter 조건으로 잡아도 걸림
하지만 kubernetes.**로 시작해야만 kubernetes.conf 에서 filter 되며 kubernetes 관련 수집 정보를 add 해주기 때문에 반드시 kubernetes로 시작하는 필터 조건을 잡아야 함
retry_count 설정 추가
기본적으로 4회 retry 하도록 되어 있으나 여러번 무한히 반복하여 retry 하는 경우가 탐지되어 3회로 지정하는 부분 추가
apiVersion: v1
kind: ConfigMap
metadata:
name: fluentd-config
namespace: kube-system
data:
fluent.conf: |
<filter kubernetes.var.log.containers.**>
@type parser
<parse>
@type json
json_parser json
</parse>
replace_invalid_sequence true
emit_invalid_record_to_error false
key_name log
reserve_data true
retry_count 3
retry_limit 3
</filter>config-map 생성
k apply -f fluentd-config.yaml
config-map 생성 확인
k get configmap -n kube-system
yaml 파일 작성
image : fluent/fluentd-kubernetes-daemonset:v1.11.5-debian-elasticsearch7-amd64-1.0
namespace : kube-system으로 설정 필요
환경변수 설정 (env)
TZ : Timezone 설정 (Asia/Seoul)
FLUENT_ELASTICSEARCH_HOST : ES의 주소 (현재는 STG ES로 설정)
FLUENT_ELASTICSEARCH_PORT : 통신할 포트 (9200이 기본값)
FLUENT_ELASTICSEARCH_SCHEME : HTTPS/HTTP 중 택1
FLUENTD_SYSTEMD_CONF : systemd.conf 파일을 사용할 것인지 여부
FLUENT_CONTAINER_TAIL_EXCLUDE_PATH : 로그 수집에서 제외할 경로 (현재는 fluentd 로그를 제외시킴)
FLUENT_CONTAINER_TAIL_PARSER_TYPE : CONTAINER LOG를 수집할 때 parse 규칙 (없으면 json이 기본). 로그가 json 형식이면 parse에 문제가 없지만 spring log를 json 포맷으로 바꿔도 "pattern not matchced" 에러 발생.
원인은 컨테이너 로그가 "time + stdout/stderr + F + JSON" 형태로 구성되어 있음. 따라서 이 형식에 맞게 아래와 같이 parse 규칙을 지정해줘야 "pattern not mathced" 에러 발생하지 않음
/^(?<time>.+) (?<stream>stdout|stderr)( (?<logtag>.))? (?<log>.*)$/
volume mount 설정 (volumeMounts, volumes)
varlog 설정 : Node에 저장되는 컨테이너 로그의 위치가 /var/log/containers/에 쌓이므로 /var/log 추가 필요(volume 설정에서 hostpath로 설정하는 이유는 실제 로그가 쌓이는 위치가 pod 내부가 아니라 node의 hostpath에 쌓이기 때문)
dockercontainerlogdirectory 설정 : /var/log/pods 위치에 저장되는 로그를 ReadOnly로 읽기 위해 추가
config path 설정 : fluentd를 구동하면 자체적으로 생성해주는 conf 파일이 있지만 그 외 추가로 필요한 conf 파일이 있을 경우 configmap을 volume으로 추가하여 conf 파일 생성(자동 생성되는 fluent.conf를 보면 conf.d/*.conf를 include 하고 있음. 따라서 mountpath를 conf.d/로 하여 해당 경로에 우리가 직접 설정한 conf 파일이 생성되도록 설정)
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd
namespace: kube-system
labels:
k8s-app: fluentd-logging
version: v1
kubernetes.io/cluster-service: "true"
spec:
selector:
matchLabels:
k8s-app: fluentd-logging
template:
metadata:
labels:
k8s-app: fluentd-logging
version: v1
kubernetes.io/cluster-service: "true"
spec:
serviceAccount: fluentd
serviceAccountName: fluentd
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
containers:
- name: fluentd
image: fluent/fluentd-kubernetes-daemonset:v1.11.5-debian-elasticsearch7-amd64-1.0
env:
- name: TZ
value: Asia/Seoul
- name: FLUENT_ELASTICSEARCH_HOST
value: "20.214.65.78"
- name: FLUENT_ELASTICSEARCH_PORT
value: "9200"
- name: FLUENT_ELASTICSEARCH_SCHEME
value: "http"
- name: FLUENTD_SYSTEMD_CONF
value: disable
- name: FLUENT_CONTAINER_TAIL_EXCLUDE_PATH
value: /var/log/containers/fluent*
- name: FLUENT_CONTAINER_TAIL_PARSER_TYPE
value: /^(?<time>.+) (?<stream>stdout|stderr)( (?<logtag>.))? (?<log>.*)$/
resources:
limits:
memory: 200Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts:
- name: varlog
mountPath: /var/log
- name: dockercontainerlogdirectory
mountPath: /var/log/pods
readOnly: true
- name: config
mountPath: /fluentd/etc/conf.d
terminationGracePeriodSeconds: 30
volumes:
- name: varlog
hostPath:
path: /var/log
- name: dockercontainerlogdirectory
hostPath:
path: /var/log/pods
- name: config
configMap:
name: fluentd-configkubernetes 배포
k apply -f fluentd-daemonset.yaml
daemonset 확인
k get damonset fluentd -n kube-system
k get pods -n kube-system | grep 'fluentd*'
Fluentd를 데몬셋으로 설치하면 노드에 설치되므로 노드에 저장되는 container.log를 ElasticSearch로 이관해 줍니다.
container.log에는 was에서 쌓는 stdout/stderr 로그들이 저장되는데요. ElasticSearch가 읽을 수 있도록 json으로 formatting 해주어야 합니다.
이번에는 Spring에서 로그를 json으로 변환하는 방법에 대해 알아보겠습니다.
물론 logback.xml 설정에서 json 포맷을 만들어 줄 수 있습니다. 하지만 logstash-logback-encoder라는 좋은 라이브러리가 있어서 pom.xml에 의존성을 추가하였습니다.
<!-- https://mvnrepository.com/artifact/net.logstash.logback/logstash-logback-encoder -->
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>6.3</version>
</dependency>logback.xml에 아래와 같이 encoder를 활용하도록 appender를 만들어줍니다.
만약 local 환경에서는 EFK 환경이 아니므로 console log를 line으로 쌓고 싶다고 하면 profile별로 console 방식 또는 json 방식을 아래와 같이 선택하여 사용할 수 있습니다.
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- line format -->
<property name="CONSOLE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) [%X{traceId}] [%X{userIdx}] [%X{nftLog}] %magenta(%-4relative) --- [ %thread{10} ] %cyan(%logger{20}) : %msg%n"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>
${CONSOLE_LOG_PATTERN}
</Pattern>
</layout>
</appender>
<!-- json format -->
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
<root>
<springProfile name="local">
<appender-ref ref="CONSOLE" />
</springProfile>
<springProfile name="dev,stg,live">
<appender-ref ref="JSON" />
</springProfile>
</root>
</configuration>Spring console log가 json 형태로 수정되었는지 확인
이제 로그를 ElasticSearch에 잘 쌓았으니 Kibana로 확인할 일만 남았는데요.
Kibana로 로그가 잘 쌓이고 있는지 확인하는 방법과 로그를 모니터링할 때 사용하는 몇가지 팁을 소개드리겠습니다.
indices 확인
kibana 접속 → 좌측 상단 햄버거 아이콘 클릭 → Dev Tools 클릭 → GET _cat/indices 명령어 수행 → logstash-YYYY.MM.DD(오늘날짜) 확인
index pattern 설정
Discover 접속 → index pattern이 logstash-*로 되어 있는지 확인
logstash-*로 설정하는 이유 : fluentd에서 logstash_format을 true로 할 경우 index_name 파라미터가 무시되며 logstash-YYYY.MM.DD 형식으로 자동 부여 됨
index명을 수정하고 싶을 경우 daemonset.yaml 파일에 환경변수를 추가하여 logstash_format을 false로 주입하고 index_name으로 변경하면 됨
로그 발생 유도
Refresh 버튼 클릭하여 로그 데이터 조회
좌측 "Available Fileds"에서 원하는 필드를 추가
추천하는 필드
level - ERROR/WARN/INFO/DEBUG/...
kubernetes.labels.app - app label 명
message - 실제 로그
로그 이력 기간 조건 및 refresh 조건 추가
Refresh every 설정 할 경우 자동으로 n초/분 마다 refresh 할 수 있다. (주기가 빠른 경우 ES와 kibana에 부하를 줄 수 있으므로 주의!)
자주 사용하는 쿼리 저장
kubernetes.labels.app: "kpop-api"로 검색하면 label에 "kpop-api"가 포함된 것을 조회할 수 있음 (kpop-api의 label은 "ifland-kpop-api"임)
kubernetes.namespace_name: 환경(env)을 선택할 때 사용 (개발계-dev, 검수계-stg, 상용계-live)
보고 싶은 데이터 설정: 아래 예시에서는 시간(Time), 로그레벨(level), 로그내용(message), API call ID(dd.trace_id or traceId)로 구성
저장: 우측 상단 Save 버튼 클릭 → 제목과 설명 입력하고 저장 → 키바나 좌측 상단 세줄 아이콘 클릭 → Recently viewed에서 조회 가능 → 클릭 시 자동으로 쿼리 출력
Filter 기능 사용
필터 걸기 원하는 데이터에 마우스를 갖다 대면 Filter for value 아이콘 생성
"+" 버튼 클릭 시 filter에 추가 되며 해당 값과 동일한 로그 데이터만 검색 됨
추가된 필터는 검색 쿼리 입력창과 데이터 사이 공간에서 볼 수 있으며 x 아이콘을 클릭하여 제거할 수 있다. (아래 이미지 참고)
이번 글에서는 k8s에서 로그를 통합 관리하기 위해 사용한 EFK 기술 스택에 대해 알아보았습니다.
k8s 환경에서 log를 통합 관리 및 모니터링하기 위해 사용하는 기술 스택은 매우 다양한데요. (예: PLG, APM log trace 기능 등) 그 중에 상황과 선호에 맞게 사용하면 될 것 같습니다.
저희 경우에는 ElasticSearch가 검색 기능을 위해 이미 설치되어 있어서 EFK가 가장 현실적으로 좋은 기술스택이라고 판단하였습니다.
긴 글 읽어주셔서 감사합니다 : )
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.