EKS FargateのDatadog導入に躓いた話

## はじめに

Datadog導入するにあたってインフラメトリクスを取得するのに困難したので躓いたポイントをまとめて行きたいと思います。

## Datadogのアーキテクチャ

以下KubernetesにDatadogを導入した際のアーキテクチャです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295942/96ce9d9d-0ffa-7569-aa53-dd55a9f4779d.png" width="70%">

公式には以下のようなことが記述されています。

>Cluster Agent は、API サーバーとノードベースの Agent 間のプロキシとして機能します。これにより、API サーバーへの直接の負荷が軽減されるだけでなく、ノードベースの Agent はノードレベルのデータの収集に集中できます。一方、Cluster Agent はマスターノードからクラスターレベルのデータを収集します。Cluster Agent は、クラスターレベルのメタデータをノードベースの Agent に送り返すため、ローカルで収集されたメトリクスを、クラスター全体で一貫したタグを設定して強化できます。また、ノードベースの Agent は API サーバーからこのデータのクエリする必要がなくなりましたので、RBAC ルールを減らしてメトリクスとメタデータのみを kubelet から読み取ることもがきます。

Ref: https://www.datadoghq.com/ja/blog/datadog-cluster-agent

## 何で躓いたのか？

### kubeletへの権限エラー

NodeAgentはFargateの場合apiserverからノードへプロキシして、ノード内のメトリクス収集を行っています。

```go:pkg/util/kubernetes/kubelet/kubelet_client.go
	if kubeletProxyEnabled {
		// Explicitly disable HTTP to reach APIServer
		kubeletHTTPPort = 0
		httpsPort, err := strconv.ParseUint(os.Getenv("KUBERNETES_SERVICE_PORT"), 10, 16)
		if err != nil {
			return nil, fmt.Errorf("unable to get APIServer port: %w", err)
		}
		kubeletHTTPSPort = int(httpsPort)

		if config.Datadog.Get("kubernetes_kubelet_nodename") != "" {
			kubeletPathPrefix = fmt.Sprintf("/api/v1/nodes/%s/proxy", kubeletNodeName)
			apiServerHost := os.Getenv("KUBERNETES_SERVICE_HOST")

			potentialHosts = &connectionInfo{
				hostnames: []string{apiServerHost},
			}
			log.Infof("EKS on Fargate mode detected, will proxy calls to the Kubelet through the APIServer at %s:%d%s", apiServerHost, kubeletHTTPSPort, kubeletPathPrefix)
		} else {
			return nil, errors.New("kubelet proxy mode enabled but nodename is empty - unable to query")
		}
	} else {
		// Building a list of potential ips/hostnames to reach Kubelet
		potentialHosts = getPotentialKubeletHosts(kubeletHost)
	}

	// Checking HTTPS first if port available
	var httpsErr error
	if kubeletHTTPSPort > 0 {
		httpsErr = checkKubeletConnection(ctx, "https", kubeletHTTPSPort, kubeletPathPrefix, potentialHosts, &clientConfig)
		if httpsErr != nil {
			log.Debug("Impossible to reach Kubelet through HTTPS")
			if kubeletHTTPPort <= 0 {
				return nil, httpsErr
			}
		} else {
			return newForConfig(clientConfig, kubeletTimeout)
		}
	}
```

ですので、ClusterRoleには以下のように権限を付与します。

```yml:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: datadog-agent
rules:
  - apiGroups:
    - ""
    resources:
    - nodes
    - namespaces
    verbs:
    - get
    - list
  - apiGroups:
      - ""
    resources:
      - nodes/metrics
      - nodes/spec
      - nodes/stats
      - nodes/proxy
      - nodes/pods
      - nodes/healthz
    verbs:
      - get
```

### Namespace間通信

ClusterAgentは「datadog」、NodeAgentは「golang」というnamespaceで運用しているので異なるnamespace間で通信しなければなりません。その為にClusterAgentのServiceのエンドポイントが必要です。ですので、`DD_CLUSTER_AGENT_URL`にて設定しました。

`svc.cluster.local`としているのはデフォルトで使用するNameServerでは名前解決できないからです。

```yml:
- name: DD_CLUSTER_AGENT_URL
  value: "https://datadog-agent-cluster-agent.datadog.svc.cluster.local:5005"
```

### tokenの不一致

ClusterAgentとNodeAgentで異なるtokenを設定してしまっており、403エラーが出ていました。

```go:pkg/api/util/util.go
if len(tok) < 2 || tok[1] != GetAuthToken() {
    err = fmt.Errorf("invalid session token")
    http.Error(w, err.Error(), 403)
}
```

ClusterAgentはデフォルトでNodeAgentの両方と共有するためのSecretにランダムトークンを自動生成します。

```go:cmd/cluster-agent/api/server.go
util.CreateAndSetAuthToken()
```

しかし、異なるNamespaceで運用する場合は、生成されたNodeAgent側からSecretを参照できなくなります。ですので、私は手動でtokenをClusterAgent、NodeAgent側に作成し、Secretとして参照できるようにしました。(clusterAgent.tokenExistingSecretにて設定)

ClusterAgent側では以下のように設定します。(HelmとArgoCDを使用)

```yaml:cluster-agent.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: datadog-agent
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    repoURL: https://helm.datadoghq.com
    targetRevision: 2.27.2
    helm:
      values: |
        targetSystem: linux
        datadog:
          apiKeyExistingSecret: datadog-secrets
          appKeyExistingSecret: datadog-secrets
          leaderElection: true
          collectEvents: true
          kubeStateMetricsEnabled: false
          kubeStateMetricsCore:
            enabled: true
          logLevel: INFO
          apm:
            enabled: true
          processAgent:
            enabled: true
            processCollection: true
        clusterAgent:
          tokenExistingSecret: datadog-secrets
          enabled: true
          metricsProvider:
            enabled: true
          rbac:
            create: true
        clusterChecks:
          enabled: true
        clusterChecksRunner:
          enabled: true
          replicas: 2
          rbac:
            create: true
        agents:
          rbac:
            create: true
      parameters:
        - name: "datadog.tags[0]"
          value: "env:prd"
        - name: "datadog.tags[1]"
          value: "system:golang"
        - name: "datadog.clusterName"
          value: "golang-cluster"
    chart: datadog
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: datadog
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
---
apiVersion: external-secrets.io/v1alpha1
kind: SecretStore
metadata:
  name: datadog
  namespace: datadog
spec:
  provider:
    aws:
      service: SecretsManager
      region: ap-northeast-1
---
apiVersion: external-secrets.io/v1alpha1
kind: ExternalSecret
metadata:
  name: datadog-secrets
  namespace: datadog
spec:
  refreshInterval: 1m
  secretStoreRef:
    name: datadog
    kind: SecretStore
  target:
    name: datadog-secrets
    creationPolicy: Owner
  data:
  - secretKey: api-key
    remoteRef:
      key: datadog/apikey
  - secretKey: app-key
    remoteRef:
      key: datadog/appkey
  # secretKeyはtokenにしなければなりません
  - secretKey: token
    remoteRef:
      key: datadog/cluster-agent-token
```

NodeAgent側では以下のように設定します。

```yaml:deployment.yml
 - name: DD_CLUSTER_AGENT_AUTH_TOKEN
   valueFrom:
     secretKeyRef:
       name: go-secrets
       key: token
```

```yaml:secrets.yml
apiVersion: external-secrets.io/v1alpha1
kind: SecretStore
metadata:
  name: golang
  namespace: golang
spec:
  provider:
    aws:
      service: SecretsManager
      region: ap-northeast-1
---
apiVersion: external-secrets.io/v1alpha1
kind: ExternalSecret
metadata:
  name: golang-secrets
  namespace: golang
spec:
  refreshInterval: 1m
  secretStoreRef:
    name: golang
    kind: SecretStore
  target:
    name: golang-secrets
    creationPolicy: Owner
  data:
  - secretKey: datadog_api_key
    remoteRef:
      key: datadog/apikey
  # token
  - secretKey: token
    remoteRef:
      key: datadog/cluster-agent-token
```

## まとめ

EKSのEC2構成ではDaemonSetで簡単に導入できますが、Fargate構成では導入にあたって考慮するべき点がたくさんありました。これに加えて、EC2とFargateのマルチ構成の場合ではセキュリティーグループのルールなども考慮しなければいけないと思い、意外と大変だなあと感じました。

## その他

調査は以下のコマンドたちを用いました。

```
datadog-cluster-agent status
```

```
cat /var/log/datadog/datadog-cluster-agent.log
```

```
agent status
```

```
cat /var/log/datadog/agent.log
```

## 参考文献

https://github.com/DataDog/helm-charts

- Amazon EKS on AWS Fargate

https://docs.datadoghq.com/ja/integrations/eks_fargate

- Cluster Agentのドキュメント

https://docs.datadoghq.com/ja/containers/cluster_agent/setup/?tab=helm