Amazon EKSのコンテナログをCloudWatch Logsに集約する

Amazon EKSにデプロイしたコンテナのログってどうやって管理するんだろ?

本日はそんな課題に対する1つの解決策であるFluentdを利用しコンテナログをCloudWatch Logsに集約する方法を紹介したいと思います。

今回構築する環境



結論から言うと以下を実行すればコンテナログをCloudWatch Logsに集約することができます。

  • WorkerノードにCloudWatch Logsへのアクセス権限を付与する
  • DaemonSetでFluentdコンテナをデプロイする

ただ、それだけ実行してもログ集約を確認することができないので、今回はEKSクラスタで以下のリソースを作成しました。

  • ReplicaSetでNginxコンテナを含むPodを6つ作成
  • LoadBalancerServiceでCLBを作成
  • DaemonSetでFluentdのPodを作成(ここがメイン)

構成図を見ればパッとわかると思いますが、各Pod(Nginxコンテナ)で出力されるログをFluentdコンテナを利用してCloudWatch Logsに飛ばします。もう少し具体的にいうとこのようなフローになります。

  1. Nginxコンテナの標準出力および標準エラー出力が、ノードの/var/log/containersに出力される
  2. Fluentdコンテナが/var/log/containersをtailしログをCloudWatch Logsに送信する

Nginxコンテナのログは明示的に指定しなくてもノードの/var/log/containersに出力さます。そして、Fluentdコンテナはそのファイルをtailする。。つまり、各コンテナではfluentd logging driverの設定をする必要はありません。素敵デス。

それでは、早速この環境を構築しコンテナログがCloudWatch Logsに集約されることを確認してみましょう!

EKS環境がない方はaws-workshop-for-kubernetes あたりを参考にKubernetesクラスタを構築してみてください。

デプロイ前の環境

今回は2つのサブネットに3つのWorkerノードを配置し、それらをクラスタ化した状態からスタートします。 kubectlでの実行結果は以下のようになっています。

$ kubectl get nodes
NAME                              STATUS    ROLES     AGE       VERSION
ip-192-168-124-66.ec2.internal    Ready     <none>    23s       v1.10.3
ip-192-168-141-159.ec2.internal   Ready     <none>    24s       v1.10.3
ip-192-168-149-122.ec2.internal   Ready     <none>    23s       v1.10.3

$ kubectl get all
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
service/kubernetes   ClusterIP   10.100.0.1   <none>        443/TCP   31m

EKS環境にNginxコンテナをデプロイする

まずは、Nginxコンテナを含むPodをデプロイするためのマニュフェストを作成します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment
spec:
  replicas: 6
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx-container
          image: nginx:1.12
          ports:
            - containerPort: 80

kubectl applyコマンドで、Podをデプロイします。

$ kubectl apply -f deployment.yaml
deployment.apps "deployment" created
$ kubectl get pods -o wide
NAME                          READY     STATUS    RESTARTS   AGE       IP                NODE
deployment-5bb5496c6d-2cxwm   1/1       Running   0          15s       192.168.149.0     ip-192-168-149-122.ec2.internal
deployment-5bb5496c6d-5dsf8   1/1       Running   0          15s       192.168.119.199   ip-192-168-124-66.ec2.internal
deployment-5bb5496c6d-8fk2t   1/1       Running   0          15s       192.168.153.84    ip-192-168-141-159.ec2.internal
deployment-5bb5496c6d-q2v4j   1/1       Running   0          15s       192.168.102.68    ip-192-168-124-66.ec2.internal
deployment-5bb5496c6d-sxnmw   1/1       Running   0          15s       192.168.155.113   ip-192-168-149-122.ec2.internal
deployment-5bb5496c6d-t82nk   1/1       Running   0          15s       192.168.173.185   ip-192-168-141-159.ec2.internal

Podが各ノードに2つずつ配置されました。このようなイメージになります。

EKS環境にロードバランサをデプロイする

CLBをデプロイするためのマニュフェストを作成します。

apiVersion: v1
kind: Service
metadata:
  name: lb
spec:
  type: LoadBalancer
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
      nodePort: 30080
  selector:
    app: nginx

kubectl applyコマンドで、LoadBalancerをデプロイします。

$ kubectl apply -f lb.yaml 
service "lb" created

$ kubectl get svc
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP        PORT(S)          AGE
kubernetes   ClusterIP      10.100.0.1      <none>             443/TCP          47m
lb           LoadBalancer   10.100.14.156   a0bf4e0dce68b...   8080:30080/TCP   1m

AWSリソースとしてもCLBが作成されたことを確認することができます。

$ aws elb describe-load-balancers
{
    "LoadBalancerDescriptions": [
        {
            "Subnets": [
                "subnet-016a4e2a0846b95ee", 
                "subnet-01817674493758c04", 
                "subnet-0b1750d9b55316554"
            ], 
            "CanonicalHostedZoneNameID": "Z35SXDOTRQ7X7K", 
            "CanonicalHostedZoneName": "a0bf4e0dce68b11e88c4b0a140bdc4fe-1579511116.us-east-1.elb.amazonaws.com", 
            "ListenerDescriptions": [
                {
                    "Listener": {
                        "InstancePort": 30080, 
                        "LoadBalancerPort": 8080, 
                        "Protocol": "TCP", 
                        "InstanceProtocol": "TCP"
                    }, 
                    "PolicyNames": []
                }
            ], 
            "HealthCheck": {
                "HealthyThreshold": 2, 
                "Interval": 10, 
                "Target": "TCP:30080", 
                "Timeout": 5, 
                "UnhealthyThreshold": 6
            }, 
            "VPCId": "vpc-0f5f39afb5fc16781", 
            "BackendServerDescriptions": [], 
            "Instances": [
                {
                    "InstanceId": "i-0445f1d21cf0e3c68"
                }, 
                {
                    "InstanceId": "i-0a1ac41a07d210f9f"
                }, 
                {
                    "InstanceId": "i-0f9e09f0290c2326c"
                }
            ], 
            "DNSName": "a0bf4e0dce68b11e88c4b0a140bdc4fe-1579511116.us-east-1.elb.amazonaws.com", 
            "SecurityGroups": [
                "sg-0f99a368d876b43d3"
            ], 
            "Policies": {
                "LBCookieStickinessPolicies": [], 
                "AppCookieStickinessPolicies": [], 
                "OtherPolicies": []
            }, 
            "LoadBalancerName": "a0bf4e0dce68b11e88c4b0a140bdc4fe", 
            "CreatedTime": "2018-11-12T14:55:52.520Z", 
            "AvailabilityZones": [
                "us-east-1a", 
                "us-east-1b", 
                "us-east-1c"
            ], 
            "Scheme": "internet-facing", 
            "SourceSecurityGroup": {
                "OwnerAlias": "XXXXXXXXXXXX", 
                "GroupName": "k8s-elb-a0bf4e0dce68b11e88c4b0a140bdc4fe"
            }
        }
    ]
}

CLB配置後のイメージです。

CLBにアクセスする

CLBで外部からリクエストを受けれる状態になったので、クライアント端末からリクエストを送信してみましょう!

$ curl XXXXXXXXXXXXXXXXXXXXXXX.us-east-1.elb.amazonaws.com:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

レスポンスが正常に返ってきました。また、接続されたコンテナにてアクセスログを確認することができます。

$ kubectl logs deployment-5bb5496c6d-t82nk
192.168.124.66 - - [12/Nov/2018:15:14:32 +0000] "GET / HTTP/1.1" 200 640 "-" "curl/7.53.1" "-"

IAMロールに権限を追加する

前置きが長くなりましたがここからが本題です。

各ノードからCloudWatch Logsにアクセスする必要があるため、ノードのIAMロールにCloudWatchLogsFullAccess権限を付与します。

EKS環境にFluentdコンテナをデプロイする

Fluentdコンテナを各ノードにデプロイします。公式リポジトリからマニュフェストファイルをクローンしましょう。

git clone https://github.com/fluent/fluentd-kubernetes-daemonset

READMEにも記載がありますが、権限エラーを回避するためFLUENT_UID環境変数に0を設定します。

Run as root

In Kubernetes and default setting, fluentd needs root permission to read logs in /var/log and write pos_file to /var/log. To avoid permission error, you need to set FLUENT_UID >environment variable to 0 in your Kubernetes configuration.

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: fluentd
  namespace: kube-system

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: fluentd
  namespace: kube-system
rules:
- apiGroups:
  - ""
  resources:
  - pods
  - namespaces
  verbs:
  - get
  - list
  - watch

---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: fluentd
roleRef:
  kind: ClusterRole
  name: fluentd
  apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
  name: fluentd
  namespace: kube-system
---
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  name: fluentd
  namespace: kube-system
  labels:
    k8s-app: fluentd-logging
    version: v1
    kubernetes.io/cluster-service: "true"
spec:
  template:
    metadata:
      annotations:
        iam.amazonaws.com/role: us-east-1a.staging.kubernetes.ruist.io-service-role
      labels:
        k8s-app: fluentd-logging
        version: v1
        kubernetes.io/cluster-service: "true"
    spec:
      serviceAccount: fluentd
      serviceAccountName: fluentd
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      containers:
      - name: fluentd
        image: fluent/fluentd-kubernetes-daemonset:cloudwatch
        env:
          - name: LOG_GROUP_NAME
            value: "k8s"
          - name: AWS_REGION
            value: "us-east-1"
          - name: FLUENT_UID
            value: "0"
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
      terminationGracePeriodSeconds: 30
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers

kubectl applyコマンドで、DaemonSetをデプロイします。

$ kubectl apply -f fluentd-daemonset-cloudwatch-rbac.yaml
serviceaccount "fluentd" created
clusterrole.rbac.authorization.k8s.io "fluentd" created
clusterrolebinding.rbac.authorization.k8s.io "fluentd" created
daemonset.extensions "fluentd" created

$ kubectl get pods -n kube-system                                                                                                                         
NAME                        READY     STATUS    RESTARTS   AGE
aws-node-9pzlb              1/1       Running   0          1h
aws-node-c88hc              1/1       Running   0          1h
aws-node-nkh94              1/1       Running   0          1h
fluentd-nqd6m               1/1       Running   0          36s
fluentd-w4fx9               1/1       Running   0          36s
fluentd-x57fs               1/1       Running   0          36s
kube-dns-64b69465b4-2x7bm   3/3       Running   0          1h
kube-proxy-2l9dt            1/1       Running   0          1h
kube-proxy-45xcr            1/1       Running   0          1h
kube-proxy-5jljs            1/1       Running   0          1h

$ kubectl get daemonset -n kube-system                                                                                                                         
NAME         DESIRED   CURRENT   READY     UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
aws-node     3         3         3         3            3           <none>          1h
fluentd      3         3         3         3            3           <none>          53s
kube-proxy   3         3         3         3            3           <none>          1h

Fluentdコンテナが各ノードに1つずつ配置されました。このようなイメージになります。



ログ集約を確認する

CloudWatch Logsにログが集約されていることを確認します。何度かロードバランサにアクセスしてみましょう!

$ curl XXXXXXXXXXXXXXXXXXXXXXX.us-east-1.elb.amazonaws.com:8080

しばらく待つと、コンテナ毎にログストリームが作成され。。

各コンテナのログを確認することができました!

最後に

FluentdコンテナをDaemonSetとして配置することで、簡単にコンテナログをCloudWatch Logsに集約することができました!メインのコンテナには手を加えずコンテナログをCloudWatch Logsに集約できるのはとても便利ですね。

あー、EKSが東京リージョンにくるのが待ち遠しい。