EKSで「CloudWatch Container Insights」を利用できるように設定する

EKSで「CloudWatch Container Insights」を設定する方法について、ステップバイステップで手順を解説します。
2020.03.25

みなさん、こんにちは!
AWS事業本部の青柳@福岡オフィスです。

CloudWatchの新機能として、ECSやEKSなどのコンテナワークロードのパフォーマンスとログデータを収集して分析することができる Container Insights が2019年9月にリリースされました。

Container Insightsの概要とECSで利用する設定手順については、リリース時に執筆されたブログ記事で紹介されています。

ECSやEKSのメトリクスを一括取得するContainer Insightsが一般公開!既存ECSクラスタも追加設定可能に! | Developers.IO

今回は、EKSでContainer Insightsを利用できるように設定してみました。

構成

設定手順

AWSドキュメント では、EKSでのContainer Insightsの設定方法について、2通りの方法が案内されています。

  1. 「クイックスタートセットアップ」で設定する
  2. ステップ・バイ・ステップで確認しながら設定する。

「1」の方法は、必要な設定を全て含んだKubernetesマニフェストを使うことで、一撃で設定を行うことができます。

「2」の方法は、一つずつ手順を追って設定する必要がありますが、EKSでContainer Insightsを利用できるようにするためにどのようなリソースや設定が必要なのか、問題が起きた時にどこを調べればよいのか、といったことを理解することができます。

今回は「2」の方法で進めることにします。

使用したツール類のバージョン

$ eksctl version
0.15.0

$ aws --version
aws-cli/1.18.26 Python/3.6.9 Linux/4.4.0-18362-Microsoft botocore/1.15.26

$ kubectl version --client --short
Client Version: v1.17.0

EKSクラスターの準備

Container Insightsを設定するEKSクラスターを準備します。
eksctl コマンドを使用してクラスターを作成します。

eksctl create cluster \
  --name eks-example \
  --nodegroup-name ng-example \
  --node-type t3.large \
  --nodes 2 \
  --managed

これで、ワーカーノードを2つ持つEKSクラスターが作成されます。

前提条件: ワーカーノードのIAMロールに必要なポリシーを追加する

Container Insightsの設定を始める前に、前提条件を確認します。

前提条件の確認 - Amazon CloudWatch

今回の環境で必要となるのは「必要なポリシーをワーカーノードのIAMロールに追加するには」の部分です。

マネジメントコンソールでEKSクラスターの情報を表示して、ノードグループ名をクリックします。

ノードグループの詳細画面で「ノードIAMロール名」をクリックします。

IAMロールにポリシーをアタッチします。

AWS管理ポリシー CloudWatchAgentServerPolicy を選択して、アタッチします。

これで、ワーカーノード (コンテナが動作するEC2インスタンス) にポリシーが設定されました。

このポリシーを設定する目的は、この後の手順で設定する「CloudWatchエージェント」「Fluentd」のDaemonSetにより実行されるコンテナに対して「CloudWatchへメトリクスやログを送信する権限」を与えることです。

ステップ1: メトリクス収集を行うCloudWatchエージェントを導入する

以下のページの内容に沿って進めます。

クラスターメトリクスを収集するよう CloudWatch エージェントをセットアップする - Amazon CloudWatch

なお、ここで言う「CloudWatchエージェント」とは、EC2インスタンスに対してインストールするCloudWatchエージェントではなく、Container Insightsのために用意されている専用のエージェントであり、コンテナイメージとして提供されています。

(1) 名前空間を作成する

Kubernetesには「名前空間」(Namespace) の概念があり、名前空間にはKubernetesのシステム関連リソースが配置されている「kube-system」や、ユーザーアプリケーションがデフォルトで配置される「default」などがあります。

ここでは、CloudWatchエージェント専用のNamespace「amazon-cloudwatch」を作成します。

Namespaceを作成するマニフェストが用意されていますので、ダウンロードします。

curl -O https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/k8s-deployment-manifest-templates/deployment-mode/daemonset/container-insights-monitoring/cloudwatch-namespace.yaml
マニフェストファイルの内容 (クリックすると展開します)

cloudwatch-namespace.yaml

# create amazon-cloudwatch namespace
apiVersion: v1
kind: Namespace
metadata:
  name: amazon-cloudwatch
  labels:
    name: amazon-cloudwatch

kubernetes apply コマンドを使ってNamespaceリソースを作成します。

$ kubectl apply -f cloudwatch-namespace.yaml
namespace/amazon-cloudwatch created

(2) サービスアカウントおよび関連リソースを作成する

「サービスアカウント」(ServiceAccount) とはKubernetes上のユーザーアカウントの一種であり、人が操作するためではなく、アプリケーション・サービスの実行主体となるアカウントです。
(AWSのIAMロールに近い概念です)

こちらもマニフェストが用意されていますので、ダウンロードします。

curl -O https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/k8s-deployment-manifest-templates/deployment-mode/daemonset/container-insights-monitoring/cwagent/cwagent-serviceaccount.yaml
マニフェストファイルの内容 (クリックすると展開します)

cwagent-serviceaccount.yaml

# create cwagent service account and role binding
apiVersion: v1
kind: ServiceAccount
metadata:
  name: cloudwatch-agent
  namespace: amazon-cloudwatch

---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: cloudwatch-agent-role
rules:
  - apiGroups: [""]
    resources: ["pods", "nodes", "endpoints"]
    verbs: ["list", "watch"]
  - apiGroups: ["apps"]
    resources: ["replicasets"]
    verbs: ["list", "watch"]
  - apiGroups: ["batch"]
    resources: ["jobs"]
    verbs: ["list", "watch"]
  - apiGroups: [""]
    resources: ["nodes/proxy"]
    verbs: ["get"]
  - apiGroups: [""]
    resources: ["nodes/stats", "configmaps", "events"]
    verbs: ["create"]
  - apiGroups: [""]
    resources: ["configmaps"]
    resourceNames: ["cwagent-clusterleader"]
    verbs: ["get","update"]

---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: cloudwatch-agent-role-binding
subjects:
  - kind: ServiceAccount
    name: cloudwatch-agent
    namespace: amazon-cloudwatch
roleRef:
  kind: ClusterRole
  name: cloudwatch-agent-role
  apiGroup: rbac.authorization.k8s.io

マニフェストでは3つのリソースを作成します。

「ServiceAccount」は、上で説明した通り、CloudWatchエージェントを実行する主体となるアカウントです。

「ClusterRole」は、アカウントに対して与える権限を定義するものです。
メトリクスを取得するために必要な「NodeやPodの情報を参照する権限」などが記述されています。
(Kubernetesの「ClusterRole」はAWSの「ロール」とは意味合いが異なり、どちらかと言うとAWSのIAMポリシーに近い概念です)

「ClusterRoleBinding」は、「ServiceAccount」に対して「ClusterRole」を割り当てるものです。

kubernetes apply コマンドを使って各リソースを作成します。

$ kubectl apply -f cwagent-serviceaccount.yaml
serviceaccount/cloudwatch-agent created
clusterrole.rbac.authorization.k8s.io/cloudwatch-agent-role created
clusterrolebinding.rbac.authorization.k8s.io/cloudwatch-agent-role-binding created

(3) ConfigMapを作成する

「ConfigMap」とは、Kubernetesのコンテナに対する設定情報などを格納するリソースです。
ConfigMapに格納された設定情報は、コンテナ内から環境変数やファイルなどを経由して参照することができます。

ここでは、CloudWatchエージェントのコンテナに対する設定をConfigMapに記述します。

ConfigMapを作成するマニフェストのテンプレートが用意されていますので、ダウンロードします。

curl -O https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/k8s-deployment-manifest-templates/deployment-mode/daemonset/container-insights-monitoring/cwagent/cwagent-configmap.yaml

マニフェストテンプレートの内容は以下のようになっています。

cwagent-configmap.yaml

# create configmap for cwagent config
apiVersion: v1
data:
  # Configuration is in Json format. No matter what configure change you make,
  # please keep the Json blob valid.
  cwagentconfig.json: |
    {
      "logs": {
        "metrics_collected": {
          "kubernetes": {
            "cluster_name": "{{cluster_name}}",
            "metrics_collection_interval": 60
          }
        },
        "force_flush_interval": 5
      }
    }
kind: ConfigMap
metadata:
  name: cwagentconfig
  namespace: amazon-cloudwatch

このマニフェストテンプレートを基に、環境に応じて内容を更新してマニフェストを準備します。

最低限の必要な修正は、11行目の "cluster_name": "{{cluster_name}}" の部分です。

具体的には {{cluster_name}} をEKSクラスター名で置き換えます。
今回の例であれば以下のようにします。

"cluster_name": "eks-example"

もしくは、EKSクラスター名の指定を省略することもできます。(その場合は11行目の全体を削除します)

その他の設定項目・設定の記述方法については、下記リンク先を参照してください。
CloudWatch エージェントの ConfigMap を作成する

ConfigMapの編集が終わりましたら、kubernetes apply コマンドを使ってConfigMapリソースを作成します。

$ kubectl apply -f cwagent-configmap.yaml
configmap/cwagentconfig created

(4) DaemonSetとしてCloudWatchエージェントをデプロイする

「DaemonSet」とは、Kubernetes上でコンテナを実行するワークロードの種類のうちの一つです。

DaemonSetで定義されたコンテナは、各ワーカーノード (EC2インスタンス) 上で常駐型のコンテナとして実行されます。
全てのワーカーノードにおいて、ワーカーノード1台に対してコンテナ1個が配置されます。

Container Insightsでは、各ワーカーノードでCloudWatchエージェントのコンテナが常駐することで、ワーカーノードのメトリクスおよびワーカーノード上で実行されるコンテナのメトリクスを収集します。

DaemonSstを作成するマニフェストが用意されていますので、ダウンロードします。

curl -O https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/k8s-deployment-manifest-templates/deployment-mode/daemonset/container-insights-monitoring/cwagent/cwagent-daemonset.yaml
マニフェストファイルの内容 (クリックすると展開します)

cwagent-daemonset.yaml

# deploy cwagent as daemonset
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: cloudwatch-agent
  namespace: amazon-cloudwatch
spec:
  selector:
    matchLabels:
      name: cloudwatch-agent
  template:
    metadata:
      labels:
        name: cloudwatch-agent
    spec:
      containers:
        - name: cloudwatch-agent
          image: amazon/cloudwatch-agent:1.231221.0
          #ports:
          #  - containerPort: 8125
          #    hostPort: 8125
          #    protocol: UDP
          resources:
            limits:
              cpu:  200m
              memory: 200Mi
            requests:
              cpu: 200m
              memory: 200Mi
          # Please don't change below envs
          env:
            - name: HOST_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.hostIP
            - name: HOST_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: K8S_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: CI_VERSION
              value: "k8s/1.1.0"
          # Please don't change the mountPath
          volumeMounts:
            - name: cwagentconfig
              mountPath: /etc/cwagentconfig
            - name: rootfs
              mountPath: /rootfs
              readOnly: true
            - name: dockersock
              mountPath: /var/run/docker.sock
              readOnly: true
            - name: varlibdocker
              mountPath: /var/lib/docker
              readOnly: true
            - name: sys
              mountPath: /sys
              readOnly: true
            - name: devdisk
              mountPath: /dev/disk
              readOnly: true
      volumes:
        - name: cwagentconfig
          configMap:
            name: cwagentconfig
        - name: rootfs
          hostPath:
            path: /
        - name: dockersock
          hostPath:
            path: /var/run/docker.sock
        - name: varlibdocker
          hostPath:
            path: /var/lib/docker
        - name: sys
          hostPath:
            path: /sys
        - name: devdisk
          hostPath:
            path: /dev/disk/
      terminationGracePeriodSeconds: 60
      serviceAccountName: cloudwatch-agent

DaemonSetで定義されるコンテナは、DockerHubのパブリックリポジトリでAWSが公開している amazon/cloudwatch-agent:1.231221.0 というコンテナイメージから起動されます。

マニフェストには envvolumeMounts などのパラメータが記述されていますが、変更せずにそのままとします。

kubernetes apply コマンドを使ってDaemonSetリソースを作成します。

$ kubectl apply -f cwagent-daemonset.yaml
daemonset.apps/cloudwatch-agent created

正常に作成されたことを確認するために、DaemonSetとPodの一覧を表示します。

$ kubectl get daemonsets -n amazon-cloudwatch
NAME               DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
cloudwatch-agent   2         2         2       2            2           <none>          38s

$ kubectl get pods -n amazon-cloudwatch -o wide
NAME                     READY   STATUS    RESTARTS   AGE   IP               NODE                                               NOMINATED NODE   READINESS GATES
cloudwatch-agent-4427l   1/1     Running   0          96s   192.168.39.252   ip-192-168-36-52.ap-northeast-1.compute.internal   <none>           <none>
cloudwatch-agent-7p2b2   1/1     Running   0          95s   192.168.25.224   ip-192-168-28-45.ap-northeast-1.compute.internal   <none>           <none>

各ワーカーノード上でPodが1つずつ実行されていることが確認できます。

もし、Podが起動しない場合や、起動・停止を繰り返す場合には、Podのイベントやログを確認して原因を調べます。

$ kubectl describe pod cloudwatch-agent-XXXXX -n amazon-cloudwatch
$ kubectl logs cloudwatch-agent-XXXXX -n amazon-cloudwatch

例えば、正常に起動した場合のイベントは以下のようになっています。

$ kubectl describe pod cloudwatch-agent-4427l -n amazon-cloudwatch
Name:           cloudwatch-agent-4427l
Namespace:      amazon-cloudwatch
(中略)
Events:
  Type    Reason     Age    From                                                       Message
  ----    ------     ----   ----                                                       -------
  Normal  Scheduled  2m56s  default-scheduler                                          Successfully assigned amazon-cloudwatch/cloudwatch-agent-4427l to ip-192-168-36-52.ap-northeast-1.compute.internal
  Normal  Pulling    2m56s  kubelet, ip-192-168-36-52.ap-northeast-1.compute.internal  Pulling image "amazon/cloudwatch-agent:1.231221.0"
  Normal  Pulled     2m51s  kubelet, ip-192-168-36-52.ap-northeast-1.compute.internal  Successfully pulled image "amazon/cloudwatch-agent:1.231221.0"
  Normal  Created    2m51s  kubelet, ip-192-168-36-52.ap-northeast-1.compute.internal  Created container cloudwatch-agent
  Normal  Started    2m51s  kubelet, ip-192-168-36-52.ap-northeast-1.compute.internal  Started container cloudwatch-agent

(5) CloudWatchへメトリクスが出力されることを確認する

DaemonSetをデプロイしてしばらく経つと、CloudWatchエージェントからCloudWatchへメトリクスが出力されるようになります。

マネジメントコンソールでCloudWatchのメトリクスを開いて、「カスタム名前空間」に「ContainerInsights」が表示されていることを確認します。

「ContainerInsights」名前空間の配下には、クラスター、ノード、ポッドなど、Kubernetesの各リソースに関するメトリクスが出力されていることが確認できます。

また、CloudWatchエージェントはメトリクスの他にログも出力します。
ロググループ /aws/containerinsights/(クラスター名)/performance が作成されていることを確認します。

これらのメトリクス、ログが出力されていれば、CloudWatchエージェントは正常に動作しています。

もし出力されていない場合には、ここまでの手順を見直してください。
(特に、ワーカーノードのIAMロールにポリシーが追加されているか? Podのイベントやログにエラーが無いか? という点を確認すると良いと思います)

ステップ2: ログ収集を行うFluentdを導入する

Container Insightsのログ収集は、オープンソースのログ収集ソフトウェアである「Fluentd」が使われています。

「Fluentd」は様々な形態で提供されていますが、Container InsightsではKubernetes向けに提供されているFluentdコンテナイメージを用います。

導入は以下のページの内容に沿って進めます。

CloudWatch Logs へログを送信する DaemonSet として FluentD をセットアップする - Amazon CloudWatch

(1) 名前空間を作成する

FluentdはCloudWatchエージェント用のNamespace「amazon-cloudwatch」に配置する必要がありますが、ステップ1の手順で既に作成しているため、改めて作成する必要はありません。

(2) ConfigMapを作成する

Fluentdに対して設定を与えるConfigMapを作成します。

マニフェストのテンプレートファイルを以下のように作成します。

cluster-info.yaml

# create configmap for cluster name and aws region for CloudWatch Logs
# need to replace the placeholders {{cluster_name}} and {{region_name}}
apiVersion: v1
data:
  cluster.name: {{cluster_name}}
  logs.region: {{region_name}}
kind: ConfigMap
metadata:
  name: cluster-info
  namespace: amazon-cloudwatch

5行目の cluster.name: {{cluster_name}} と、6行目の logs.region: {{region_name}} の部分を修正します。
今回の例であれば以下のようにします。

`cluster.name: eks-example`
`logs.region: ap-northeast-1`

kubernetes apply コマンドを使ってConfigMapリソースを作成します。

$ kubectl apply -f fluentd-configmap.yaml
configmap/cluster-info created

(3) Fluentdをインストールする

ここからは、Fluentdを構成する各種リソースを一気に作成します。

マニフェストが用意されていますので、ダウンロードします。

curl -O https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/k8s-deployment-manifest-templates/deployment-mode/daemonset/container-insights-monitoring/fluentd/fluentd.yaml
マニフェストファイルの内容 (クリックすると展開します)

cloudwatch-fluentd.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: fluentd
  namespace: amazon-cloudwatch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: fluentd-role
rules:
  - apiGroups: [""]
    resources:
      - namespaces
      - pods
      - pods/logs
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: fluentd-role-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: fluentd-role
subjects:
  - kind: ServiceAccount
    name: fluentd
    namespace: amazon-cloudwatch
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentd-config
  namespace: amazon-cloudwatch
  labels:
    k8s-app: fluentd-cloudwatch
data:
  fluent.conf: |
    @include containers.conf
    @include systemd.conf
    @include host.conf

    <match fluent.**>
      @type null
    </match>
  containers.conf: |
    <source>
      @type tail
      @id in_tail_container_logs
      @label @containers
      path /var/log/containers/*.log
      exclude_path ["/var/log/containers/cloudwatch-agent*", "/var/log/containers/fluentd*"]
      pos_file /var/log/fluentd-containers.log.pos
      tag *
      read_from_head true
      <parse>
        @type json
        time_format %Y-%m-%dT%H:%M:%S.%NZ
      </parse>
    </source>

    <source>
      @type tail
      @id in_tail_cwagent_logs
      @label @cwagentlogs
      path /var/log/containers/cloudwatch-agent*
      pos_file /var/log/cloudwatch-agent.log.pos
      tag *
      read_from_head true
      <parse>
        @type json
        time_format %Y-%m-%dT%H:%M:%S.%NZ
      </parse>
    </source>

    <source>
      @type tail
      @id in_tail_fluentd_logs
      @label @fluentdlogs
      path /var/log/containers/fluentd*
      pos_file /var/log/fluentd.log.pos
      tag *
      read_from_head true
      <parse>
        @type json
        time_format %Y-%m-%dT%H:%M:%S.%NZ
      </parse>
    </source>

    <label @fluentdlogs>
      <filter **>
        @type kubernetes_metadata
        @id filter_kube_metadata_fluentd
      </filter>

      <filter **>
        @type record_transformer
        @id filter_fluentd_stream_transformer
        <record>
          stream_name ${tag_parts[3]}
        </record>
      </filter>

      <match **>
        @type relabel
        @label @NORMAL
      </match>
    </label>

    <label @containers>
      <filter **>
        @type kubernetes_metadata
        @id filter_kube_metadata
      </filter>

      <filter **>
        @type record_transformer
        @id filter_containers_stream_transformer
        <record>
          stream_name ${tag_parts[3]}
        </record>
      </filter>

      <filter **>
        @type concat
        key log
        multiline_start_regexp /^\S/
        separator ""
        flush_interval 5
        timeout_label @NORMAL
      </filter>

      <match **>
        @type relabel
        @label @NORMAL
      </match>
    </label>

    <label @cwagentlogs>
      <filter **>
        @type kubernetes_metadata
        @id filter_kube_metadata_cwagent
      </filter>

      <filter **>
        @type record_transformer
        @id filter_cwagent_stream_transformer
        <record>
          stream_name ${tag_parts[3]}
        </record>
      </filter>

      <filter **>
        @type concat
        key log
        multiline_start_regexp /^\d{4}[-/]\d{1,2}[-/]\d{1,2}/
        separator ""
        flush_interval 5
        timeout_label @NORMAL
      </filter>

      <match **>
        @type relabel
        @label @NORMAL
      </match>
    </label>

    <label @NORMAL>
      <match **>
        @type cloudwatch_logs
        @id out_cloudwatch_logs_containers
        region "#{ENV.fetch('REGION')}"
        log_group_name "/aws/containerinsights/#{ENV.fetch('CLUSTER_NAME')}/application"
        log_stream_name_key stream_name
        remove_log_stream_name_key true
        auto_create_stream true
        <buffer>
          flush_interval 5
          chunk_limit_size 2m
          queued_chunks_limit_size 32
          retry_forever true
        </buffer>
      </match>
    </label>
  systemd.conf: |
    <source>
      @type systemd
      @id in_systemd_kubelet
      @label @systemd
      filters [{ "_SYSTEMD_UNIT": "kubelet.service" }]
      <entry>
        field_map {"MESSAGE": "message", "_HOSTNAME": "hostname", "_SYSTEMD_UNIT": "systemd_unit"}
        field_map_strict true
      </entry>
      path /var/log/journal
      <storage>
        @type local
        persistent true
        path /var/log/fluentd-journald-kubelet-pos.json
      </storage>
      read_from_head true
      tag kubelet.service
    </source>

    <source>
      @type systemd
      @id in_systemd_kubeproxy
      @label @systemd
      filters [{ "_SYSTEMD_UNIT": "kubeproxy.service" }]
      <entry>
        field_map {"MESSAGE": "message", "_HOSTNAME": "hostname", "_SYSTEMD_UNIT": "systemd_unit"}
        field_map_strict true
      </entry>
      path /var/log/journal
      <storage>
        @type local
        persistent true
        path /var/log/fluentd-journald-kubeproxy-pos.json
      </storage>
      read_from_head true
      tag kubeproxy.service
    </source>

    <source>
      @type systemd
      @id in_systemd_docker
      @label @systemd
      filters [{ "_SYSTEMD_UNIT": "docker.service" }]
      <entry>
        field_map {"MESSAGE": "message", "_HOSTNAME": "hostname", "_SYSTEMD_UNIT": "systemd_unit"}
        field_map_strict true
      </entry>
      path /var/log/journal
      <storage>
        @type local
        persistent true
        path /var/log/fluentd-journald-docker-pos.json
      </storage>
      read_from_head true
      tag docker.service
    </source>

    <label @systemd>
      <filter **>
        @type kubernetes_metadata
        @id filter_kube_metadata_systemd
      </filter>

      <filter **>
        @type record_transformer
        @id filter_systemd_stream_transformer
        <record>
          stream_name ${tag}-${record["hostname"]}
        </record>
      </filter>

      <match **>
        @type cloudwatch_logs
        @id out_cloudwatch_logs_systemd
        region "#{ENV.fetch('REGION')}"
        log_group_name "/aws/containerinsights/#{ENV.fetch('CLUSTER_NAME')}/dataplane"
        log_stream_name_key stream_name
        auto_create_stream true
        remove_log_stream_name_key true
        <buffer>
          flush_interval 5
          chunk_limit_size 2m
          queued_chunks_limit_size 32
          retry_forever true
        </buffer>
      </match>
    </label>
  host.conf: |
    <source>
      @type tail
      @id in_tail_dmesg
      @label @hostlogs
      path /var/log/dmesg
      pos_file /var/log/dmesg.log.pos
      tag host.dmesg
      read_from_head true
      <parse>
        @type syslog
      </parse>
    </source>

    <source>
      @type tail
      @id in_tail_secure
      @label @hostlogs
      path /var/log/secure
      pos_file /var/log/secure.log.pos
      tag host.secure
      read_from_head true
      <parse>
        @type syslog
      </parse>
    </source>

    <source>
      @type tail
      @id in_tail_messages
      @label @hostlogs
      path /var/log/messages
      pos_file /var/log/messages.log.pos
      tag host.messages
      read_from_head true
      <parse>
        @type syslog
      </parse>
    </source>

    <label @hostlogs>
      <filter **>
        @type kubernetes_metadata
        @id filter_kube_metadata_host
      </filter>

      <filter **>
        @type record_transformer
        @id filter_containers_stream_transformer_host
        <record>
          stream_name ${tag}-${record["host"]}
        </record>
      </filter>

      <match host.**>
        @type cloudwatch_logs
        @id out_cloudwatch_logs_host_logs
        region "#{ENV.fetch('REGION')}"
        log_group_name "/aws/containerinsights/#{ENV.fetch('CLUSTER_NAME')}/host"
        log_stream_name_key stream_name
        remove_log_stream_name_key true
        auto_create_stream true
        <buffer>
          flush_interval 5
          chunk_limit_size 2m
          queued_chunks_limit_size 32
          retry_forever true
        </buffer>
      </match>
    </label>
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd-cloudwatch
  namespace: amazon-cloudwatch
spec:
  selector:
    matchLabels:
      k8s-app: fluentd-cloudwatch
  template:
    metadata:
      labels:
        k8s-app: fluentd-cloudwatch
      annotations:
        configHash: 8915de4cf9c3551a8dc74c0137a3e83569d28c71044b0359c2578d2e0461825
    spec:
      serviceAccountName: fluentd
      terminationGracePeriodSeconds: 30
      # Because the image's entrypoint requires to write on /fluentd/etc but we mount configmap there which is read-only,
      # this initContainers workaround or other is needed.
      # See https://github.com/fluent/fluentd-kubernetes-daemonset/issues/90
      initContainers:
        - name: copy-fluentd-config
          image: busybox
          command: ['sh', '-c', 'cp /config-volume/..data/* /fluentd/etc']
          volumeMounts:
            - name: config-volume
              mountPath: /config-volume
            - name: fluentdconf
              mountPath: /fluentd/etc
        - name: update-log-driver
          image: busybox
          command: ['sh','-c','']
      containers:
        - name: fluentd-cloudwatch
          image: fluent/fluentd-kubernetes-daemonset:v1.7.3-debian-cloudwatch-1.0
          env:
            - name: REGION
              valueFrom:
                configMapKeyRef:
                  name: cluster-info
                  key: logs.region
            - name: CLUSTER_NAME
              valueFrom:
                configMapKeyRef:
                  name: cluster-info
                  key: cluster.name
            - name: CI_VERSION
              value: "k8s/1.1.0"
          resources:
            limits:
              memory: 400Mi
            requests:
              cpu: 100m
              memory: 200Mi
          volumeMounts:
            - name: config-volume
              mountPath: /config-volume
            - name: fluentdconf
              mountPath: /fluentd/etc
            - name: varlog
              mountPath: /var/log
            - name: varlibdockercontainers
              mountPath: /var/lib/docker/containers
              readOnly: true
            - name: runlogjournal
              mountPath: /run/log/journal
              readOnly: true
            - name: dmesg
              mountPath: /var/log/dmesg
              readOnly: true
      volumes:
        - name: config-volume
          configMap:
            name: fluentd-config
        - name: fluentdconf
          emptyDir: {}
        - name: varlog
          hostPath:
            path: /var/log
        - name: varlibdockercontainers
          hostPath:
            path: /var/lib/docker/containers
        - name: runlogjournal
          hostPath:
            path: /run/log/journal
        - name: dmesg
          hostPath:
            path: /var/log/dmesg

このマニフェストには以下のリソースが含まれています。

  • ServiceAccount「fluentd」
  • ClusterRole「fluentd-role」
  • ClusterRoleBinding「fluentd-role-binding」
  • ConfigMap「fluentd-config」(*)
  • DaemonSet「fluentd-cloudwatch」

(*) このConfigMapは、前の手順で作成したConfigMapとは別物です。Fluentdの動作をカスタマイズする様々な設定が含まれますが、今回はデフォルト設定のまま進めます。

これらのリソースの役割は、CloudWatchエージェントの時とほぼ同様です。
ServiceAccount、ClusterRole、ClusterRoleBindingによってFluentdの動作に必要な権限を設定して、DaemonSetによってFluentdを常駐型コンテナとして実行します。

kubernetes apply コマンドを使って各リソースを作成します。

$ kubectl apply -f fluentd.yaml
serviceaccount/fluentd created
clusterrole.rbac.authorization.k8s.io/fluentd-role created
clusterrolebinding.rbac.authorization.k8s.io/fluentd-role-binding created
configmap/fluentd-config created
daemonset.apps/fluentd-cloudwatch created

正常に作成されたことを確認するために、DaemonSetとPodの一覧を表示します。

$ kubectl get daemonsets -n amazon-cloudwatch
NAME                 DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
cloudwatch-agent     2         2         2       2            2           <none>          71m
fluentd-cloudwatch   2         2         2       2            2           <none>          85s

$ kubectl get pods -n amazon-cloudwatch -o wide
NAME                       READY   STATUS    RESTARTS   AGE    IP               NODE                                               NOMINATED NODE   READINESS GATES
cloudwatch-agent-4427l     1/1     Running   0          71m    192.168.39.252   ip-192-168-36-52.ap-northeast-1.compute.internal   <none>           <none>
cloudwatch-agent-7p2b2     1/1     Running   0          71m    192.168.25.224   ip-192-168-28-45.ap-northeast-1.compute.internal   <none>           <none>
fluentd-cloudwatch-g9jm8   1/1     Running   0          105s   192.168.52.175   ip-192-168-36-52.ap-northeast-1.compute.internal   <none>           <none>
fluentd-cloudwatch-gdv4b   1/1     Running   0          105s   192.168.8.71     ip-192-168-28-45.ap-northeast-1.compute.internal   <none>           <none>

ステップ1で作成したCloudWatchエージェントのDaemonSetやPodに加えて、FluentdのDaemonSetとPodが作成されていることが確認できます。

(4) CloudWatchへログが出力されることを確認する

Fluentdをデプロイしてしばらく経つと、CloudWatchエージェントからCloudWatchへメトリクスが出力されるようになります。

マネジメントコンソールでCloudWatchのログを開いて、以下の3つのロググループが作成されていることを確認します。

  • /aws/containerinsights/(クラスター名)/application
  • /aws/containerinsights/(クラスター名)/host
  • /aws/containerinsights/(クラスター名)/dataplane

これらのログが出力されていれば、Fluentdは正常に動作しています。

Container Insightsのダッシュボードを表示する

CloudWatchのトップ画面を表示します。
「概要」プルダウンをクリックして「Container Insights」を選択します。

Container Insightsのダッシュボードが表示され、EKSクラスターのメトリクスデータがグラフ表示されていることを確認します。

おわりに

今回ご紹介した手順を一通り試してみることにより、Container InsightsがどのようなKubernetesのリソースによって動作しているのかを理解することができるのではないかと思います。

Container Insightsを素早く導入するには「クイックスタート」の手順に沿って行うのが良いでしょう。
クイックスタートのマニフェストを使う際に、今回の内容を参考にしてマニフェストをカスタマイズすることも可能かと思います。