OpenTelemetry Operator
The OpenTelemetry project's Kubernetes operator. It runs OpenTelemetry Collectors from an OpenTelemetryCollector custom resource (as a Deployment, DaemonSet, StatefulSet or injected sidecar) and injects zero-code auto-instrumentation into pods for Java, Node.js, Python, .NET, Go, Apache HTTPD and Nginx through a mutating admission webhook driven by an Instrumentation resource and pod annotations. Apache 2.0 and free; the operator version tracks the Collector version, and the webhook needs a TLS certificate, by default from cert-manager. It fits clusters that want traces from many services without code changes in each one.
The OpenTelemetry Operator is a Kubernetes operator that manages two things: OpenTelemetry Collector instances and the auto-instrumentation of application pods. A cluster that runs it can get traces, metrics and logs out of services without a code change in each one, because the operator adds the language agent to a pod when the pod is created. The official docs and the repository docs own the full CRD reference.
What it is
A controller plus an admission webhook, written in Go, maintained by the OpenTelemetry project under the CNCF. The licence is Apache 2.0. It is the Kubernetes member of the OpenTelemetry family, next to the Collector, the language SDKs and the zero-code instrumentation agents that it packages.
The current release is v0.161.0 (2026-10-13). Releases come out about every two to four weeks. The operator follows the Collector's version up to the minor number: operator v0.161.0 deploys Collector v0.161.0 by default, and the patch number belongs to the operator alone. The v0.161.0 release bundles Java agent 2.32.0, .NET 1.17.0, Node.js 0.78.0, Python 0.66b1 and Go 0.24.0 instrumentation images. The compatibility matrix lists Kubernetes v1.25 to v1.37 and cert-manager v1 for this release.
The operator owns four custom resources:
| Resource | API version | What it declares |
|---|---|---|
OpenTelemetryCollector |
opentelemetry.io/v1beta1 |
A Collector: mode, image, replicas and the Collector config |
Instrumentation |
opentelemetry.io/v1alpha1 |
SDK settings for injected agents: exporter endpoint, propagators, sampler, per-language images |
TargetAllocator |
opentelemetry.io/v1alpha1 |
Spreads Prometheus scrape targets across Collector replicas |
OpAMPBridge |
opentelemetry.io/v1alpha1 |
Reports Collector state to an OpAMP server and takes remote config |
Recent breaking changes worth knowing before an upgrade:
- v0.161.0 removed the
--create-rbac-permissionsflag (deprecated since v0.88.0); RBAC creation is now only autodetected. Standalone OpAMP Bridge agents get new instance IDs, and the bridge needsgeton thekube-systemnamespace. - v0.160.0 turned the generated operator, Collector and Target Allocator NetworkPolicies off by default, because they could block API server access on some clusters. Turn them back on with
--feature-gates=+operator.networkpolicy,+operand.networkpolicyorspec.networkPolicy.enabled: true. - v0.159.0 made
nonean explicit Target Allocator filter strategy and rejects unknownfilter_strategyvalues at start-up.
How it works
The operator has two jobs that run on different triggers. The controller watches OpenTelemetryCollector resources and reconciles them into ordinary Kubernetes objects: a workload, a ConfigMap with the Collector config, Services for each receiver port it finds, and a ServiceAccount. The mutating webhook sees every pod creation. When a pod or its namespace carries an instrumentation.opentelemetry.io/inject-<language> annotation, the webhook adds an init container that copies the language agent into a shared emptyDir volume, mounts that volume in the app container and sets the OTEL_* environment variables from the matching Instrumentation resource. The agent then sends telemetry over OTLP to a Collector, which exports it to a backend such as Tempo.
Step 1 of 3. The API server sends the new pod to the operator's mutating webhook before it stores the pod. The annotation must be on the pod template, not on the Deployment's own metadata.
Two paths: injection at pod creation, and trace export from the running pod through a managed Collector to Tempo.
The injection path runs once per pod, at creation; pods that already run keep their old spec until they restart. The export path runs for as long as the pod lives. The webhook is on the critical path for both the Collector resources and every annotated pod, so its certificate and availability matter (see Gotchas).
Collector deployment modes
| Mode | Workload | Typical use |
|---|---|---|
deployment (default) |
Deployment | A central gateway that receives OTLP from many apps |
daemonset |
DaemonSet | One Collector per node, for node-local logs, host metrics or kubelet data |
statefulset |
StatefulSet | Stable pod names, for Prometheus scraping with the Target Allocator |
sidecar |
Container in the app pod | Injected by the sidecar.opentelemetry.io/inject annotation; one Collector per pod |
When to use it
Reach for it when:
- Many services need traces and changing the code of each one is not practical.
- You want Collectors declared as Kubernetes resources and kept in the same Git workflow as the rest of the cluster.
- You need sidecar Collectors per pod, or Prometheus scraping sharded across Collector replicas with the Target Allocator.
- The services use Java, Node.js, Python or .NET, where zero-code agents cover the common frameworks well.
Skip it when:
- One Collector is enough. The plain
opentelemetry-collectorHelm chart deploys it with no webhook and no CRDs. - The services already use an OpenTelemetry SDK in code and only need an endpoint.
- The collector of record is Grafana Alloy and you do not need injection. Alloy already runs the upstream
otelcol.*components. - The cluster cannot accept a mutating webhook with
failurePolicy: Failon its pod path.
At a glance
Core concepts
- Reconcile, do not template. An
OpenTelemetryCollectoris the source of truth; the operator owns the workload, ConfigMap and Services and overwrites manual edits. - Annotation-driven injection.
instrumentation.opentelemetry.io/inject-java: "true"(or-nodejs,-python,-dotnet,-go,-apache-httpd,-nginx) on a pod template or namespace. The most specific annotation wins. - Version lock-step. The operator upgrades managed Collectors to its own minor version.
- Language gates. Java, Node.js, Python, .NET and Apache HTTPD injection are on by default. Go and Nginx are off until
--enable-go-instrumentationor--enable-nginx-instrumentationis set. - Config is passed through. The operator reads the Collector config to find ports and the metrics endpoint, but it does not validate all of it. An invalid config can still create pods that crash.
Key facts
| Aspect | Value |
|---|---|
| Current release | v0.161.0 (2026-10-13) |
| Helm chart | open-telemetry/opentelemetry-operator 0.126.0, appVersion 0.161.0 |
| Kubernetes | v1.25 to v1.37 for v0.161.0 |
| Webhook TLS | cert-manager v1 by default; Helm-generated self-signed cert as the alternative |
| Default Collector image | opentelemetry-collector-k8s distribution |
| Node OS | Linux only |
| Licence | Apache 2.0 |
Comparison
| Aspect | OpenTelemetry Operator | Collector Helm chart | Grafana Alloy |
|---|---|---|---|
| Unit of config | CRDs reconciled by a controller | Helm values | Alloy config syntax, Helm values |
| Auto-instrumentation | Yes, via webhook | No | No |
| Sidecar Collectors | Yes | No | No |
| Extra moving parts | Webhook, cert, CRDs | None | None |
| Best fit | Many services, zero-code tracing | One gateway or agent | Logs and metrics to the Grafana stack |
How to integrate
Install cert-manager first, then the operator from the official chart:
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm repo update
helm install opentelemetry-operator open-telemetry/opentelemetry-operator \
--namespace opentelemetry-operator-system --create-namespace \
--version 0.126.0 \
--set "manager.collectorImage.repository=ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-k8s"
Without cert-manager, let Helm generate the webhook certificate:
# values.yaml
admissionWebhooks:
certManager:
enabled: false
autoGenerateCert:
enabled: true
manager:
extraArgs:
- --enable-go-instrumentation=true # only if Go injection is needed
A gateway Collector that receives OTLP and forwards traces to Tempo:
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: gateway
namespace: observability
spec:
mode: deployment
config:
receivers:
otlp:
protocols:
grpc: { endpoint: 0.0.0.0:4317 }
http: { endpoint: 0.0.0.0:4318 }
processors:
memory_limiter: { check_interval: 1s, limit_percentage: 75, spike_limit_percentage: 15 }
batch: {}
exporters:
otlp:
endpoint: tempo-distributor.tempo.svc:4317 # adjust to the Tempo service
tls: { insecure: true }
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp]
An Instrumentation resource, created before the workloads it instruments:
apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
name: default
namespace: my-app
spec:
exporter:
endpoint: http://gateway-collector.observability.svc:4317
propagators: [tracecontext, baggage]
sampler:
type: parentbased_traceidratio
argument: "0.25"
python:
env:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://gateway-collector.observability.svc:4318
Then annotate the pod template and restart the workload:
spec:
template:
metadata:
annotations:
instrumentation.opentelemetry.io/inject-python: "true"
kubectl rollout restart deploy/my-api -n my-app
kubectl get otelinst,otelcol -A
In the GL stack
The operator runs at v0.161.0 on the beta cluster only. It is a trial: it runs there first, and it goes to the production cluster only after it proves itself on beta. On beta it manages the OpenTelemetry Collectors and the auto-instrumentation of workloads. Traces go to Tempo, the trace store on the management cluster next to Loki, and Grafana on the management cluster reads them through its Tempo data source next to Prometheus and Loki.
This keeps tracing apart from the rest of the observability layer. Logs come from Grafana Alloy on every cluster and metrics come from Prometheus agents that remote-write to the hub on the management cluster; neither depends on the operator. cert-manager already runs on every cluster for in-cluster certificates, so the webhook's certificate dependency adds no new component. Keeping the trial on beta limits the effect of a broken webhook or a bad agent image to the beta environment, which the beta branch feeds before anything reaches main.
Gotchas
- Create the
Instrumentationbefore the workload. The webhook acts only at pod creation. Pods created before the resource existed, or before the annotation was added, run without the agent until they restart. - Annotate the pod template. The annotation goes on
spec.template.metadata.annotations. On the Deployment's own metadata it does nothing. - Port 4317 versus 4318. Python, .NET and Go agents export over HTTP/protobuf by default. If
spec.exporter.endpointpoints at 4317, setOTEL_EXPORTER_OTLP_ENDPOINTto the 4318 address for those languages, as the official example does. - The webhook can block pods. The chart sets
failurePolicy: Fail. If the operator pod or its certificate is broken, creating annotated resources fails. Watch the operator Deployment like any other part of the control path. - Go needs privilege. Go injection is off by default, needs
instrumentation.opentelemetry.io/otel-go-auto-target-exeset to the binary path, runs withprivileged: trueandrunAsUser: 0, and supports only the first container in a pod. - Multi-container pods. Injection targets the first container unless
instrumentation.opentelemetry.io/container-nameslists the others. - musl images. Python on Alpine-based images needs
instrumentation.opentelemetry.io/otel-python-platform: "musl". - CRDs on upgrade. The chart uses templated CRDs and does not support
--skip-crds; usecrds.create=falseto manage them elsewhere. - Move off
v1alpha1Collectors. Oldv1alpha1OpenTelemetryCollectorresources still convert tov1beta1, but support is planned for removal. Re-apply manifests asv1beta1and patch the CRD'sstoredVersions. - Private GKE. The control plane must reach port 9443 on the nodes for the webhook.
Risks
- Licence: Apache 2.0 under the CNCF. There is no separate commercial edition.
- Project health: active, with releases every few weeks and named maintainers across several vendors. The release pace also means frequent breaking notes, as the last three releases show; read each changelog.
- Pre-1.0 APIs: the operator is still v0.x, and
Instrumentation,TargetAllocatorandOpAMPBridgearev1alpha1. Field changes between minors are possible. - Lock-in: low. The output is OTLP, and the Collector config is the upstream format, so the backend and even the operator can be swapped.
- Security: the webhook mutates pods cluster-wide, and Go injection runs a privileged container. Limit which namespaces use injection, keep the operator's RBAC under review, and scan the agent images like any other image.
Alternatives
- OpenTelemetry Collector Helm chart — pick it for one gateway or agent Collector with no CRDs or webhook.
- Grafana Alloy — pick it when the backends are Loki, Prometheus and Tempo and the work is collection, not injection.
- OpenTelemetry eBPF Instrumentation — pick it for zero-code HTTP and gRPC spans from the kernel, with no change to the app process.
- SDKs in code — pick them when teams own their services and want full control over spans and attributes.
Related
- /resources/tempo/ — the trace store the Collectors export to
- /resources/grafana/ — where traces are read
- /resources/grafana-alloy/ — the log collector on every cluster
- /resources/prometheus/ — the metrics hub
- /resources/loki/ — the log store
- /resources/cert-manager/ — issues the webhook certificate
- /resources/stack-observability/ — the observability layer overview
- OpenTelemetry Operator docs
- Operator releases
- Operator Helm chart
- Auto-instrumentation troubleshooting
