Observability 파이프라인을 코드로 배포하여 번거로운 수작업 없이도 시스템을 즉시 명확하게 파악할 수 있는 방법을 공유합니다.
Observability as Code는 배포 방식을 혁신했던 Infrastructure as Code 원칙을 그대로 적용해 Telemetry 파이프라인을 정의하고, 배포하고, 관리하는 접근 방식입니다.
# Observability의 세 가지 핵심
본격적인 구현으로 들어가기 전에, 현대 Observability를 구성하는 요소를 간단히 짚어보겠습니다.
- Metrics: 시간에 따라 시스템 동작을 수치로 나타내는 데이터 포인트
- Logs: 애플리케이션 내에서 발생한 이벤트를 기록한 상세한 기록
- Traces: 분산 서비스 전반에 걸친 요청 흐름에 대한 정보
# Observability 도구를 위한 Infrastructure as Code
우선 Terraform을 이용해 Observability 인프라를 배포하는 것부터 시작하겠습니다. 다음은 AWS EKS에 OpenTelemetry Collector를 설정하는 예시입니다.
resource "kubernetes_namespace" "observability" {
metadata {
name = "observability"
}
}
resource "helm_release" "opentelemetry_collector" {
name = "opentelemetry-collector"
repository = "https://open-telemetry.github.io/opentelemetry-helm-charts"
chart = "opentelemetry-collector"
namespace = kubernetes_namespace.observability.metadata[0].name
values = [<<EOF
mode: deployment
config:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 1s
send_batch_size: 1024
exporters:
awsxray:
region: "${var.aws_region}"
awsemf:
region: "${var.aws_region}"
namespace: "EKSObservability"
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [awsxray]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [awsemf]
EOF
]
depends_on = [
kubernetes_namespace.observability
]
}
Datadog 연동을 위해서는 다음과 같은 설정을 추가할 수 있습니다.
resource "helm_release" "datadog" {
name = "datadog"
repository = "https://helm.datadoghq.com"
chart = "datadog"
namespace = kubernetes_namespace.observability.metadata[0].name
set {
name = "datadog.apiKey"
value = var.datadog_api_key
}
set {
name = "datadog.apm.enabled"
value = "true"
}
set {
name = "datadog.logs.enabled"
value = "true"
}
set {
name = "datadog.logs.containerCollectAll"
value = "true"
}
set {
name = "datadog.otlp.receiver.protocols.grpc.endpoint"
value = "0.0.0.0:4317"
}
}
# Telemetry 파이프라인 아키텍처
잘 설계된 Telemetry 파이프라인은 다음과 같은 주요 구성 요소를 포함합니다.
- Collection: 다양한 소스로부터 Telemetry 데이터를 수집
- Processing: 데이터를 필터링, 변환, 보강
- Routing: 데이터를 적절한 저장 및 분석 시스템으로 전달
- Storage: 이후 분석을 위해 데이터 저장
- Visualization: 데이터를 접근 가능하고 활용 가능하게 시각화
OpenTelemetry는 이러한 파이프라인을 구축하기 위한 표준화된 접근을 제공합니다. 다음은 세 가지 Telemetry 유형을 모두 처리하는 OpenTelemetry Collector 설정 예시입니다.
apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
metadata:
name: telemetry-pipeline
namespace: observability
spec:
mode: deployment
config: |
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 1s
send_batch_size: 1024
memory_limiter:
check_interval: 1s
limit_mib: 1000
resourcedetection:
detectors: [env, eks]
timeout: 2s
exporters:
jaeger:
endpoint: jaeger-collector.observability.svc.cluster.local:14250
tls:
insecure: true
prometheus:
endpoint: 0.0.0.0:8889
logging:
loglevel: info
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch, resourcedetection]
exporters: [jaeger]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch, resourcedetection]
exporters: [prometheus]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [logging]
# 분산 트레이싱: Jaeger와 OpenTelemetry
분산 트레이싱은 마이크로서비스 전반의 요청 흐름을 이해하는 데 필수입니다. Jaeger를 OpenTelemetry와 함께 설정하는 방법은 다음과 같습니다.
resource "helm_release" "jaeger" {
name = "jaeger"
repository = "https://jaegertracing.github.io/helm-charts"
chart = "jaeger"
namespace = kubernetes_namespace.observability.metadata[0].name
set {
name = "provisionDataStore.cassandra"
value = "false"
}
set {
name = "storage.type"
value = "elasticsearch"
}
set {
name = "storage.elasticsearch.host"
value = "elasticsearch-master.observability.svc.cluster.local"
}
set {
name = "storage.elasticsearch.port"
value = "9200"
}
}
애플리케이션 계측은 Python 예제로도 가능합니다.
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
resource = Resource(attributes={
SERVICE_NAME: "order-service"
})
tracer_provider = TracerProvider(resource=resource)
otlp_exporter = OTLPSpanExporter(endpoint="opentelemetry-collector.observability.svc.cluster.local:4317")
span_processor = BatchSpanProcessor(otlp_exporter)
tracer_provider.add_span_processor(span_processor)
trace.set_tracer_provider(tracer_provider)
tracer = trace.get_tracer(__name__)
def process_order(order_id):
with tracer.start_as_current_span("process_order") as span:
span.set_attribute("order_id", order_id)
validate_order(order_id)
def validate_order(order_id):
with tracer.start_as_current_span("validate_order") as span:
span.set_attribute("validation_method", "full")
# Validation logic here
# Metrics 및 Logging 통합
Metrics와 Logs를 통합적으로 다루기 위해서는 OpenTelemetry Collector를 활용하여 둘 다 수집하고 적절한 Backend로 전송할 수 있습니다.
apiVersion: v1
kind: ConfigMap
metadata:
name: otel-agent-conf
namespace: observability
data:
otel-agent-config.yaml: |
receivers:
filelog:
include: [ /var/log/pods/*/*/*.log ]
exclude: [ /var/log/pods/*/kube-proxy/*.log ]
start_at: end
include_file_path: true
include_file_name: true
operators:
- type: regex_parser
regex: '^(?P<time>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z) (?P<level>\w+) (?P<message>.*)'
timestamp:
parse_from: time
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
prometheus:
config:
scrape_configs:
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
processors:
batch:
timeout: 1s
exporters:
otlp:
endpoint: opentelemetry-collector.observability.svc.cluster.local:4317
tls:
insecure: true
service:
pipelines:
logs:
receivers: [filelog]
processors: [batch]
exporters: [otlp]
metrics:
receivers: [prometheus]
processors: [batch]
exporters: [otlp]
# Kubernetes와 AWS 환경에서의 구현
Kubernetes 전용 모니터링을 위해서는 OpenTelemetry Operator를 사용할 수 있습니다.
apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
metadata:
name: k8s-collector
namespace: observability
spec:
mode: daemonset
config: |
receivers:
kubeletstats:
collection_interval: 10s
auth_type: "serviceAccount"
endpoint: "${env:K8S_NODE_NAME}:10250"
insecure_skip_verify: true
processors:
resourcedetection:
detectors: [env, eks]
timeout: 2s
exporters:
awsemf:
region: "${env:AWS_REGION}"
namespace: "EKSObservability"
service:
pipelines:
metrics:
receivers: [kubeletstats]
processors: [resourcedetection]
exporters: [awsemf]
env:
- name: K8S_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: AWS_REGION
value: "us-west-2"
AWS 통합을 위해서는 적절한 IAM 권한을 보장해야 합니다.
resource "aws_iam_role" "otel_collector_role" {
name = "otel-collector-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRoleWithWebIdentity"
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.eks.arn
}
Condition = {
StringEquals = {
"${aws_iam_openid_connect_provider.eks.url}:sub": "system:serviceaccount:observability:opentelemetry-collector"
}
}
}
]
})
}
resource "aws_iam_policy" "otel_collector_policy" {
name = "otel-collector-policy"
description = "Policy for OpenTelemetry Collector"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"logs:PutLogEvents",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:DescribeLogStreams",
"logs:DescribeLogGroups",
"xray:PutTraceSegments",
"xray:PutTelemetryRecords",
"cloudwatch:PutMetricData"
]
Effect = "Allow"
Resource = "*"
}
]
})
}
resource "aws_iam_role_policy_attachment" "otel_collector_attachment" {
role = aws_iam_role.otel_collector_role.name
policy_arn = aws_iam_policy.otel_collector_policy.arn
}
# Datadog 통합
더 깊이 있는 Datadog 통합을 위해, 커스텀 Metrics와 APM을 구성할 수 있습니다.
apiVersion: v1
kind: ConfigMap
metadata:
name: datadog-agent-config
namespace: observability
data:
datadog.yaml: |
api_key: ${DATADOG_API_KEY}
site: datadoghq.com
logs_enabled: true
apm_config:
enabled: true
otlp_config:
receiver:
protocols:
grpc:
endpoint: 0.0.0.0:4317
logs_config:
container_collect_all: true
process_config:
enabled: true
dogstatsd_mapper_profiles:
- name: "custom_metrics"
prefix: "app."
mappings:
- match: "app.request.duration"
name: "request.duration"
tags:
service: "$1"
endpoint: "$2"
그리고 애플리케이션 계측을 Datadog으로 수행하는 예시는 다음과 같습니다.
from ddtrace import tracer, config
from ddtrace.propagation.http import HTTPPropagator
# Configure Datadog tracer
config.service = "order-service"
config.env = "production"
config.version = "1.0.0"
# Use the tracer in your application
@tracer.wrap(service="order-service", resource="process_order")
def process_order(order_id):
# Add custom tags
tracer.current_span().set_tag("order_id", order_id)
# Your order processing logic here
validate_order(order_id)
@tracer.wrap(service="order-service", resource="validate_order")
def validate_order(order_id):
tracer.current_span().set_tag("validation_method", "full")
# Validation logic here
# 실제 구현 예시
마이크로서비스 애플리케이션을 위한 완전한 예시로 모든 구성을 연결해 보겠습니다.
- 인프라 배포:
module "observability_stack" {
source = "./modules/observability"
eks_cluster_name = var.eks_cluster_name
aws_region = var.aws_region
datadog_api_key = var.datadog_api_key
enable_jaeger = true
enable_prometheus = true
enable_opentelemetry = true
}
- 서비스 계측(Node.js 예시):
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-proto');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
// Configure the tracer
const provider = new NodeTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'payment-service',
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: 'production',
}),
});
const exporter = new OTLPTraceExporter({
url: 'http://opentelemetry-collector.observability.svc.cluster.local:4318/v1/traces',
});
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
provider.register();
// Register auto-instrumentations
registerInstrumentations({
instrumentations: [
new HttpInstrumentation(),
new ExpressInstrumentation(),
],
});
// Your application code follows
const express = require('express');
const app = express();
app.get('/process-payment', (req, res) => {
// Your payment processing logic
res.send('Payment processed');
});
app.listen(3000, () => {
console.log('Payment service listening on port 3000');
});
- Kubernetes 배포:
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: payment-service
template:
metadata:
labels:
app: payment-service
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8888"
spec:
containers:
- name: payment-service
image: payment-service:latest
ports:
- containerPort: 3000
- containerPort: 8888
name: metrics
env:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://opentelemetry-collector.observability.svc.cluster.local:4318"
- name: OTEL_SERVICE_NAME
value: "payment-service"
- name: OTEL_RESOURCE_ATTRIBUTES
value: "service.namespace=payment,service.version=1.0.0"
# 결론
Observability를 코드로 구현하면, Infrastructure as Code가 배포 프로세스에 가져온 것과 동일한 이점—일관성, 반복 가능성, 확장성—을 Telemetry 파이프라인에도 적용할 수 있습니다.
