DevOps/Observability

MSA 모니터링을 구축해보자 ! - 구성

journalctl 2025. 1. 29. 23:10

개요

ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

모니터링 분야가 왜 비용이 많이 들어가는지 조금이나마 알게 되는 느낌이었다.

상용 도구가 왜 인기있는지 몸소 실감하게 되었다. ㅠㅠ


💡 아래 내용과 관련해서 틀린 부분이 매우매우 많을 수도 있습니다.. 듣고 이해한 내용을 바탕으로 작성된 글이므로 참고 정도로 생각해주시면 감사하겠습니다 ㅠㅠ

Alloy

Log, Metric, Trace 정보를 Receiver & Exporter 하기위해 Collector를 구성해야 한다.

Otel Collector -  https://opentelemetry.io/docs/collector/

Receiver를 통해서 각 데이터를 수집, Processor를 통해서 데이터를 가공, Exporter를 통해서 각 데이터에 맞는 Backend Storage(Prometheus, Loki, Tempo)에 전달하는 역할을 한다.

Alloy Collector도 Opentelemetry Collector를 기반으로 만들어졌기때문에 구성 방식이 비슷하게 설정할 수 있습니다.

Alloy 관련 설정은 Kubernetes Configmap으로 배포되어 있어 있습니다.

$ kubectl get cm -n monitoring | grep alloy
alloy                                                          1      7d

$ kubectl get cm alloy -n monitoring -o yaml
Name:         alloy
Namespace:    monitoring
Labels:       app.kubernetes.io/component=config
              app.kubernetes.io/instance=alloy
              app.kubernetes.io/managed-by=Helm
              app.kubernetes.io/name=alloy
              app.kubernetes.io/part-of=alloy
              app.kubernetes.io/version=v1.5.1
              helm.sh/chart=alloy-0.10.1
Annotations:  meta.helm.sh/release-name: alloy
              meta.helm.sh/release-namespace: monitoring

Data
====
config.alloy:
----
logging {
  level  = "debug"
  format = "logfmt"
}
...

Events:  <none>

Receiver 설정

Otlp를 통해 수집을 진행하기 때문에 다음과 같이 설정합니다. 해당 내용과 관련해서 Alloy 공식 문서를 참고해주세요. (https://grafana.com/docs/alloy/latest/reference/components/)

otelcol.receiver.otlp "otel" {
    http {}
    grpc {}

    output {
        metrics = [otelcol.exporter.prometheus.prometheus.input]
        logs = [otelcol.exporter.loki.loki.input]
        traces = [otelcol.exporter.otlp.tempo.input]
    }
}

Processor 설정

Receiver를 통해 받은 데이터를 가공하는 부분입니다. 아래는 Log에서 레이블을 추가하여 Grafana에서 해당 레이블을 통해 검색할 수 있도록 설정하였습니다.

loki.process "create_label" {
    forward_to = [loki.write.loki.receiver]

    stage.json {
        expressions = {
            resources = "",
        }
    }

    stage.json {
        source = "resources"
        expressions = {
            "app" = "\"k8s.pod.name\"",
        }
    }

    stage.labels {
        values = {
            "app" = "app",
        }
    }
}

Exporter 설정

processor 과정을 거쳐 최종적으로 각 데이터 스토리지에 저장하도록 설정합니다. Metric, Log, Trace의 Endpoint에 맞게 각각 설정합니다.

loki.write "loki" {
    endpoint {
        url = "http://loki-loki-distributed-distributor:3100/loki/api/v1/push"
        tls_config {
            insecure_skip_verify = true
        }
    }
}

otelcol.exporter.prometheus "prometheus" {
  forward_to = [prometheus.remote_write.prometheus.receiver]
}

prometheus.remote_write "prometheus" {
    endpoint {
        url = "http://prometheus-kube-prometheus-prometheus:9090/api/v1/push"
        tls_config {
            insecure_skip_verify = true
        }
    }
}

otelcol.exporter.otlp "tempo" {
    client {
        endpoint = "http://tempo-distributor:4317"
        tls {
            insecure = true
            insecure_skip_verify = true
        }
    }
}

테스트용 모니터링으로 memory_limiter, sampling은 따로 설정하지 않았습니다.

각 Components 별로 Export Argument, Consume Argument가 설정되어 있습니다.

예를 들어, otelcol components를 통해 전달받은 데이터를 loki components로 전달하려면 “otelcol.exporter.loki”를 사용하여 데이터를 변환한 후 전달합니다.

Alloy.config

logging {
    level  = "debug"
    format = "logfmt"
}

otelcol.receiver.otlp "otel" {
    http {}
    grpc {}

    output {
        metrics = [otelcol.exporter.prometheus.prometheus.input]
        logs = [otelcol.exporter.loki.loki.input]
        traces = [otelcol.exporter.otlp.tempo.input]
    }
}

otelcol.exporter.loki "loki" {
    forward_to = [loki.process.create_label.receiver]
}

loki.process "create_label" {
    forward_to = [loki.write.loki.receiver]

    stage.json {
        expressions = {
            resources = "",
        }
    }

    stage.json {
        source = "resources"
        expressions = {
            "app" = "\"k8s.pod.name\"",
        }
    }

    stage.labels {
        values = {
            "app" = "app",
        }
    }
}

loki.write "loki" {
    endpoint {
        url = "http://loki-loki-distributed-distributor:3100/loki/api/v1/push"
        tls_config {
            insecure_skip_verify = true
        }
    }
}

otelcol.exporter.prometheus "prometheus" {
  forward_to = [prometheus.remote_write.prometheus.receiver]
}

prometheus.remote_write "prometheus" {
    endpoint {
        url = "http://prometheus-kube-prometheus-prometheus:9090/api/v1/push"
        tls_config {
            insecure_skip_verify = true
        }
    }
}

otelcol.exporter.otlp "tempo" {
    client {
        endpoint = "http://tempo-distributor:4317"
        tls {
            insecure = true
            insecure_skip_verify = true
        }
    }
}

Alloy Dashboard

위 내용을 바탕으로 설정을 진행하면 Alloy Dashboard - Graph에 다음과 같이 나타나게 됩니다.

정상적으로 모든 컴포넌트가 healthy로 되어있는지 확인합니다.

Alloy Dashboard - Graph

만약 unhealthy로 나타나게 되면 해당 Component를 선택해서 어떤 부분에서 오류가 발생했는지 확인하면 됩니다.


Opentelemetry Instrumentation

Opentelemetry Operator 설치

Openetelemetry에서 제공하는 Instrumentation을 사용해서 각 Pod에서 발생하는 Metric, Log, Trace 정보를 수집하고 Collector에 전달하도록 설정합니다.

자세한 내용은 공식문서를 참고해주세요! (https://opentelemetry.io/docs/concepts/instrumentation/)

먼저 Kubernetes에서 사용할 수 있도록 제공한 Operator를 먼저 설치합니다.

Opentelemetry Operator를 설치하기 위해서는 Cert-manager가 설치되어 있어야 합니다. Cert-manager 설치 이후 Operator를 설치하도록 구성합니다.

$ helm repo add jetstack https://charts.jetstack.io

$ helm install \
  cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set crds.enabled=true

$ kubectl apply -f https://github.com/open-telemetry/opentelemetry-operator/releases/latest/download/opentelemetry-operator.yaml

Opentelemetry Operator가 설치되었다면 관련 CRD가 생성되었는지 확인합니다.

$ kubectl api-resources | grep -i opentelemetry
instrumentations                    otelinst,otelinsts   opentelemetry.io/v1alpha1         true         Instrumentation
opampbridges                                             opentelemetry.io/v1alpha1         true         OpAMPBridge
opentelemetrycollectors             otelcol,otelcols     opentelemetry.io/v1beta1          true         OpenTelemetryCollector

Opentelemetry Instrumentation 구성 & 배포

💡

예제 MSA 구성을 위해 간단한 Python 코드로 작성되어 아래 내용은 Python 관련 설정으로 되어있습니다.

python-instrumentation.yaml

apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: python-instrumentation
  namespace: default
spec:
  exporter:
    endpoint: http://alloy.monitoring.svc:4318 # Alloy Service 도메인 설정
  env:
  propagators:
    - tracecontext # 부모 및 자식 TraceID 전파를 위해 설정,  Trace ID, Span ID를 전달하여 서비스 간 Trace를 연결하는 역할.
    - baggage # 서비스 간 추가적인 Key-Value 데이터를 전파하는 역할
  python:
  # 아래 환경 변수와 관련해서는 Opentelemetry 공식문서를 참고해주세요!
  # https://opentelemetry.io/docs/kubernetes/operator/automatic/#python-excluding-auto-instrumentation
  # https://opentelemetry.io/docs/zero-code/
    env:
      - name: OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED
        value: 'true'
      - name: OTEL_PYTHON_LOG_CORRELATION
        value: 'true'
      - name: OTEL_PYTHON_LOG_FORMAT
        value: "%(msg)s [span_id=%(span_id)s]"
      - name: OTEL_PYTHON_LOG_LEVEL
        value: debug
      - name: OTEL_PYTHON_AUTO_INSTRUMENTATION_ENABLED
        value: 'true'

위 내용을 바탕으로 Instrumentation을 배포합니다.

$ kubectl apply -f python-instrumentation.yaml
$ kubectl get otelinst -A

NAMESPACE   NAME                     AGE    ENDPOINT                           SAMPLER   SAMPLER ARG
default     python-instrumentation   7d1h   http://alloy.monitoring.svc:4318

다음으로 예제 파드를 배포합니다.

이때 각 Pod Annotation에 해당 Instrumentation이 Injection 될 수 있도록 설정합니다.

[instrumentation.opentelemetry.io/inject-python:](http://instrumentation.opentelemetry.io/inject-python:) "${namespace}/${instrumentation-name}"

test.yaml

kind: Pod
metadata:
  name: flask-web
  labels:
    app: flask-web
  annotations:
    instrumentation.opentelemetry.io/inject-python: "default/python-instrumentation"
spec:
  containers:
  - image: journalctlxe/journalctlxe:web
    name: flask-web
    ports:
    - containerPort: 8000
---
apiVersion: v1
kind: Pod
metadata:
  name: flask-user
  labels:
    app: flask-user
  annotations:
    instrumentation.opentelemetry.io/inject-python: "default/python-instrumentation"
spec:
  containers:
  - image: journalctlxe/journalctlxe:user
    name: flask-user
    ports:
    - containerPort: 8002
---
apiVersion: v1
kind: Pod
metadata:
  name: flask-order
  labels:
    app: flask-order
  annotations:
    instrumentation.opentelemetry.io/inject-python: "default/python-instrumentation"
spec:
  containers:
  - image: journalctlxe/journalctlxe:order
    name: flask-order
    ports:
    - containerPort: 8001
---
apiVersion: v1
kind: Service
metadata:
  name: web-service
spec:
  selector:
    app: flask-web
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8000
---
apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: flask-order
  ports:
  - protocol: TCP
    port: 8001
    targetPort: 8001
---
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: flask-user
  ports:
  - protocol: TCP
    port: 8002
    targetPort: 8002
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: flask-web-ingress
spec:
  ingressClassName: nginx
  rules:
  - host: "app.local.example"
    http:
      paths:
      - path: /dashboard
        pathType: Prefix
        backend:
          service:
            name: flask-web-service
            port:
              number: 80

Instrumentation이 정상적으로 Injection 되었다면 init Container와 앞서 설정한 Instrumentation 환경변수들이 Container에 추가된 것을 확인할 수 있습니다.

Name:             flask-web
Namespace:        default
Priority:         0
Service Account:  default
Node:             kind-worker/172.18.0.3
Start Time:       Wed, 29 Jan 2025 19:10:42 +0900
Labels:           app=flask-web
Annotations:      instrumentation.opentelemetry.io/inject-python: default/python-instrumentation
Status:           Running
IP:               10.244.1.48
IPs:
  IP:  10.244.1.48
Init Containers:
  opentelemetry-auto-instrumentation-python:
    Container ID:  containerd://a7cbd51be993f960b01c83f830f6e985afe37e724581951bfd0775bd08289921
    Image:         ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-python:0.50b0
    Image ID:      ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-python@sha256:53d3f773d95b3a9124051233abdb967e5f2a9a53d01509c42bf1682dfd61624b
    Port:          <none>
    Host Port:     <none>
    Command:
      cp
      -r
      /autoinstrumentation/.
      /otel-auto-instrumentation-python
    State:          Terminated
      Reason:       Completed
      Exit Code:    0
      Started:      Wed, 29 Jan 2025 19:10:42 +0900
      Finished:     Wed, 29 Jan 2025 19:10:43 +0900
    Ready:          True
    Restart Count:  0
    Limits:
      cpu:     500m
      memory:  64Mi
    Requests:
      cpu:        50m
      memory:     64Mi
    Environment:  <none>
    Mounts:
      /otel-auto-instrumentation-python from opentelemetry-auto-instrumentation-python (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-pzjpc (ro)
Containers:
  flask-web:
    Container ID:   containerd://4d8f6180fa6eb91c00ce55ec712102a37b27d6010cf0eb16437fad8cfe2f60a4
    Image:          journalctlxe/journalctlxe:web
    Image ID:       docker.io/journalctlxe/journalctlxe@sha256:bb29ae2bea4e763a55499a1af7dfc83a09ee5ce7de5a79f8ee36bfa70e1c9ccd
    Port:           8000/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Wed, 29 Jan 2025 19:10:43 +0900
    Ready:          True
    Restart Count:  0
    Environment:
      OTEL_NODE_IP:                                       (v1:status.hostIP)
      OTEL_POD_IP:                                        (v1:status.podIP)
      OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED:  true
      OTEL_PYTHON_LOG_CORRELATION:                       true
      OTEL_PYTHON_LOG_FORMAT:                            %(msg)s [span_id=%(span_id)s]
      OTEL_PYTHON_LOG_LEVEL:                             debug
      OTEL_PYTHON_AUTO_INSTRUMENTATION_ENABLED:          true
      PYTHONPATH:                                        /otel-auto-instrumentation-python/opentelemetry/instrumentation/auto_instrumentation:/otel-auto-instrumentation-python
      OTEL_EXPORTER_OTLP_PROTOCOL:                       http/protobuf
      OTEL_TRACES_EXPORTER:                              otlp
      OTEL_METRICS_EXPORTER:                             otlp
      OTEL_LOGS_EXPORTER:                                otlp
      OTEL_SERVICE_NAME:                                 flask-web
      OTEL_EXPORTER_OTLP_ENDPOINT:                       http://alloy.monitoring.svc:4318
      OTEL_RESOURCE_ATTRIBUTES_POD_NAME:                 flask-web (v1:metadata.name)
      OTEL_RESOURCE_ATTRIBUTES_NODE_NAME:                 (v1:spec.nodeName)
      OTEL_PROPAGATORS:                                  tracecontext,baggage
      OTEL_RESOURCE_ATTRIBUTES:                          k8s.container.name=flask-web,k8s.namespace.name=default,k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME),service.instance.id=default.$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME).flask-web,service.version=web
    Mounts:
      /otel-auto-instrumentation-python from opentelemetry-auto-instrumentation-python (rw)

Grafana를 통해서 Collector가 정상적으로 수집하고 전달하는지 확인합니다.

Grafana - Tempo
Grafana - Loki
Grafana - Prometheus


후기

모니터링은 돈 주고 맡기자.. 내 영역은 아닌것 같아..

간단한 설정인데도 불구하고 1주일정도 공부하고 구성했던것 같다. 정상적으로 설정된 것 같으니 다음에는 Log, Metric, Trace를 연관지어서 추적할 수 있도록 Grafana에서 구성해야겠다.