로그 시스템 배포하기 (Fluent-bit, Loki, Grafana)

온프레미스 K8s 클러스터 구축하기 - 4
이민석's avatar
Mar 13, 2025
로그 시스템 배포하기 (Fluent-bit, Loki, Grafana)

💡

모든 글은 가급적 미괄식으로 작성되어, 독자도 같은 상황을 겪게 설계했습니다.

직전에 노드 모니터링 구축하기[1]를 진행했습니다.
이 과정에서 여러 매트릭(metric)을 PromQL로 질의하고 테스트했습니다.

다음에 진행할 클러스터 모니터링 구축하기[2]를 진행하기 전에
Prometheus로 수집이 불가능한 여러 정보를 로그(log) 형태로 수집하려고 합니다.
이를 위해 Loki를 배포하고 Fluent-bit을 포함한 실습을 진행하고자 합니다.

마지막으로 모니터링 시스템 배포하기[3]에서 누락한 Persistent 설정을 추가합니다.

A. 실습 환경

A.1. rancher local-path-provisioner 설치하기

권장 사례인 Dynamic Provisioning*을 사용하기 위해 StorageClass가 필요합니다.

💡

[Dynamic Provisioing*] PersistentVolume을 별도로 생성 및 관리하지 않기 위해서 PersistentVolume의 생성 및 관리를 StorageClass에 위임을 하여 필요한 시점에 PersistentVolume을 생성 및 제거할 수 있게 해줍니다.

Local 을 지원하는 기본 플러그인인 kubernetes.io/no-provisioner [A.1.1]
Dynamic Provisioing을 지원하지 않고 강제로 사용할 시 순환 참조*가 발생합니다.

💡

[순환 참조*] Local Plugin인 no-provisioner을 사용하는 StorageClass인 local-storage를 만들고 나서 PersistentVolumeClaim의 storageClassName에 local-storage를 참조하고 Pod에서 이 persistentVolumeClaim을 참조하면 아래의 순환 참조가 발생한다. 1. Pod가 생성되기 전에 PersistentVolume이 필요하다. 2. PersistentVolume은 파드가 생성되어야 생성된다.

kubernetes.io/no-provisioner [A.1.1]에서는 두 가지 방법만 쓸 수 있다.

  1. nodeSelector를 명시하고 Immediate 방식으로 PersistentVolume 생성

  2. PersistentVolume을 별도로 생성하고 PersistentVolumeClaim을 생성

💡

1. Immediate : PersistentVolumeClaim이 생기면 PersistentVolume도 생성 2. WaitForFirstConsumer : 실제로 PersistentVolumeClaim을 소비하면 PersistentVolume 생성 (Pod가 생성된 이후…)

아래 코드를 실행하면 local-storage라는 StorageClass가 생성된다.
본 문서에서는 사용하지 않으나, 참고를 위해서 첨부하였습니다.

mkdir -p ~/local-storage-provisioner

cd ~/local-storage-provisioner

cat <<EOF > local-storage-provisioner.yaml
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
# volumeBindingMode: Immediate
reclaimPolicy: Delete
allowVolumeExpansion: false
EOF
kubectl apply -f ~/local-storage-provisioner/local-storage-provisioner.yaml

Dyanmic을 지원하는 rancher/local-path-provisioner [A.1.2]를 사용합니다.
GitHub에서 가이드하는 Git Clone 방식에 따라서 설치를 진행하였습니다.

  1. Helm 차트 클론받기

    mkdir -p ~/rancher
    
    cd ~/rancher
    
    git clone https://github.com/rancher/local-path-provisioner.git
    
    cd local-path-provisioner
  2. Helm 릴리즈

    cd ~/rancher/local-path-provisioner
    
    helm upgrade --install local-path \
      --namespace kube-system \
      --create-namespace  \
      ~/rancher/local-path-provisioner/deploy/chart/local-path-provisioner/

권장하지는 않으나,
Immediate를 기본값으로 하고 싶다면 아래를 참고하면 됩니다.

cd ~/rancher/local-path-provisioner

helm upgrade --install local-path \
  --namespace kube-system \
  --create-namespace  \
  --set storageClass.volumeBindingMode=Immediate \
  ~/rancher/local-path-provisioner/deploy/chart/local-path-provisioner/

A.2. Loki 배포하기

클러스터 모니터링에서 중요한 2가지 지표를 보기 위해서는 로그가 필요합니다.

  1. kubernetes events

  2. kubernetes control plane logs (journalctl logs / crictl logs)

로그 저장, 조회를 위해서 grafana/loki [A.2.1]을 사용합니다.
로그 저장 및 인덱스를 위해서 S3, DynamoDB를 사용할 수도 있지만
온프레미스의 환경에 맞게 FileSystem 타입을 사용하여 구현했습니다.

  1. tHelm 저장소 추가하기

    helm repo add grafana https://grafana.github.io/helm-charts
  2. Helm 저장소 업데이트

    helm repo update
  3. Helm 저장소 확인하기

    helm search repo loki-stack
  4. Loki values.yaml 파일 생성하기

    mkdir -p ~/loki
    cd ~/loki
    cat <<EOF > loki-stack.values.yaml
    loki:
      auth_enabled: false
      commonConfig:
        replication_factor: 1
      storage:
        type: filesystem
      # 스키마 구성 추가
      schemaConfig:
        configs:
          - from: 2020-10-24
            store: tsdb
            object_store: filesystem
            schema: v13
            index:
              prefix: index_
              period: 24h
      # 스토리지 구성 상세화
      storageConfig:
        boltdb_shipper:
          active_index_directory: /data/loki/boltdb-shipper-active
          cache_location: /data/loki/boltdb-shipper-cache
          cache_ttl: 24h
          shared_store: filesystem
        filesystem:
          directory: /data/loki/chunks
      
    serviceMonitor:
      enabled: true
      
    gateway:
      enabled: true
      replicas: 1
    
    # 단일 배포 모드 설정
    singleBinary:
      replicas: 1
      persistence:
        enabled: true
        size: 10Gi
        storageClass: local-path
      
    # 다른 배포 컴포넌트 비활성화
    read:
      replicas: 0
    write:
      replicas: 0
    backend:
      replicas: 0
    
    # 명시적으로 deploymentMode 설정
    deploymentMode: "SingleBinary"
    
    service:
      type: ClusterIP
    
    EOF
    helm upgrade --install loki grafana/loki \
      --namespace kube-monitor \
      --create-namespace \
      -f ~/loki/loki-stack.values.yaml
  5. Loki 배포 상태 확인하기

    kubectl get all -n kube-monitor | grep loki 
    pod/loki-0                                               0/2     Pending   0                4m6s
    pod/loki-canary-btf6w                                    1/1     Running   0                4m6s
    pod/loki-chunks-cache-0                                  0/2     Pending   0                4m6s
    pod/loki-gateway-5d9698cd4b-n9lcc                        1/1     Running   0                4m6s
    pod/loki-results-cache-0                                 2/2     Running   0                4m6s
    service/loki                                  ClusterIP   10.102.123.246   <none>        3100/TCP,9095/TCP    4m6s
    service/loki-canary                           ClusterIP   10.107.220.44    <none>        3500/TCP             4m6s
    service/loki-chunks-cache                     ClusterIP   None             <none>        11211/TCP,9150/TCP   4m6s
    service/loki-gateway                          ClusterIP   10.99.106.75     <none>        80/TCP               4m6s
    service/loki-headless                         ClusterIP   None             <none>        3100/TCP             4m6s
    service/loki-memberlist                       ClusterIP   None             <none>        7946/TCP             4m6s
    service/loki-results-cache                    ClusterIP   None             <none>        11211/TCP,9150/TCP   4m6s
    daemonset.apps/loki-canary                           1         1         1       1            1           <none>                   4m6s
    deployment.apps/loki-gateway                        1/1     1            1           4m6s
    replicaset.apps/loki-gateway-5d9698cd4b                        1         1         1       4m6s
    statefulset.apps/loki                 0/1     4m6s
    statefulset.apps/loki-chunks-cache    0/1     4m6s
    statefulset.apps/loki-results-cache   1/1     4m6s

A.3. FluentBit 배포 준비하기

💡

A.3. Loki 는 기본적으로 로그 수집 기능을 제공하지 않습니다. 따라서 PromTail, Fluentd, Fluent-bit 등의 로그 수집기를 사용해야 합니다. 저는 확장성, 출력값 조정 등의 이유로 Fluent-bit을 선택하였습니다.

컨트롤 플레인의 로그들은 다양한 형태로 저장되고 있습니다.
이를 가공하기 위해서 grafana/helm-charts/fluent-bit[A.3.1]을 사용합니다.

  1. Helm 저장소 추가하기

    helm repo add grafana https://grafana.github.io/helm-charts
  2. Helm 저장소 업데이트

    helm repo update
  3. Helm 저장소 확인하기

    helm search repo fluent-bit
    NAME               CHART VERSION APP VERSION DESCRIPTION                                       
    grafana/fluent-bit 2.6.0         v2.1.0      Uses fluent-bit Loki go plugin for gathering lo...

A.4. Prometheus Persistent 수정

모니터링 시스템 배포하기 (Prometheus, Grafana) [A.4.1]에서
빠르게 스킵하고 지나간 Persistent 설정을 보완하여 진행합니다.

helm upgrade \
  --install prometheus prometheus-community/prometheus \
  --set pushgateway.enabled=false \
  --set alertmanager.enabled=false \
  --set nodeExporter.enabled=true \
  --set server.service.type="NodePort" \
  --set server.statefulSet.enabled=false \
  --set server.persistentVolume.enabled=true \
  --set server.persistentVolume.storageClass=local-path \
  --namespace kube-monitor \
  --create-namespace

이후 노드 모니터링 구축하기 (Prometheus, Grafana) [A.4.1]에서
수집 간격을 줄이기 위해서 수정한 Grafana ConfigMap을 다시 수정해줍니다.

kubectl edit configmap prometheus-server -n kube-monitor -o yaml
  • 기존

      prometheus.yml: |
        global:
          evaluation_interval: 1m
          scrape_interval: 1m
          scrape_timeout: 10s
  • 변경

      prometheus.yml: |
        global:
          evaluation_interval: 1m
          scrape_interval: 30s
          scrape_timeout: 10s

ConfigMap 변경을 반영하기 위해서 Rollout을 진행합니다.

kubectl rollout restart daemonset/prometheus-prometheus-node-exporter -n kube-monitor
kubectl rollout restart deployment/prometheus-server -n kube-monitor
kubectl rollout restart deployment/prometheus-kube-state-metrics -n kube-monitor
daemonset.apps/prometheus-prometheus-node-exporter restarted
deployment.apps/prometheus-server restarted
deployment.apps/prometheus-kube-state-metrics restarted

A.5. Grafana Persistent 수정

모니터링 시스템 배포하기 (Prometheus, Grafana) [A.5.1]에서
빠르게 스킵하고 지나간 Persistent 설정을 보완하여 진행합니다.

helm upgrade \
  --install grafana grafana/grafana \
  --set replicas=1 \
  --set service.enabled=true \
  --set service.type=NodePort \
  --set persistence.enabled=true \
  --set persistence.storageClassName=local-path \
  --namespace kube-monitor \
  --create-namespace

B. Fluent-bit, Loki, Grafana 실습하기

B.1. 기본 지식

모니터링 시스템 배포하기 (Prometheus, Grafana) [B.1]에서는
Prometheus*에서 node-exporter 등을 활성화 하는 것으로 편하게 진행했습니다.

💡

[Prometheus*] 이는 시계열 데이터를 TSDB(Time-series database)에 저장합니다. 시계열 데이터에는 라벨(Label)을 통해서 많은 정보들을 포함합니다. 대부분 쿼리(PromQL)는 라벨을 통한 필터링 및 집계가 가능합니다.

Loki*에서는 시간, 메타데이터, 콘텐츠로 데이터를 분할합니다.

💡

[Loki*] 이는 시계열 데이터를 메타데이터 인덱싱 기법으로 저장합니다. 저장 장소는 TSDB, Object Storage, Block Storage 등으로 다양합니다. 대부분 쿼리(LogQL)은 메타데이터 기반의 필터링 및 집계가 가능합니다.

https://grafana.com/oss/loki/
Loki - Metadata Indexing

PromTail, Fluent-bit* 등을 사용하면 Loki로 로그를 전송할 수 있습니다.
하지만 앞으로 진행할 실습의 특성상 확장성 높은 Fluent-Bit을 선택했습니다.
이번 문서의 특성상 가볍고 확장성 좋은 Fluent-bit을 선택했습니다.

💡

[PromTail vs Fluent-bit*] 대부분의 실습에서는 PromTail을 사용하여 작업을 진행합니다. 이 친구는 DaemonSet 형태로 배포되어 추가적인 설정이 없어도 작동합니다. 하지만 시스템(journalctl), 커널 감시(falco), 감사 로그(trivy, kyverno) 등 다양한 형태로 저장되는 로그를 수집하기 위해서는 그 기능이 부족합니다. 이번 실습에서는 Fluent-bit을 선택했으며, 그 이유는 다음과 같습니다. 1. Sidecar container, Operator pod 등의 다양한 패턴으로 구축 가능 2. Log filtering, output formatting 등의 다양한 최적화 가능 3. 비슷한 기능을 하는 Fluentd에 비해 더 적은 리소스 사용

Fluent-Bit은 아래와 같이 INPUT, FILTER, BUFFER, OUTPUT으로 구성됩니다.
각 계층은 1~N개 존재할 수 있으며 세부적인 설정을 통해 확장할 수 있습니다.
결정적으로 Plugin을 활용하면 Loki, Kafka, Splunk 등과도 연계 가능합니다.

https://fluentbit.io/how-it-works/
Fluent-Bit - Logs

편의성 측면에서 PromTail이 훨씬 편리함에도 불구하고
확장성 측면을 고려하여 Fluent-Bit을 사용하는 방안으로 결정했습니다.

B.2. Fluent-bit으로 NGINX 로그 수집하기

Fluent-Bit의 INPUT, FILTER, PARSER 등을 정의할 ConfigMap을 만들어줍니다.

mkdir -p ~/fluent-bit/example/
cd ~/fluent-bit/example/

cat <<EOF > configmap.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: fluent-bit-config
data:
  fluent-bit.conf: |
    [SERVICE]
        Flush     5
        Daemon    off
        Log_Level info
        Parsers_File parsers.conf

    [INPUT]
        Name        tail
        Path        /var/log/nginx/access.log
        Parser      nginx
        Tag         kube.var.log.containers.nginx.access

    [INPUT]
        Name        tail
        Path        /var/log/nginx/error.log
        Parser      nginx-error
        Tag         kube.var.log.containers.nginx.error

    [FILTER]
        Name        kubernetes
        Match       nginx.*
        Merge_Log   On
        Keep_Log    On
        K8S-Logging.Parser    On
        K8S-Logging.Exclude   On

    [OUTPUT]
        Name        loki
        Match       *
        Host        loki.kube-monitor.svc.cluster.local
        Port        3100
        Labels      job=fluentbit,service_name=nginx-with-fluentbit-sidecar
        Label_Keys  $level,$detected_level

  parsers.conf: |
    [PARSER]
        Name        nginx
        Format      regex
        Regex       ^(?<remote>[^ ]*) (?<host>[^ ]*) (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")
        Time_Key    time
        Time_Format %d/%b/%Y:%H:%M:%S %z

    [PARSER]
        Name        nginx-error
        Format      regex
        Regex       ^(?<time>[0-9]{4}/[0-9]{2}/[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}) \[(?<level>.*?)\] (?<pid>[0-9]+)#(?<tid>[0-9]+): (?<message>.*)
        Time_Key    time
        Time_Format %Y/%m/%d %H:%M:%S
EOF
kubectl apply -f ~/fluent-bit/example/configmap.yaml
kubectl replace -f ~/fluent-bit/example/configmap.yaml

Sidecar pattern으로 Fluent-Bit을 실행하는 NGINX Pod를 실행합니다.

Sidecar Container
mkdir -p ~/fluent-bit/example/
cd ~/fluent-bit/example/

cat <<EOF > pod.yaml
---
apiVersion: v1
kind: Pod
metadata:
  name: nginx-with-fluentbit-sidecar
  labels:
    app: nginx
spec:
  containers:
  - name: nginx
    image: nginx:latest
    ports:
    - containerPort: 80
    volumeMounts:
    - name: varlog
      mountPath: /var/log/nginx

  - name: fluent-bit
    image: fluent/fluent-bit:latest
    volumeMounts:
    - name: varlog
      mountPath: /var/log/nginx
      readOnly: true

    - name: config-volume
      mountPath: /fluent-bit/etc/

  volumes:
  - name: varlog
    emptyDir: {}
  - name: config-volume
    configMap:
      name: fluent-bit-config
EOF
kubectl delete -f ~/fluent-bit/example/pod.yaml --force
kubectl apply -f ~/fluent-bit/example/pod.yaml

배포한 NGINX Pod의 앤드포인트를 확인하고 요청을 보내서 로그를 생성합니다.

  1. NGINX Pod 앤드포인트 확인하기

    kubectl get pods nginx-with-fluentbit-sidecar -o=custom-columns=NAME:.metadata.name,IP:.status.podIP,CONTAINER:.spec.containers[0].name,PORT:.spec.containers[0].ports[0].containerPort
    NAME                           IP             CONTAINER   PORT
    nginx-with-fluentbit-sidecar   10.244.0.150   nginx       80
  2. NGINX Pod 앤드포인트 요청 전송하기

    POD_ENDPOINT=$(kubectl get pods nginx-with-fluentbit-sidecar -o=custom-columns=NAME:.metadata.name,IP:.status.podIP,CONTAINER:.spec.containers[0].name,PORT:.spec.containers[0].ports[0].containerPort | awk 'NR>1 {print $2":"$4}')
    
    for i in {1..10}; do curl -k $POD_ENDPOINT; done
    <h1>Welcome to nginx!</h1>
    <p>If you see this page, the nginx web server is successfully installed and
    working. Further configuration is required.</p>
    
    <p>For online documentation and support please refer to
    <a href="http://nginx.org/">nginx.org</a>.<br/>
    Commercial support is available at
    <a href="http://nginx.com/">nginx.com</a>.</p>
    
    <p><em>Thank you for using nginx.</em></p>
    </body>
    </html>

아래와 같이 Data source에 Loki를 선택하고 아래와 같이 LogQL을 작성합니다.

Grafana - LogQL로 Loki 쿼리 질의하기

샘플인 LogQL은 아래와 같이 총 3개로 지정하였습니다.

  1. LogQL 작성하기

    {job=~".+"}
  2. LogQL 수정하기

    {job="fluentbit", service_name="nginx-with-fluentbit-sidecar"}
  3. LogQL 포맷팅 수정하기

    {job="fluentbit", service_name="nginx-with-fluentbit-sidecar"} 
    | json 
    | line_format "{{.service_name}}({{.remote}}) - {{.host}} [{{.time}}] \"{{.method}} {{.path}}\" {{.code}} {{.size}} \"{{.referer}}\" \"{{.agent}}\""

Share article

Unchaptered