﻿---
title: Deploy EDOT Collector using OpenTelemetryCollector custom resources
description: Deploy the EDOT Collector on Kubernetes using `OpenTelemetryCollector` custom resources, covering gateway (Deployment) and agent (DaemonSet) modes.
url: https://docs-v3-preview.elastic.dev/elastic/docs-content/pull/7153/solutions/observability/get-started/opentelemetry/use-cases/kubernetes/collector-custom-resource
products:
  - Elastic Cloud Serverless
  - Elastic Distribution of OpenTelemetry Collector
  - Elastic Observability
applies_to:
  - Serverless Observability projects: Generally available
  - Elastic Stack: Generally available
  - Elastic Distribution of OpenTelemetry Collector: Generally available
---

# Deploy EDOT Collector using OpenTelemetryCollector custom resources
The [recommended path](https://docs-v3-preview.elastic.dev/elastic/docs-content/pull/7153/solutions/observability/get-started/opentelemetry/use-cases/kubernetes/deployment) for deploying EDOT on Kubernetes uses the `opentelemetry-kube-stack` Helm chart, which installs the OpenTelemetry Operator and configures all collectors automatically.
Use the information on this page if you need to create `OpenTelemetryCollector` custom resources (CRs) directly (for example in GitOps workflows, when Helm is unavailable, or when you want fine-grained control over each collector's configuration).
The CRs on this page replicate the architecture that the Helm chart deploys.

## Architecture

All telemetry flows through the gateway collector before reaching Elastic:
- **DaemonSet collectors** run on every node. They collect host metrics, pod logs, and Kubernetes node metrics, receive OTLP traces and metrics from applications, and forward everything to the gateway.
- **Cluster collector** runs as a single Deployment. It collects cluster-level Kubernetes events and metrics and forwards them to the gateway.
- **Gateway collector** is the central ingestion layer. It receives OTLP from the DaemonSet and Cluster collectors. For self-managed deployments, the gateway also processes traces for Elastic APM compatibility.

The gateway's export destination depends on your deployment type:
- **Serverless and Elastic Cloud Hosted**: gateway exports to the Managed OTLP endpoint.
- **Self-managed, ECE, and Elastic Cloud on Kubernetes**: gateway exports directly to Elasticsearch.

The following diagram shows how telemetry flows from the collectors to Elastic:
```mermaid
%%{init: {"themeVariables": {"fontSize": "24px"}}}%%
flowchart TB
    apps[Instrumented applications]
    daemon[DaemonSet collectors]
    cluster[Cluster collector]
    gateway[Gateway collector]
    otlp[Managed OTLP endpoint]
    es[Elasticsearch]

    apps -->|OTLP traces and metrics| daemon
    daemon -->|Host metrics, pod logs, node metrics| gateway
    cluster -->|Cluster events and metrics| gateway
    gateway -->|Serverless and ECH| otlp
    gateway -->|Self-managed, ECE, and ECK| es
```


## Prerequisites

- The [OpenTelemetry Operator installed](https://docs-v3-preview.elastic.dev/elastic/docs-content/pull/7153/solutions/observability/get-started/opentelemetry/use-cases/kubernetes/deployment) in the `opentelemetry-operator-system` namespace
- `kubectl` configured to access your cluster
- An Elasticsearch cluster or a Serverless/Elastic Cloud Hosted project

Follow these steps to deploy the EDOT collectors:
<stepper>
  <step title="Create a credentials secret">
    All CRs on this page use the dedicated EDOT Collector image. Unlike the full Elastic Agent image, this image starts unconditionally in OTel collector mode — you don't need any extra environment variables.
    ```text
    docker.elastic.co/elastic-agent/elastic-otel-collector:9.4.2
    ```
    Create a Kubernetes secret with your Elastic credentials in the `opentelemetry-operator-system` namespace.
    <tab-set>
      <tab-item title="Serverless and Elastic Cloud Hosted">
        ```bash
        kubectl create secret generic elastic-secret-otel \
          --namespace opentelemetry-operator-system \
          --from-literal=elastic_otlp_endpoint='<YOUR_MANAGED_OTLP_ENDPOINT>' \
          --from-literal=elastic_api_key='<YOUR_API_KEY>'
        ```
      </tab-item>

      <tab-item title="Self-managed, ECE, and Elastic Cloud on Kubernetes">
        ```bash
        kubectl create secret generic elastic-secret-otel \
          --namespace opentelemetry-operator-system \
          --from-literal=elastic_endpoint='<YOUR_ELASTICSEARCH_ENDPOINT>' \
          --from-literal=elastic_api_key='<YOUR_API_KEY>'
        ```
      </tab-item>
    </tab-set>
  </step>

  <step title="Deploy the gateway collector">
    The gateway collector is the central ingestion layer. It runs as a Deployment with two replicas for availability.
    <tab-set>
      <tab-item title="Serverless and Elastic Cloud Hosted">
        ```yaml
        apiVersion: opentelemetry.io/v1beta1
        kind: OpenTelemetryCollector
        metadata:
          name: edot-gateway
          namespace: opentelemetry-operator-system
        spec:
          mode: deployment
          image: docker.elastic.co/elastic-agent/elastic-otel-collector:9.4.2
          replicas: 2
          env:
            - name: ELASTIC_OTLP_ENDPOINT
              valueFrom:
                secretKeyRef:
                  name: elastic-secret-otel
                  key: elastic_otlp_endpoint
            - name: ELASTIC_API_KEY
              valueFrom:
                secretKeyRef:
                  name: elastic-secret-otel
                  key: elastic_api_key
          config:
            receivers:
              otlp:
                protocols:
                  grpc:
                    endpoint: 0.0.0.0:4317
                  http:
                    endpoint: 0.0.0.0:4318
            exporters:
              otlp/ingest_metrics_traces:
                endpoint: ${env:ELASTIC_OTLP_ENDPOINT}
                headers:
                  Authorization: ApiKey ${env:ELASTIC_API_KEY}
                sending_queue:
                  enabled: true
                  sizer: bytes
                  queue_size: 50000000
                  block_on_overflow: true
                  batch:
                    flush_timeout: 1s
                    min_size: 1000000
                    max_size: 4000000
                timeout: 15s
              otlp/ingest_logs:
                endpoint: ${env:ELASTIC_OTLP_ENDPOINT}
                headers:
                  Authorization: ApiKey ${env:ELASTIC_API_KEY}
                sending_queue:
                  enabled: true
                  sizer: bytes
                  queue_size: 50000000
                  block_on_overflow: true
                  batch:
                    flush_timeout: 1s
                    min_size: 1000000
                    max_size: 4000000
                timeout: 15s
            service:
              pipelines:
                traces:
                  receivers: [otlp]
                  processors: []
                  exporters: [otlp/ingest_metrics_traces]
                metrics:
                  receivers: [otlp]
                  processors: []
                  exporters: [otlp/ingest_metrics_traces]
                logs:
                  receivers: [otlp]
                  processors: []
                  exporters: [otlp/ingest_logs]
        ```
      </tab-item>

      <tab-item title="Self-managed, ECE, and Elastic Cloud on Kubernetes">
        ```yaml
        apiVersion: opentelemetry.io/v1beta1
        kind: OpenTelemetryCollector
        metadata:
          name: edot-gateway
          namespace: opentelemetry-operator-system
        spec:
          mode: deployment
          image: docker.elastic.co/elastic-agent/elastic-otel-collector:9.4.2
          replicas: 2
          env:
            - name: ELASTIC_ENDPOINT
              valueFrom:
                secretKeyRef:
                  name: elastic-secret-otel
                  key: elastic_endpoint
            - name: ELASTIC_API_KEY
              valueFrom:
                secretKeyRef:
                  name: elastic-secret-otel
                  key: elastic_api_key
          config:
            receivers:
              otlp:
                protocols:
                  grpc:
                    endpoint: 0.0.0.0:4317
                  http:
                    endpoint: 0.0.0.0:4318
            connectors:
              elasticapm: {}
            processors:
              batch:
                send_batch_size: 1000
                timeout: 1s
                send_batch_max_size: 1500
              batch/metrics:
                send_batch_max_size: 0
                timeout: 1s
              elasticapm: {}
            exporters:
              elasticsearch/otel:
                endpoints:
                  - ${env:ELASTIC_ENDPOINT}
                api_key: ${env:ELASTIC_API_KEY}
                mapping:
                  mode: otel
            service:
              pipelines:
                traces:
                  receivers: [otlp]
                  processors: [batch, elasticapm]
                  exporters: [elasticapm, elasticsearch/otel]
                metrics:
                  receivers: [otlp]
                  processors: [batch/metrics]
                  exporters: [elasticsearch/otel]
                metrics/aggregated-otel-metrics:
                  receivers: [elasticapm]
                  processors: []
                  exporters: [elasticsearch/otel]
                logs:
                  receivers: [otlp]
                  processors: [batch]
                  exporters: [elasticapm, elasticsearch/otel]
        ```
      </tab-item>
    </tab-set>
    The operator creates a Service named `edot-gateway-collector` that exposes ports 4317 (gRPC) and 4318 (HTTP) inside the cluster. DaemonSet and Cluster collectors use this Service name to forward data to the gateway.
  </step>

  <step title="Deploy the DaemonSet collector">
    The DaemonSet collector runs on every node to collect host metrics, pod logs, Kubernetes node metrics, and application OTLP telemetry, then forwards everything to the gateway.Because it requires access to the node's filesystem, Kubelet API, and Kubernetes object metadata, create a `ServiceAccount` and `ClusterRole` first:
    ```yaml
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: edot-collector
      namespace: opentelemetry-operator-system
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      name: edot-collector
    rules:
      - apiGroups: [""]
        resources: [nodes, nodes/proxy, nodes/metrics, services, endpoints, pods, events, namespaces]
        verbs: [get, list, watch]
      - apiGroups: [apps]
        resources: [replicasets, deployments, statefulsets, daemonsets]
        verbs: [get, list, watch]
      - nonResourceURLs: [/metrics, /metrics/cadvisor]
        verbs: [get]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: edot-collector
    subjects:
      - kind: ServiceAccount
        name: edot-collector
        namespace: opentelemetry-operator-system
    roleRef:
      kind: ClusterRole
      name: edot-collector
      apiGroup: rbac.authorization.k8s.io
    ```
    Then create the `OpenTelemetryCollector` CR:
    ```yaml
    apiVersion: opentelemetry.io/v1beta1
    kind: OpenTelemetryCollector
    metadata:
      name: edot-daemon
      namespace: opentelemetry-operator-system
    spec:
      mode: daemonset
      image: docker.elastic.co/elastic-agent/elastic-otel-collector:9.4.2
      serviceAccount: edot-collector
      hostNetwork: true
      securityContext:
        runAsUser: 0
        runAsGroup: 0
      env:
        - name: OTEL_K8S_NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
      volumes:
        - name: varlogpods
          hostPath:
            path: /var/log/pods
        - name: hostfs
          hostPath:
            path: /
      volumeMounts:
        - name: varlogpods
          mountPath: /var/log/pods
          readOnly: true
        - name: hostfs
          mountPath: /hostfs
          readOnly: true
          mountPropagation: HostToContainer
      config:
        receivers:
          otlp:
            protocols:
              grpc:
                endpoint: 0.0.0.0:4317
              http:
                endpoint: 0.0.0.0:4318
          filelog:
            retry_on_failure:
              enabled: true
            start_at: end
            include:
              - /var/log/pods/*/*/*.log
            exclude:
              - /var/log/pods/opentelemetry-operator-system_edot-daemon*/*/*.log
            include_file_name: false
            include_file_path: true
            operators:
              - id: container-parser
                type: container
          hostmetrics:
            collection_interval: 60s
            root_path: /hostfs
            scrapers:
              cpu:
                metrics:
                  system.cpu.utilization:
                    enabled: true
                  system.cpu.logical.count:
                    enabled: true
              disk: {}
              load: {}
              filesystem:
                exclude_mount_points:
                  mount_points: [/dev/*,/proc/*,/sys/*,/run/k3s/containerd/*,/var/lib/docker/*,/var/lib/kubelet/*,/snap/*]
                  match_type: regexp
                exclude_fs_types:
                  fs_types: [autofs,binfmt_misc,bpf,cgroup2fs,configfs,debugfs,devpts,devtmpfs,fusectl,hugetlbfs,iso9660,mqueue,nsfs,overlay,proc,procfs,pstore,rpc_pipefs,securityfs,selinuxfs,squashfs,sysfs,tracefs]
                  match_type: strict
              memory:
                metrics:
                  system.memory.utilization:
                    enabled: true
              network: {}
              paging:
                metrics:
                  system.paging.utilization:
                    enabled: true
              process:
                mute_process_exe_error: true
                mute_process_io_error: true
                mute_process_user_error: true
          kubeletstats:
            auth_type: serviceAccount
            collection_interval: 60s
            endpoint: ${env:OTEL_K8S_NODE_NAME}:10250
            node: ${env:OTEL_K8S_NODE_NAME}
            insecure_skip_verify: true
        processors:
          batch: {}
          batch/metrics:
            send_batch_max_size: 0
            timeout: 1s
          k8sattributes:
            filter:
              node_from_env_var: OTEL_K8S_NODE_NAME
            passthrough: false
            pod_association:
              - sources:
                  - from: resource_attribute
                    name: k8s.pod.ip
              - sources:
                  - from: resource_attribute
                    name: k8s.pod.uid
              - sources:
                  - from: connection
            extract:
              metadata:
                - k8s.namespace.name
                - k8s.deployment.name
                - k8s.replicaset.name
                - k8s.statefulset.name
                - k8s.daemonset.name
                - k8s.cronjob.name
                - k8s.job.name
                - k8s.node.name
                - k8s.pod.name
                - k8s.pod.ip
                - k8s.pod.uid
                - k8s.pod.start_time
          resourcedetection/system:
            detectors: [system]
            system:
              hostname_sources: [os]
          resource/hostname:
            attributes:
              - key: host.name
                from_attribute: k8s.node.name
                action: upsert
        exporters:
          otlp/gateway:
            endpoint: "http://edot-gateway-collector.opentelemetry-operator-system.svc.cluster.local:4317"
            tls:
              insecure: true
        service:
          pipelines:
            logs: null
            metrics: null
            traces: null
            logs/node:
              receivers: [filelog]
              processors: [batch, k8sattributes, resourcedetection/system, resource/hostname]
              exporters: [otlp/gateway]
            metrics/node:
              receivers: [kubeletstats, hostmetrics]
              processors: [batch/metrics, k8sattributes, resourcedetection/system, resource/hostname]
              exporters: [otlp/gateway]
            metrics/app:
              receivers: [otlp]
              processors: [batch/metrics, resource/hostname]
              exporters: [otlp/gateway]
            logs/app:
              receivers: [otlp]
              processors: [batch, resource/hostname]
              exporters: [otlp/gateway]
            traces/app:
              receivers: [otlp]
              processors: [batch, resource/hostname]
              exporters: [otlp/gateway]
    ```

    <note>
      The example above includes `resourcedetection/system` for host attribute detection. For cloud-managed Kubernetes clusters, also add the appropriate cloud provider detector: `resourcedetection/eks` for AWS EKS, `resourcedetection/gcp` for Google GKE, or `resourcedetection/aks` for Azure AKS. Refer to the [kube-stack `values.yaml`](https://github.com/elastic/elastic-agent/blob/main/deploy/helm/edot-collector/kube-stack/values.yaml) for the complete processor configurations.
    </note>
  </step>

  <step title="Deploy the cluster collector">
    The cluster collector runs as a single Deployment to gather cluster-level Kubernetes events and metrics. It reuses the same `edot-collector` ServiceAccount created in the previous step.
    ```yaml
    apiVersion: opentelemetry.io/v1beta1
    kind: OpenTelemetryCollector
    metadata:
      name: edot-cluster
      namespace: opentelemetry-operator-system
    spec:
      mode: deployment
      replicas: 1
      image: docker.elastic.co/elastic-agent/elastic-otel-collector:9.4.2
      serviceAccount: edot-collector
      env:
        - name: OTEL_K8S_NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
      config:
        receivers:
          k8s_events: {}
          k8s_cluster:
            collection_interval: 60s
            node_conditions_to_report: [Ready, MemoryPressure]
            allocatable_types_to_report: [cpu, memory]
        processors:
          batch: {}
          k8sattributes:
            passthrough: false
            pod_association:
              - sources:
                  - from: resource_attribute
                    name: k8s.pod.ip
              - sources:
                  - from: resource_attribute
                    name: k8s.pod.uid
              - sources:
                  - from: connection
            extract:
              metadata:
                - k8s.namespace.name
                - k8s.node.name
                - k8s.pod.name
                - k8s.pod.uid
          resource/hostname:
            attributes:
              - key: host.name
                from_attribute: k8s.node.name
                action: upsert
        exporters:
          otlp/gateway:
            endpoint: "http://edot-gateway-collector.opentelemetry-operator-system.svc.cluster.local:4317"
            tls:
              insecure: true
        service:
          pipelines:
            logs:
              receivers: [k8s_events]
              processors: [batch, resource/hostname]
              exporters: [otlp/gateway]
            metrics:
              receivers: [k8s_cluster]
              processors: [batch, k8sattributes, resource/hostname]
              exporters: [otlp/gateway]
    ```
  </step>

  <step title="Confirm the collectors are running">
    Confirm the collector resources and their pods are running:
    ```bash
    kubectl get opentelemetrycollectors -n opentelemetry-operator-system
    kubectl get pods -n opentelemetry-operator-system
    ```
    Confirm you see:
    - Two `edot-gateway` Deployment pods
    - One `edot-daemon` DaemonSet pod per node
    - One `edot-cluster` Deployment pod
  </step>

  <step title="Confirm you see the data in {{kib}}">
    1. Open the **OTEL][Metrics Kubernetes] Cluster Overview** dashboard in Kibana.
    2. In **Discover**, confirm data is available in the `logs-*` and `metrics-*` data views.

    <note>
      This step verifies only logs and metrics because the collectors generate them automatically from Kubernetes infrastructure. Traces appear in the `traces-*` data views only after you [instrument your applications](https://docs-v3-preview.elastic.dev/elastic/docs-content/pull/7153/solutions/observability/get-started/opentelemetry/use-cases/kubernetes/instrumenting-applications) to send OTLP data to the collectors.
    </note>
    If no data appears, refer to [No logs, metrics, or traces visible in Kibana](https://docs-v3-preview.elastic.dev/elastic/docs-content/pull/7153/troubleshoot/ingest/opentelemetry/no-data-in-kibana).
  </step>
</stepper>