AWS Distro for OpenTelemetry の Amazon EKS アドオン (ADOT Operator) でやれること

Amazon EKS のアドオンに「AWS Distro for OpenTelemetry (ADOT)」というものがあります。説明としては
> ADOT is generally available (GA) for tracing and can also be used for metrics. Amazon EKS add-ons support for ADOT enables a simplified experience through EKS APIs to install one component of ADOT, the ADOT Operator, in your Amazon EKS cluster for your metrics and/or trace collection pipeline.

> ADOT は一般にトレースに使用でき (GA)、メトリクスにも使用できます。ADOT の Amazon EKS アドオンサポートにより、EKS API を使用して ADOT の 1 つのコンポーネントである ADOT オペレーターを Amazon EKS クラスターにインストールして、メトリックスやトレース収集パイプラインを簡単に行うことができます。

https://aws-otel.github.io/docs/getting-started/adot-eks-add-on#introduction-1

というものですが、ADOT Operator は具体的にどのようなことをしてくれるのでしょうか。

### ADOT Operator とは
ADOT Operator は Kubernetes Operator の実装で、カスタムリソース定義 (CRD) を使用してカスタムリソース (CR) のデプロイと設定を簡素化してくれます。具体的に言うと ADOT Operator は OpenTelemetryCollector というカスタムリソースの定義をもとに、OpenTelemetry Collector を Kubernetes クラスター上に SideCar や Daemonset などのタイプで配置してくれます。

https://aws-otel.github.io/docs/getting-started/adot-eks-add-on#the-adot-operator-and-adot-collector

また、instrumentation というカスタムリソースの定義をもとに auto-instrumentation-injection を行ってくれる機能があります。

https://aws-otel.github.io/docs/getting-started/adot-eks-add-on/config-auto-instrumentation

auto-instrumentation-injection についてはこちらのページで紹介されていますが、こちらの機能はまた別の機会で触りたいと思います。

https://github.com/open-telemetry/opentelemetry-operator#opentelemetry-auto-instrumentation-injection

なお、auto-instrumentation-injection は特定のアプリケーション言語限定の機能のようです。

https://opentelemetry.io/docs/kubernetes/operator/automatic/

### OpenTelemetry Collector とは
カスタムリソースの OpenTelemetryCollector でデプロイされる　OpenTelemetry Collector とは何なのでしょうか？こちらにドキュメントがあります。
https://opentelemetry.io/docs/collector/

> The OpenTelemetry Collector offers a vendor-agnostic implementation of how to receive, process and export telemetry data. It removes the need to run, operate, and maintain multiple agents/collectors. This works with improved scalability and supports open source observability data formats (e.g. Jaeger, Prometheus, Fluent Bit, etc.) sending to one or more open source or commercial backends. The local Collector agent is the default location to which instrumentation libraries export their telemetry data.

> OpenTelemetry Collectorは、テレメトリデータの受信、処理、エクスポートの方法をベンダーにとらわれずに実装します。これにより、複数のエージェント/コレクターを実行、運用、保守する必要がなくなります。これにより、スケーラビリティが向上し、オープンソースのオブザーバビリティデータ形式 (Jaeger、Prometheus、Fluent Bit など) を 1 つ以上のオープンソースまたは商用バックエンドに送信できます。ローカル Collector エージェントは、インストルメンテーションライブラリがテレメトリデータをエクスポートするデフォルトの場所です。

テレメトリデータの収集や処理は、可視化を行うツールなどに併せて計装を行う必要があります。例えば AWS X-Ray でトレースデータを可視化する場合は AWS X-Ray の SDK を計装する必要があります。OpenTelemetry Collector は以下の図のようにテレメトリデータの受け渡しを仲介し、様々な場所にテレメトリデータを送信できます。

<img src="https://opentelemetry.io/docs/collector/img/otel-collector.svg">

OpenTelemetry 自体も OTLP (OpenTelemetry Protocol) を利用して通信を行うので、OpenTelemetry SDK から直接テレメトリデータを送る場合、送信先が OTLP で受けれる必要がありますが、OpenTelemetry Collector を利用することで送信先が OTLP に対応してなくても、OpenTelemetry Collector の exporter が送信を可能にします。

OpenTelemetry Collector のデプロイパターンはいくつかあり、以下にまとまっています。

https://github.com/jpkrohling/opentelemetry-collector-deployment-patterns/?tab=readme-ov-file

この中の Pattern #3 には Kubernetes 上でのデプロイパターンが紹介されています。こちらでは  SideCar としてデプロイするパターンと DaemonSet でデプロイするパターンとがあります。

https://github.com/jpkrohling/opentelemetry-collector-deployment-patterns/tree/main/pattern-3-kubernetes

SideCar パターン

<img src="https://github.com/jpkrohling/opentelemetry-collector-deployment-patterns/blob/main/pattern-3-kubernetes/sidecar.png?raw=true">

DaemonSet パターン

<img src="https://github.com/jpkrohling/opentelemetry-collector-deployment-patterns/blob/main/pattern-3-kubernetes/daemonset.png?raw=true">

どちらを利用するかは利用するコンテキストによりますが、例えば Collector の設定を Injection する Pod 単位で分けたい場合は SideCar パターンでしょうか。しかし SideCar は Pod の数だけ起動するためリソースを多く消費します。リソースの集約をしたり設定の共通化をしたい場合は DaemonSet パターンが考えられます。DaemonSet パターンは namespace に関係なく、Node 内のすべての Pod からテレメトリデータを受信するため、マルチテナントデプロイメントにはお勧めできません。リソース状況やワークロードにより選択する必要があります。

他のパターンとしては通常の Deployment パターン、StatefulSet パターンがあります。Deployment は任意の Collector をデプロイします。DaemonSet との差はやはりリソースの集約や設定の共通化が考慮ポイントでしょうか。

StatefulSet パターンですが、以下のブログにあるように Persistent Volume を利用して状態を管理します。

https://aws.amazon.com/jp/blogs/opensource/adding-statefulset-support-in-the-opentelemetry-operator/

具体的なユースケースがドキュメント上では明記されていませんが、Target Allocator を利用する際に、複数配置された Collector の PrometheusReceiver がどのターゲットをスクレイピングするかの状態を持っておく、という感じでしょうか。ユースケースについては引き続き探したいと思います。

https://opentelemetry.io/docs/kubernetes/operator/target-allocator/

### ADOT Operator は何をしてくれるのか
上記で 4つのパターン(SideCar、DaemonSet、Deployment、StatefulSet) を紹介しましたが、それぞれのパターンごとに手動で Collector をデプロイするのは手間が掛かります。DaemonSet パターンで配置したい場合は Node がスケールするタイミングで配置を行いたいですし、SideCar パターンの場合は yaml で定義しておけばよいですが、デプロイされるすべての Pod の yaml に一つ一つ定義を入れるのは大変です。
ADOT Operator は OpenTelemetryCollector というカスタムリソース定義の中に、上記のデプロイモードとデプロイしたい設定を定義しておくことで、Operator が自動で設定をしてくれます。Amazon EKS アドオンはこの Operator の管理やクラスターへのインストールの簡易化をしてくれます。

### SideCar パターンの場合
以下のようなカスタムリソースを定義し、
```
apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
metadata:
  name: sidecar-for-my-app
spec:
  mode: sidecar
  config: |
    receivers:
      jaeger:
        protocols:
          thrift_compact:
    processors:

    exporters:
      debug:

    service:
      pipelines:
        traces:
          receivers: [jaeger]
          processors: []
          exporters: [debug]
```

SideCar を injection したい Pod の annotations に以下の定義を入れておきます。

```
apiVersion: v1
kind: Pod
metadata:
  name: myapp
  annotations:
    sidecar.opentelemetry.io/inject: "true" #　←　ここ
spec:
  containers:
  - name: myapp
    image: jaegertracing/vertx-create-span:operator-e2e-tests
    ports:
      - containerPort: 8080
        protocol: TCP
```

annotations に定義しておくことにより、Pod を起動した際に自動的に Collector がカスタムリソース定義で設定した config をもとに Collector のコンテナが起動します。

```
Containers:
  myapp:
    Container ID:   containerd://d666e37dcbf8e1dc3a616b9ea4b6433ef34b0bd63bb50b741ae131baec88e262
    Image:          jaegertracing/vertx-create-span:operator-e2e-tests
    Image ID:       docker.io/jaegertracing/vertx-create-span@sha256:6704312715644554fe4d51e0ce5cb0032e9231653ac61bdbdb5f290cb637d421
    Port:           8080/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Thu, 01 Feb 2024 22:12:47 +0900
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-tl8zh (ro)
  otc-container:
    Container ID:  containerd://1e54ac49d512cad3839e6d381ecf00439664244a85d03e7d730a632f0c47910d
    Image:         ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector:0.92.0
    Image ID:      ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector@sha256:92f6e2efd014152bee26f8324e3a511980b512a36d8793d3fee708715caaa6c0
    Ports:         8888/TCP, 6831/UDP
    Host Ports:    0/TCP, 0/UDP
    Args:
      --config=env:OTEL_CONFIG
    State:          Running
      Started:      Thu, 01 Feb 2024 22:12:47 +0900
    Ready:          True
    Restart Count:  0
    Environment:
      POD_NAME:                            myapp (v1:metadata.name)
      OTEL_CONFIG:                         receivers:
                                             jaeger:
                                               protocols:
                                                 thrift_compact:
                                           processors:
                                           
                                           exporters:
                                             debug:
                                           
                                           service:
                                             pipelines:
                                               traces:
                                                 receivers: [jaeger]
                                                 processors: []
                                                 exporters: [debug]
                                           
      OTEL_RESOURCE_ATTRIBUTES_POD_NAME:   myapp (v1:metadata.name)
      OTEL_RESOURCE_ATTRIBUTES_POD_UID:     (v1:metadata.uid)
      OTEL_RESOURCE_ATTRIBUTES_NODE_NAME:   (v1:spec.nodeName)
      OTEL_RESOURCE_ATTRIBUTES:            k8s.namespace.name=default,k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME),k8s.pod.uid=$(OTEL_RESOURCE_ATTRIBUTES_POD_UID)
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-tl8zh (ro)
```

[参考]

https://github.com/open-telemetry/opentelemetry-operator?tab=readme-ov-file#sidecar-injection

### DaemonSet パターンの場合
以下のようなカスタムリソースを定義します。
```
apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
metadata:
  name: daemonset
spec:
  mode: daemonset
  hostNetwork: true
  config: |
    receivers:
      jaeger:
        protocols:
          grpc:
    processors:
    exporters:
      debug:
    service:
      pipelines:
        traces:
          receivers: [jaeger]
          processors: []
          exporters: [otlp]
```

DaemonSet がデプロイされました。
```
$ kubectl get pods -o wide
NAME                                   READY   STATUS             RESTARTS       AGE   IP              NODE                            NOMINATED NODE   READINESS GATES
daemonset-collector-8b6r4              1/1     Running            0              8s    10.10.131.136   ip-10-10-131-136.ec2.internal   <none>           <none>
```

Node を増やしてみます。
```
$ kubectl get pods -o wide
NAME                                   READY   STATUS             RESTARTS         AGE     IP              NODE                            NOMINATED NODE   READINESS GATES
daemonset-collector-8b6r4              1/1     Running            0                2m32s   10.10.131.136   ip-10-10-131-136.ec2.internal   <none>           <none>
daemonset-collector-d2ncx              1/1     Running            0                35s     10.10.160.223   ip-10-10-160-223.ec2.internal   <none>           <none>
daemonset-collector-p64xm              1/1     Running            0                31s     10.10.156.69    ip-10-10-156-69.ec2.internal    <none>           <none>
```

カスタムリソースの定義をもとに、DaemonSet として動作しています。
```
Containers:
  otc-container:
    Container ID:  containerd://b6816823c9cccd153b0e40f22010b2084aad48dc58ae19721edae26d92c59f44
    Image:         public.ecr.aws/aws-observability/aws-otel-collector:v0.35.0
    Image ID:      public.ecr.aws/aws-observability/aws-otel-collector@sha256:0c69de5b75b85a4520eff546c123404dd1074448066905ef30a86511d421f618
    Ports:         8888/TCP, 6831/UDP
    Host Ports:    8888/TCP, 6831/UDP
    Args:
      --config=/conf/collector.yaml
    State:          Running
      Started:      Fri, 02 Feb 2024 18:18:33 +0900
    Ready:          True
    Restart Count:  0
    Environment:
      POD_NAME:  daemonset-collector-8b6r4 (v1:metadata.name)
    Mounts:
      /conf from otc-internal (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-bn4l4 (ro)
```

## まとめ
Amazon EKS アドオンで有効にできる AWS Distro for OpenTelemetry (ADOT) Operator でどんなことができるのかを調べた結果をまとめました。ADOT Operator は OpenTelemetryCollector というカスタムリソース定義を元に OpenTelemetry Collector の管理を行ってくれます。また一部の言語限定ではありますが auto-instrumentation-injection も実施してくれます。EKS アドオンはこの ADOT Operator の管理や更新の簡易化を可能にします。auto-instrumentation-injection については別の機会に DiveDeep しようと思います。