이제 fluentd의 다양한 plugin들을 사용하여 로그들을 파싱하고 고도화해보도록 하자.
kubernetes에서는 docker지원을 공식 중단했다. 이유는 간단한데, 공식 CRI를 docker에서 미지원했기 때문이다.
이때 바뀌게 되는 부분이 하나 있는데, 바로 log format이다. 기존의 docker에서 지원하는 log format은 다음과 같은 json형식이다.
{"log":"INFO: \"GET /healthcheck HTTP/1.1\" 200 OK\n","stream":"stdout","time":"2023-12-07T06:56:33.076295527Z"}
따라서, fluentd를 사용할 때 source부분의 parse plugin으로 json을 사용하는 것이다.
<source>
@type tail
@id in_tail_container_logs
path "/var/log/pods/*/*/*.log"
pos_file "/var/log/fluentd-containers.log.pos"
read_from_head true
tag "**"
format json
time_format %Y-%m-%dT%H:%M:%S.%NZ
<parse>
time_format %Y-%m-%dT%H:%M:%S.%NZ
@type json
time_type string
</parse>
</source>
json으로 된 log data는 fluentd에서 기가막히게 parsing하지만, 안타깝게도 containerd, cri-o에서의 log format은 json이 아니다.
2023-12-07T06:56:33.076295527Z stdout F INFO: "GET /healthcheck HTTP/1.1" 200 OK
정리하면 다음과 같다.
<time> <stream> <logtag> <log>
F이후로는 모두 log이다. <time>, <stream>, <logta>, <log> 사이는 모두 빈 칸이 하나씩이다. 따라서, 위의 docker에서 parsing하던 json으로 해당 log를 parsing하면 다음의 에러만 반복될 것이다.
ParserError error="pattern not matched with data"
따라서, 다음과 같이 regular expression을 사용해주도록 하자.
<source>
@type tail
@id in_tail_container_logs
path "/var/log/pods/*/*/*.log"
pos_file "/var/log/fluentd-e2term-pod.log.pos"
read_from_head true
tag "**"
<parse>
@type multi_format
<pattern>
format regexp
time_key time
time_format %Y-%m-%dT%H:%M:%S.%N%Z
expression /^(?<time>.+) (?<stream>stdout|stderr) (?<logtag>.)? (?<log>.*)/
</pattern>
</parse>
</source>
위의 regular expression으로 crio, containerd의 log를 parsing하면 된다. record로 time, stream, logtag, log가 저장된다. 다행인 것은 time의 format이 docker일 때와 동일하기 때문에 이전과 동일한 time_format을 쓰면 된다.
결과로 다음과 record가 만들어지게 된다.
{
"time": "2023-12-07T06:56:33.076295527Z",
"stream": "stdout",
"logtag": "F",
"log": "INFO: \"GET /healthcheck HTTP/1.1\" 200 OK"
}
log를 파싱하여 원하는 형식으로 record를 만들어주는 것이 좋다. 그래야 elasticsearch에서 검색하기가 좋기 때문이다.
log가 json으로 되어있다면 아주 happy한 경우이다. 다음과 같은 filter만 만들어주면 되기 때문이다.
<filter **>
@type parser
key_name log
<parse>
@type json
json_parser json
</parse>
replace_invalid_sequence true
emit_invalid_record_to_error false
reserve_data true
</filter>
record key로 log를 json으로 parsing하겠다는 것이다. 그러나 모든 pod의 log가 json으로 되어있지 않을 수 있으므로 emit_invalid_record_to_error는 false로 두어서 parsing이 안되더라도 error로 남기지 않도록 한다. 또한, reserve_data는 true로 하여, log를 parsing 후에도 원본을 record에 남기도록 한다.
만약, log가 공백과 같은 특별한 구분자를 통해 형식화되었다면 regular expression을 사용하는 것이 좋다. 가령 다음의 log형식을 갖는다고 하자.
2023-12-07T06:56:33.076295527Z INFO original:96 Failed to read files.
위의 log를 형식화하면 다음과 같다.
<time>\t<level>\t<caller>\t<log>
다음과 같이 filter를 통해서 regular expression으로 log를 parsing할 수 있다.
<filter **>
@type parser
key_name log
<parse>
@type regexp
expression /^(?<time>.+)\t(?<level>.+)\t(?<caller>.+)\t(?<log>.*)/
time_format %Y-%m-%dT%H:%M:%S.%N%Z
</parse>
</filter>
record결과로 다음과 같이 나오게 된다.
{
"time": "2023-12-07T06:56:33.076295527Z",
"level": "INFO",
"caller": "original:96",
"log": "Failed to read files."
}
만약, 특정 record field를 추가하거나, 삭제하고 싶다면 record_transformer를 사용하면 된다.
<filter **>
@type record_transformer
enable_ruby
remove_keys $.content.time
<record>
tag ${tag}
</record>
</filter>
다음의 경우에는 record의 field로 content.time을 삭제하고, tag field를 만들어 놓는다. tag field값은 fluentd에서 사용하는 tag가 되는 것이다.
주의: 조심해할 것 중 하나는 parsing을 한 다음에 record에 특정 field를 삭제, 추가, 변경하는 것이 좋다. 만약 특정 field를 추가, 삭제, 변경한 다음에 parsing이 이루어지면, 변동사항은 남지않고 parsing된 결과가 덮어써지게 되는 수가 있다. 가령 위에서
tag를 추가한다음log를 parsing하면tagfield가 사라지게 된다.
이제 배포해서 사용하면 문제없이 구동될 것 같지만, 한 가지 문제가 남아있다. 바로 rollover이다. rollover가 무엇이냐하면, 현재 log를 받고 있는 elasticsearch index를 김밥마냥 말아(roll)버린다음에 저장하고, 새로운 index를 만들어 log들을 받겠다는 것이다.
[2023/12/06] pod logs -> fluentd -> elasticsearch -> index: server-000001
[2023/12/07] pod logs -> fluentd -> elasticsearch -> index: server-000002 (rollover: server-000001)
rollover된 index는 그냥 놔두거나 삭제할 수 있다. 일반적으로 data engineering에서 이러한 작업을 retention이라고 한다.
이렇게 rollover를 하는 이유는 아주 간단한데, 계속해서 index를 만들어 log를 저장하기에는 하드디스크의 자원이 한정적이기 때문이다.
elasticsearch에서 rollover는 직접 API를 사용해 요청을 보낼 수 있지만, 공식적으로는 ILM(index lifecycle management)를 적용해서 rollover가 자동으로 이루어지도록 추천하고 있다.
ILM은 index를 hot-warm-cold-frozen-delete 페이즈로 구분하여 관리하고 지정한 기간이 지나면 phase를 전황시키고 지정한 작업을 수행하도록 한다.
가령, 다음과 같이 설정할 수 있다. index가 처음 생성되면 hot phase로 넘어가고 하루 단위 또는 4GB의 index가 넘어가면 rollover시키도록 한다. rollover로 넘어간 index는 hot에서 warm phase로 넘아가도록 하고 warm phase에서는 읽기 전용으로 바꾸도록 한다. 생성한 지 7일이 지나면 cold phase로 넘어가도록 하며, cold phase로 넘어간 index는 저장만 하는 node로 이동시키도록 한다. 생성된 지 30일이 지난 index는 delete phase로 가도록 하며, delete phase로 전환된 index는 snapshot으로 백업되며 완료 후 삭제하도록 한다. (단, snapshot 저장은 유료 엔터프라이즈에서만 가능하다.)
각 phase의 컨셉을 정리하면 다음과 같다.
위의 phase는 공식 홈페이지에서 제안하는 phase일 뿐이지, 각 phase를 개인마다 다르게 관리해도 문제될 것은 없다.
이 phase로 넘어가는 단계의 조건는 시간과 용량이다. 가령 hot phase에서 6분 후에 delete phase로 보내거나, hot phase에서 10G가 넘어가면 delete phase로 넘기자는 것이다.
그런데 이 ILM에서 조심해야할 것이 있는데, 바로 ILM을 API를 통해 직접 index에 하나하나 적용시켜줄 수도 있지만 일반적으로 index-template를 할당해서 index-template의 pattern을 따르는 index만 ILM을 따르도록 할 수 있다.
|----------Index-------|
| server-1, server-2 | <---------index template(index pattern) <--------- ILM(rollover)
|----------------------|
index-template는 index의 pattern위주니까 index의 이름들이 server*라는 pattern을 따르고, server* index pattern을 가지는 index-template에 ILM을 할당해주면 자동으로 server-1, server-2들이 모두 rollover되겠지?? 싶지만 안된다.
안되는 이유에는 몇 가지 이유가 있다.
1. index의 이름은 마지막에 -{숫자}로 끝나야 한다. 가령 server-1-000001 이렇게 끝나야 한다. 만약 해당 규칙을 안따르면 illegal_argument_exception: index name does not match pattern '^.*-\d+$'가 발생한다.
2. rollover는 index이름이 아니라, alias로 결정된다. 따라서, index-template의 index pattern을 따르고 있는 index들이라도 alias가 index template에서 지정한 rollover_alias이어야 한다. 주의할 것은 index-template에서도 aliases를 설정할 수 있는데, 해당 부분의 alias이름이 rollover_alias이름과 동일하면 안된다.
정리하면 다음과 같다.
index template(index pattern: server*, rollover_alias: server) <--------- ILM(rollover)
|
↓
|---------------------Index------------------------|
| server-1(alias: server), server-2(alias: park) |
|--------------------------------------------------|
위의 예시와 같이 index template가 pattern으로 server*를 가지고 있어서 server-1, server-2 index가 모두 적용된 것을 알 수 있다. 그리고 index template는 ILM과 연결되어있는데, ILM에서 rollover를 수행한다고 하자.
문제는 server-1, server-2 모두 index template의 pattern을 따르지만, rollover_alias는 server만 해당하기 때문에 server-1은 alias가 server라 적용되는 반면 server-2는 alias가 park이라서 반영되지 않는다.
따라서, index마다 alias를 rollover ILM이 달린 index template의 rollover_alias로 설정해주어야 한다는 것이다.
그런데, 이걸 어떻게 fluentd에서 할 수 있는가?? 라고 한다면 막막하다. 이를 위해서 어느 2020년 용자가 issue를 만들고 contribution을 해주었다. https://github.com/uken/fluent-plugin-elasticsearch/issues/752
결론만 만들자면 자동으로 ILM을 만들고 index-template를 만들며, index에 rollover_alias와 동일한 alias를 달아준다. 이 덕분에 자동으로 rollver가 동작하도록 한다는 것이다.
fluentd의 elasticsearch plugin을 사용하면 되는데, docs만 봤다가는 적어도 일주일간 눈물의 삽질을 해야한다. 다음의 예시를 보고, 만들면 된다.
<match **>
@type elasticsearch
host $HOST_IP
port $HOST_PORT
log_es_400_reason true
logstash_format true
logstash_prefix server
type_name fluentd
application_name server
template_name index-template
template_file /fluentd/etc/index_template.json
enable_ilm true
ilm_policy_id retention-3m
ilm_policy {"policy":{"phases":{"hot":{"min_age":"0ms","actions":{"rollover":{"max_age":"3m","max_size":"5gb"}}},"delete":{"min_age":"3m","actions":{"delete":{}}}}}}
ilm_policy_overwrite false
</match>
뭔가 복잡하지만 ILM에 관련된 부분만 차례대로 보면 다음과 같다.
1. enable_ilm: default가 false인데 true로 바꾸지 않으면 구동되지 않는다.
2. ilm_policy_id: 생성할 ILM이름이다.
3. ilm_policy: ILM policy이다. elasticsearch에서 적용하는 ILM json형식과 동일하다. 참고로, 위의 예시는 rollover가 잘되는 지를 확인하기 위해 3분마다 rollover가 이루어지고 rollover된 index는 3분 뒤에 삭제된다는 의미이다.
4. ilm_policy_overwrite: 이름이 동일한 경우, overwrite할 것인지를 묻는다. 만약 false일 때, 이름이 동일한 ILM이 있다면 기존의 ILM을 적용하고 새로 만들지 않는다.
5. template_name: 생성할 index-template이름이다. 그러나 동작하지 않는 것으로 보이지만, 없으면 안만들어질 수도 있다. docs에는 기존에 template_name으로 index-template가 있다면 이를 적용한다고 하지만, 실제로 해봤는데 잘안된다...
6. template_file: 생성할 index-template의 정의가 있는 path이다.. elasticsearch와 동일한 정의를 가지는데, 그닥 중요한 부분은 없다. rollover_alias알아서 만들어주고 생성될 index와의 맵핑도 자동으로 해준다. 단, 없으면 rollover적용이 안된다
kibana에서 template를 만들 때는 다음과 같다.
{
"template": {
"settings": {
"index": {
"lifecycle": {
"name": "template",
"rollover_alias": "retention"
}
}
},
"aliases": {},
"mappings": {}
}
}
그런데 굳이 이렇게까지 만들 필요는 없다. elasticsearch plugin에서 자동으로 lim도 만들어주고 그 정보를 이용해서 template에 rollover_alias, name등을 다 설정해준다.
index_template.json의 정의는 다음과 같았다.
{
"index_patterns": [
"logstash-default*"
],
"settings": {
"index": {
"number_of_replicas": "1"
}
}
}
해당 index_template.json을 fluentd pod의 /etc/fluentd/index_template.json에 제공해야하므로 ConfigMap에 추가하고 Volume에 추가해야한다.
따라서 최종 daemonset은 다음과 같다.
apiVersion: v1
kind: Namespace
metadata:
name: fluentd
---
apiVersion: v1
kind: ConfigMap
metadata:
name: fluentd-config
namespace: fluentd
data:
index_template.json: |-
{
"settings": {
"index": {
"number_of_replicas": "1"
}
}
}
fluent.conf: |-
<source>
@type tail
@id in_tail_container_logs
path "/var/log/pods/*/*/*.log"
pos_file "/var/log/fluentd-log.pos"
read_from_head true
tag "**"
<parse>
@type multi_format
<pattern>
format regexp
time_key time
time_format %Y-%m-%dT%H:%M:%S.%N%Z
expression /^(?<time>.+) (?<stream>stdout|stderr) (?<logtag>.)? (?<log>.*)/
</pattern>
</parse>
</source>
<filter **>
@type parser
key_name log
<parse>
@type json
json_parser json
</parse>
replace_invalid_sequence true
emit_invalid_record_to_error false
reserve_data true
</filter>
<match **>
@type elasticsearch
host ${ELASTICSEARCH_HOST_IP}
port ${ELASTICSEARCH_HOST_REST_PORT}
log_es_400_reason true
logstash_format true
logstash_prefix ${LOGSTASH_INDEX_PREFIX}
type_name fluentd
application_name ${APP_NAME}
template_name index_template
template_file /fluentd/etc/index_template.json
enable_ilm true
ilm_policy_id retention-3m
ilm_policy {"policy":{"phases":{"hot":{"min_age":"0ms","actions":{"rollover":{"max_age":"3m","max_size":"5gb"}}},"delete":{"min_age":"3m","actions":{"delete":{}}}}}}
ilm_policy_overwrite false
</match>
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: fluentd
namespace: fluentd
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: fluentd
rules:
- apiGroups:
- ""
resources:
- pods
- namespaces
verbs:
- get
- list
- watch
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: fluentd
roleRef:
kind: ClusterRole
name: fluentd
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
name: fluentd
namespace: fluentd
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd
namespace: fluentd
labels:
k8s-app: fluentd-logging
version: v1
spec:
selector:
matchLabels:
k8s-app: fluentd-logging
version: v1
template:
metadata:
labels:
k8s-app: fluentd-logging
version: v1
spec:
serviceAccount: fluentd
serviceAccountName: fluentd
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
- key: node-role.kubernetes.io/master
effect: NoSchedule
containers:
- name: fluentd
image: fluent/fluentd-kubernetes-daemonset:v1-debian-elasticsearch
env:
- name: K8S_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
#- name: FLUENT_ELASTICSEARCH_HOST
# value: "xx.xx.xx.xxx"
#- name: FLUENT_ELASTICSEARCH_PORT
# value: "xxxxx"
#- name: FLUENT_ELASTICSEARCH_SCHEME
# value: "http"
# Option to configure elasticsearch plugin with self signed certs
# ================================================================
- name: FLUENT_ELASTICSEARCH_SSL_VERIFY
value: "true"
# Option to configure elasticsearch plugin with tls
# ================================================================
- name: FLUENT_ELASTICSEARCH_SSL_VERSION
value: "TLSv1_2"
# X-Pack Authentication
# =====================
- name: FLUENT_ELASTICSEARCH_USER
value: "elastic"
- name: FLUENT_ELASTICSEARCH_PASSWORD
value: "changeme"
resources:
limits:
memory: 200Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts:
- name: varlog
mountPath: /var/log
- name: dockercontainerlogdirectory2
mountPath: /var/lib/docker/containers
readOnly: true
# When actual pod logs in /var/log/pods, the following lines should be used.
- name: dockercontainerlogdirectory
mountPath: /var/log/pods
readOnly: true
- name: config
mountPath: /fluentd/etc/fluent.conf
subPath: fluent.conf
- name: config
mountPath: /fluentd/etc/index_template.json
subPath: index_template.json
terminationGracePeriodSeconds: 30
volumes:
- name: varlog
hostPath:
path: /var/log
# When actual pod logs in /var/lib/docker/containers, the following lines should be used.
# - name: dockercontainerlogdirectory
# hostPath:
# path: /var/lib/docker/containers
# When actual pod logs in /var/log/pods, the following lines should be used.
- name: dockercontainerlogdirectory
hostPath:
path: /var/log/pods
- name: dockercontainerlogdirectory2
hostPath:
path: /var/lib/docker/containers
- name: config
configMap:
name: fluentd-config
입력이 필요한 부분은 ConfigMap에서 환경변수로 표시해두었으니 추가해주도록 하자. ex) ${ELASTICSEARCH_HOST_IP}, ${ELASTICSEARCH_HOST_REST_PORT}
위 deamonset을 만든다음에 3분마다 kibana의 index management tab에 가서 rollover되고 오래된 데이터는 삭제되는지 확인해주도록 하자. 단, elasticsearch의 ILM poll은 default가 10분이다. 따라서, 결과를 바로바로 확인하고 싶다면 다음과 같이 poll_interval을 10초로 바꿀 수 있다.
PUT /_cluster/settings
{
"transient" : {
"indices.lifecycle.poll_interval" : "10s"
}
}
필자는 ${LOGSTASH_INDEX_PREFIX}에 park이라는 index를 만들었고, fluentd에서 다음과 같이 자동으로 다음과 같이 만들어지는 것을 볼 수 있다.
GET _cat/indices
...
yellow open park--2023.12.08-000001 GV-NrXxuQsW9L-z3j5n3HQ 1 1 233 0 98.2kb 98.2kb
...
참고로, elasticsearch plugin에 application_name을 설정하면 park-${application_name}-2023.12.08이 된다. 필자는 application_name: ""으로 설정해서 park--2023.12.08이 된 것이다.
park만 주었는데도, ilm과 template덕분에 park--{날짜}-000001이라는 index가 나오는 것이다. 다음으로 _alias를 확인해보도록 하자.
GET park--2023.12.08-000001
{
"park--2023.12.08-000001" : {
"aliases" : {
"park-2023.12.08" : {
"is_write_index" : true
}
},
...
"settings" : {
"index" : {
"lifecycle" : {
"name" : "retention-3m",
"rollover_alias" : "park-2023.12.08"
},
"routing" : {
"allocation" : {
"include" : {
"_tier_preference" : "data_content"
}
}
},
"number_of_shards" : "1",
"provided_name" : "<park--2023.12.08-000001>",
"creation_date" : "1702001064437",
"number_of_replicas" : "1",
"uuid" : "GV-NrXxuQsW9L-z3j5n3HQ",
"version" : {
"created" : "7171499"
}
}
}
}
}
index park--2023.12.08-000001에 park-2023.12.08이라는 alias가 자동으로 할당된 것을 볼 수 있다. rollover_alias도 park-2023.12.08로 잘 붙어있는 것을 볼 수 있다. 그렇다면 park--2023.12.08-000001에는 index-template와 ilm이 잘 붙어있는 지 확인해보도록 하자.
GET park--2023.12.08-000001/_ilm/explain
응답으로 다음이 나온다.
{
"indices" : {
"park--2023.12.08-000001" : {
"index" : "park--2023.12.08-000001",
"managed" : true,
"policy" : "retention-3m",
"lifecycle_date_millis" : 1702001064437,
"age" : "6m",
"phase" : "hot",
"phase_time_millis" : 1702001064512,
"action" : "rollover",
"action_time_millis" : 1702001064512,
"step" : "check-rollover-ready",
"step_time_millis" : 1702001064512,
"phase_execution" : {
"policy" : "retention-3m",
"phase_definition" : {
"min_age" : "0ms",
"actions" : {
"rollover" : {
"max_size" : "5gb",
"max_age" : "3m"
}
}
},
"version" : 1,
"modified_date_in_millis" : 1702001064488
}
}
}
}
retention-3m ILM이 잘 걸린 것이 확인된다.
마지막으로 index-template를 확인하여, rollover_alias가 park--2023.12.08-000001 index의 alias인 park-2023.12.08과 동일한 지 확인해야한다. index-template의 이름은 kibana에서 index management -> index template칸으로 가면 park-2023.12.08라는 index template가 만들어진 것을 볼 수 있다.
GET _template/park-2023.12.08
...
{
"park-2023.12.08" : {
"order" : 53,
"index_patterns" : [
"park--2023.12.08-*"
],
"settings" : {
"index" : {
"lifecycle" : {
"name" : "retention-3m",
"rollover_alias" : "park-2023.12.08"
},
"number_of_replicas" : "1"
}
},
"mappings" : { },
"aliases" : { }
}
}
park-2023.12.08 index-template가 index pattern으로 fluentd에서 data를 쏘고 있는 index로 보내고 있는 것을 확인할 수 있고, licycle부분에 ILM로 retention-3m이 연결된 것을 볼 수 있다. 마지막으로 rollover_alias가 park-2023.12.08으로, park--2023.12.08-000001 index의 rollover alias와 동일한 것을 확인할 수 있다.
이제 rollover가 성립되는 조건은 모두 만족하였다. 실제로 rollover가 되는 지 안되는 지 확인해보도록 하자. 필자의 경우 3분 후에 rollover되고 3분 후에 rollover된 index를 삭제하도록 하였다. 실제로 시간이 지나면 다음과 같이 index가 만들어진다.
park--2023.12.08-000001
park--2023.12.08-000002
park--2023.12.08-000001이 rollover되고 park--2023.12.08-000002에 새로운 데이터가 쓰이고 있을 것이다. 확인해보도록하자.
GET park--2023.12.08-000001
{
"park--2023.12.08-000001" : {
"aliases" : {
"park-2023.12.08" : {
"is_write_index" : false
}
},
...}
}
alias인 park-2023.12.08의 is_write_index부분이 false인 것을 확인할 수 있다. 해당 index에 write되고 있는 data가 없다는 것이다. 반면에 park--2023.12.08-000002를 확인하면 log가 들어오고 있기 때문에 is_write_index는 true가 된다.
GET park--2023.12.08-000002
{
"park--2023.12.08-000002" : {
"aliases" : {
"park-2023.12.08" : {
"is_write_index" : true
}
},
...}
}
시간이 왕창 지나면 다음과 같이 이전에 rollover된 index들은 사라지게 된다.
park--2023.12.08-000010
park--2023.12.08-000011
park--2023.12.08-000012
참고로, 다음의 기능은 날짜가 바뀌면 다음과 같이 날짜가 바뀌는대로 새로운 index-template를 만들고 index를 rollover한다.
park--2023.12.09-000023
park--2023.12.09-000024
park--2023.12.09-000025
index-template로 park-2023.12.09가 만들어지고 alias는 park-2023.12.09가 된다.
이것으로 fluentd에 대한 내용은 끝이다. 그 밖에 plugin들에 대한 내용은 일부 다룰 수는 있겠지만, docs를 참고하는 편이 좋으며 직접하는 것을 추천한다.