MSA 모니터링을 구축해보자 ! - 구성
개요
모니터링 분야가 왜 비용이 많이 들어가는지 조금이나마 알게 되는 느낌이었다.
상용 도구가 왜 인기있는지 몸소 실감하게 되었다. ㅠㅠ
💡 아래 내용과 관련해서 틀린 부분이 매우매우 많을 수도 있습니다.. 듣고 이해한 내용을 바탕으로 작성된 글이므로 참고 정도로 생각해주시면 감사하겠습니다 ㅠㅠ
Alloy
Log, Metric, Trace 정보를 Receiver & Exporter 하기위해 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로 되어있는지 확인합니다.
만약 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가 정상적으로 수집하고 전달하는지 확인합니다.
후기
간단한 설정인데도 불구하고 1주일정도 공부하고 구성했던것 같다. 정상적으로 설정된 것 같으니 다음에는 Log, Metric, Trace를 연관지어서 추적할 수 있도록 Grafana에서 구성해야겠다.