OpenTelemetry - Basic Kubernetes Setup
This article describes how to deploy a basic setup of OpenTelemetry-supported monitoring and tracing tools on Kubernetes.
OpenTelemetry offers a Kubernetes Operator and Helm charts which can be used to deploy various tools. Since Prometheus is not one of them, we have opted for using the Prometheus Operator to deploy Prometheus, and to deploy Grafana and Jaeger using regular Kubernetes Deployments.
All workloads created in this article, are created in a separate ‘monitoring’ namespace.
Prometheus
To deploy Prometheus, you can use the Prometheus Operator. This can be done in various ways, but in this article, we use the regular, Kubernetes manifests, method.
Before Prometheus Operator can be deployed, we need to install the Custom Resource Definitions.
The latest version can be found in their GitHub releases at https://github.com/prometheus-operator/prometheus-operator/releases/.
Once the Custom Resource Definitions have been created, you can deploy the Prometheus Operator using the following manifest
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/name: prometheus-operator
name: prometheus-operator
namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/name: prometheus-operator
name: prometheus-operator
rules:
- apiGroups:
- ""
resources:
- nodes/metrics
verbs:
- get
- apiGroups:
- monitoring.coreos.com
resources:
- alertmanagers
- alertmanagers/finalizers
- alertmanagerconfigs
- prometheuses
- prometheuses/finalizers
- thanosrulers
- thanosrulers/finalizers
- servicemonitors
- podmonitors
- prometheusrules
- probes
verbs:
- '*'
- apiGroups:
- apps
resources:
- statefulsets
verbs:
- '*'
- apiGroups:
- ""
resources:
- configmaps
- secrets
verbs:
- '*'
- apiGroups:
- ""
resources:
- pods
verbs:
- list
- delete
- apiGroups:
- ""
resources:
- services
- services/finalizers
- endpoints
verbs:
- get
- create
- update
- delete
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- namespaces
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/name: prometheus-operator
name: prometheus-operator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: prometheus-operator
subjects:
- kind: ServiceAccount
name: prometheus-operator
namespace: monitoring
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/name: prometheus-operator
name: prometheus-operator
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: controller
app.kubernetes.io/name: prometheus-operator
template:
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/name: prometheus-operator
spec:
containers:
- args:
- --kubelet-service=kube-system/kubelet
- --logtostderr=true
- --config-reloader-image=docker.io/jimmidyson/configmap-reload:v0.4.0
- --prometheus-config-reloader=quay.io/coreos/prometheus-config-reloader:v0.42.1
image: "quay.io/coreos/prometheus-operator:v0.42.1"
name: prometheus-operator
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
ports:
- containerPort: 8080
name: http
resources:
requests:
memory: 100Mi
cpu: 100m
limits:
memory: 200Mi
cpu: 200m
nodeSelector:
kubernetes.io/os: linux
serviceAccountName: prometheus-operator
---
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/name: prometheus-operator
name: prometheus-operator
namespace: monitoring
spec:
clusterIP: None
ports:
- name: http
port: 8080
targetPort: http
selector:
app.kubernetes.io/component: controller
app.kubernetes.io/name: prometheus-operator
The Operator will watch for Prometheus workloads to be defined before creating Prometheus Services.
Below manifest can be used to deploy a simple Prometheus Service including an Azure Disk for its data storage.
apiVersion: v1
kind: ServiceAccount
metadata:
name: prometheus
namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: prometheus
namespace: monitoring
rules:
- apiGroups: [""]
resources:
- nodes
- services
- endpoints
- pods
verbs: ["get", "list", "watch"]
- apiGroups:
- ""
resources:
- nodes/metrics
verbs:
- get
- apiGroups: [""]
resources:
- configmaps
verbs: ["get"]
- nonResourceURLs: ["/metrics"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: prometheus
namespace: monitoring
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: prometheus
subjects:
- kind: ServiceAccount
name: prometheus
namespace: monitoring
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: prometheus
allowVolumeExpansion: true
provisioner: kubernetes.io/azure-disk
parameters:
storageaccounttype: Standard_LRS
---
apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
name: prometheus-servicemonitors
namespace: monitoring
spec:
nodeSelector:
kubernetes.io/os: linux
securityContext:
runAsUser: 0
serviceAccountName: prometheus
alerting:
alertmanagers:
- namespace: monitoring
name: alertmanager-service
port: alertmanager
serviceMonitorSelector:
matchLabels:
type: servicemonitor
ruleSelector:
matchLabels:
role: alert-rules
prometheus: prometheus-service
resources:
requests:
memory: 400Mi
cpu: 128m
limits:
memory: 800Mi
cpu: 256m
enableAdminAPI: false
logLevel: warn
retention: 15d
storage:
volumeClaimTemplate:
spec:
accessModes:
- ReadWriteOnce
storageClassName: prometheus
resources:
requests:
storage: 8Gi
---
apiVersion: v1
kind: Service
metadata:
name: prometheus-service
namespace: monitoring
spec:
ports:
- name: http
port: 9090
protocol: TCP
selector:
prometheus: prometheus-servicemonitors
The Prometheus Service will watch any ServiceMonitor definitions created and ingest data from the defined endpoints. By default, it will make requests to the /metrics
path and ingest the data outputted there.
You will want to create a ServiceMonitor for any application you want to monitor.
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: microservice-service-monitor
namespace: monitoring
labels:
type: servicemonitor
spec:
namespaceSelector:
any: true
selector:
matchLabels:
name: microservice
endpoints:
- targetPort: 80
A ServiceMonitor will expose any application it finds based on the defined selectors. In above example, a selector is used to find workloads in any namespace with the name
label set to microservice
.
Jaeger
The easiest way to deploy Jaeger is through a Kubernetes manifest which creates a Deployment and Service definition.
Before creating the Deployment, we need to create a ConfigMap with the necessary Jaeger configuration.
apiVersion: v1
kind: ConfigMap
metadata:
name: jaeger-collector
namespace: monitoring
data:
collector.yaml: |
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
exporters:
jaeger_storage_exporter:
trace_storage: memstore
extensions:
jaeger_query:
storage:
traces: memstore
jaeger_storage:
backends:
memstore:
memory:
max_traces: 100000
service:
extensions:
- jaeger_storage
- jaeger_query
telemetry:
metrics:
address: 0.0.0.0:8888
pipelines:
traces:
exporters:
- jaeger_storage_exporter
receivers:
- otlp
This ConfigMap defines what ports Jaeger should listen on to retrieve traces.
With the ConfigMap in place, we can create the Deployment and Service definitions.
apiVersion: v1
kind: Service
metadata:
name: jaeger-service
namespace: monitoring
spec:
ports:
- name: jaeger
port: 16686
targetPort: jaeger
- name: metrics
port: 8888
targetPort: metrics
- name: otlp-grpc
port: 4317
targetPort: otlp-grpc
- name: otlp-http
port: 4318
targetPort: otlp-http
selector:
app.kubernetes.io/name: jaeger-deployment
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/name: jaeger-deployment
name: jaeger-deployment
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: jaeger-deployment
template:
metadata:
labels:
app.kubernetes.io/name: jaeger-deployment
annotations:
prometheus.io/path: /metrics
prometheus.io/port: "8888"
prometheus.io/scrape: "true"
spec:
nodeSelector:
kubernetes.io/os: linux
containers:
- args:
- --config=/conf/collector.yaml
env:
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
image: jaegertracing/jaeger:latest
imagePullPolicy: Always
name: jaeger-deployment
ports:
- containerPort: 16686
name: jaeger
protocol: TCP
- containerPort: 16686
name: jaeger-query
protocol: TCP
- containerPort: 8888
name: metrics
protocol: TCP
- containerPort: 4317
name: otlp-grpc
protocol: TCP
- containerPort: 4318
name: otlp-http
protocol: TCP
resources:
requests:
memory: 100Mi
cpu: 100m
limits:
memory: 500Mi
cpu: 500m
volumeMounts:
- mountPath: /conf
name: otc-internal
restartPolicy: Always
terminationGracePeriodSeconds: 30
volumes:
- configMap:
items:
- key: collector.yaml
path: collector.yaml
name: jaeger-collector
name: otc-internal
This will deploy Jaeger in the most simplistic way, using memory for storage. Note that this does mean that the data in Jaeger will disappear and reset whenever the application is restarted.
With above configuration, OpenTelemetry traces can be sent to jaeger-service over port 4317 for GRPC or 4318 for HTTP.
Grafana
Grafana can be deployed in various ways. In this example we deploy it as a simple manifest to deploy ConfigMaps, a Deployment and a Service definition.
First, you will want to create a ConfigMap to define the data sources for Grafana:
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-datasource
namespace: monitoring
labels:
grafana_datasource: '1'
data:
datasource.yaml: |-
apiVersion: 1
datasources:
- name: Prometheus
uid: webstore-metrics
type: prometheus
access: proxy
url: http://prometheus-service.monitoring:9090/
isDefault: true
- name: Jaeger
uid: webstore-traces
type: jaeger
url: http://jaeger:16686/jaeger/ui
editable: true
isDefault: false
In above example, two data sources will be created for Prometheus and Jaeger.
Additionally, we want to create a ConfigMap to define Grafana configuration regarding where to locate dashboards.
apiVersion: v1
data:
dashboards.yaml: |-
apiVersion: 1
providers:
- name: '0'
orgId: 1
folder: ''
folderUid: ''
type: file
disableDeletion: false
editable: true
updateIntervalSeconds: 30
options:
path: /etc/grafana/provisioning/dashboards
kind: ConfigMap
metadata:
name: grafana-dashboards
namespace: monitoring
labels:
app: grafana
Having created the necessary ConfigMaps, we can now deploy the Deployment and Service configurations.
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/name: grafana-deployment
name: grafana-deployment
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: grafana-deployment
template:
metadata:
labels:
app.kubernetes.io/name: grafana-deployment
spec:
containers:
- image: docker.io/grafana/grafana:latest
name: grafana-deployment
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
ports:
- containerPort: 3000
name: grafana
resources:
requests:
memory: 100Mi
cpu: 100m
limits:
memory: 2500Mi
cpu: 500m
volumeMounts:
- mountPath: /var/lib/grafana
name: grafana-storage
- mountPath: /etc/grafana/provisioning/datasources
name: grafana-datasources
readOnly: true
- mountPath: /etc/grafana/provisioning/dashboards
name: grafana-dashboards
readOnly: true
- mountPath: /etc/grafana/provisioning/dashboards/rabbitmq-overview-dashboard
name: rabbitmq-overview-dashboard
readOnly: true
- mountPath: /etc/grafana/provisioning/dashboards/dotnet-overview-dashboard
name: dotnet-overview-dashboard
readOnly: true
- mountPath: /etc/grafana/provisioning/dashboards/redis-overview-dashboard
name: redis-overview-dashboard
readOnly: true
livenessProbe:
httpGet:
path: /api/health
port: 3000
httpHeaders:
- name: X-Kubernetes-Probe
value: Liveness
timeoutSeconds: 300
periodSeconds: 30
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/health
port: 3000
httpHeaders:
- name: X-Kubernetes-Probe
value: Liveness
timeoutSeconds: 300
periodSeconds: 30
failureThreshold: 3
volumes:
- name: grafana-storage
emptyDir: {}
- name: grafana-datasources
configMap:
name: grafana-datasource
- configMap:
name: grafana-dashboards
name: grafana-dashboards
- name: rabbitmq-overview-dashboard
configMap:
name: rabbitmq-overview-dashboard
- name: dotnet-overview-dashboard
configMap:
name: dotnet-overview-dashboard
- name: redis-overview-dashboard
configMap:
name: redis-overview-dashboard
nodeSelector:
kubernetes.io/os: linux
---
apiVersion: v1
kind: Service
metadata:
name: grafana-service
namespace: monitoring
spec:
ports:
- port: 3000
protocol: TCP
selector:
app.kubernetes.io/name: grafana-deployment
You may have noticed that, in above example, we specified three volumes and volumeMounts for dashboards.
These dashboards have also been created as ConfigMaps, where the JSON schema of a Grafana dashboard is stored in the below way.
apiVersion: v1
kind: ConfigMap
metadata:
name: redis-overview-dashboard
namespace: monitoring
labels:
app: grafana
data:
kubernetes.json: |-
{
... JSON here
}