IBM Cloud Privateが集めたコンテナログをLogstashでファイルに保存する

ICPはコンテナのログを自動的に集めて自動的にElasticsearchに保存します。デフォルトではElasticsearchの保管日数は1日です。エンタープライズ環境では、ログを一定期間保管する要件があったり、既存のログ監視の仕組みがあったりするので、ELKスタックで監視というより既存の仕組みで監視したい場合も多そうです。

最初から`/var/log/containers/*.log`を直接保存したり、監視することも考えられると思います。

あるいはLogstashからsyslogに送ることも考えられます。その場合の方法は、@teruq さんの以下の記事が参考になります。

（参考）
[IBM Cloud Private(ICP)でコンテナログをsyslogに転送する - logstash差替編](https://qiita.com/teruq/items/ff7a55eed3929d5e6a30)

ここでは、Logstashでファイルに保存する方法を試してみます。ICPのバージョンは2.1.0.3で確認しています。

## Logstashのファイルプラグイン

LogstashのOutputプラグインでFileプラグインがあり、このプラグインはICP標準のLogstashに入っています。

[Logstash Reference » Output plugins » File output plugin](https://www.elastic.co/guide/en/logstash/current/plugins-outputs-file.html)

このプラグインを使いますが、どのようなパスに、どのようなフォーマットでログを出力するかは設計・検討する必要があります。

## ICPのロギング動作

ICPのデフォルトの仕組みでは、コンテナが`Hello World!`と標準出力に出すと、Dockerのlogging driverによって`/var/log/containers/hogehoge.log`に以下のような形式でログが追記されます。

```json
{"log":"Hello World!","stream":"stdout","time":"2018-07-23T07:16:54.353021511Z"}
```

これをDaemonSetとして動作しているFilebeatが監視してLogstashに転送し、Logstashのフィルターを通ると、FilebeatやLogstashがフィールドを付加したり削除したりしてElasticsearchに送ります。そのあたりの動作はこちらに書きました。

[IBM Cloud Privateのログ収集の仕組みの調査（Logstashまで）](https://qiita.com/sotoiwa/items/116f0d1cc46432c18d34)

その結果Logstashを出るときには以下の様なデータになります。

```json
{
    "offset": 6275,
    "pod": {
        "ip": "10.1.170.243"
    },
    "log": "Hello World!",
    "kubernetes.pod": "hello-5564fcdfc6-v2n72",
    "kubernetes.container_name": "hello",
    "input_type": "log",
    "source": "/var/log/containers/hello-5564fcdfc6-v2n72_ns3_hello-32ee39e2d8d4dec3e65964b9f579456269bf97e8a4d742ccd65fb906e33363c6.log",
    "type": "kube-logs",
    "tags": [
        "k8s-app",
        "beats_input_raw_event"
    ],
    "kubernetes.container_id": "32ee39e2d8d4dec3e65964b9f579456269bf97e8a4d742ccd65fb906e33363c6",
    "node": {
        "hostname": "172.30.1.224"
    },
    "@timestamp": "2018-07-23T07:16:54.353Z",
    "stream": "stdout",
    "kubernetes.namespace": "ns3",
    "beat": {
        "hostname": "logging-elk-filebeat-ds-ln2gp",
        "name": "logging-elk-filebeat-ds-ln2gp",
        "version": "5.5.1"
    },
    "@version": "1",
    "time": "2018-07-23T07:16:54.353021511Z"
}
```

この中で重要そうなフィールドは以下です。

|フィールド|説明|
|---|---|
|log|ログの本文|
|[pod][ip]|FilebeatのPodのIPアドレス|
|time|logging driverでのロギング時刻|
|[node][hostname]|FilebeatのPodが稼働していたノードのホスト名|
|kubernetes.namespace|Namespace名|
|kubernetes.pod|Pod名|
|kubernetes.container_name|コンテナ名|
|kubernetes.container_id|コンテナID|

なお、ログがjsonの場合はパースされてさらにフィールドが増えますが、もとのjson形式のログ本文は`log`に入ったままです。

このデータを、例えば`/log/%{kubernetes.namespace}.log`というファイルに、

```
%{time}% {kubernetes.pod} {%log}
```

という形式でを出力するとしたら、

Logstashのoutput設定は、

```ruby

output {
    file {
      path => "/log/%{kubernetes.namespace}.log"
      codec => line { format => "%{time}% {kubernetes.pod} {%log}" }
    }
}
```

になります。フォーマット等は検討が必要です。

期待値としては次の様なログになります。

```
2018-05-31T05:49:56.659607745Z hello-758d49f796-g9nm5 Hello World!
```

## ICPの設定変更

ICPの設定を変更してコンテナログをファイルに保存するようにします。

### PV/PVCの作成

Logstashがファイルを保存するPVを作成します。

ここではhostPathで作成することにしてディレクトリーを用意します。logstashコンテナ内では1000番のUIDでプロセスが稼働するので、1000番のユーザーがディレクトリーに書き込める必要があります。ここでは777にしてしまいます。

```
root@myicp01:/export# pwd
/export
root@myicp01:/export# mkdir logstash
root@myicp01:/export# chmod 777 logstash
root@myicp01:/export# ls -l
total 12
drwxrwxrwx  2 root root 4096 Jul  4 10:46 logstash
drwxr-xr-x 15 root root 4096 Jul  4 09:59 MC_jenkins02
drwxr-xr-x  3 root root 4096 Jul  4 07:15 MC_microclimate02
root@myicp01:/export#
```

PV/PVCを作成します。

```yaml:logstash-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: logstash-pv
spec:
  accessModes:
  - ReadWriteOnce
  capacity:
    storage: 4Gi
  hostPath:
    path: /export/logstash
    type: ""
  storageClassName: logstash
  persistentVolumeReclaimPolicy: Retain
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: logstash-pvc
  namespace: kube-system
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 4Gi
  storageClassName: logstash
```

```
kubectl apply -f logstash-pv.yaml
```

### PVCのマウント

作成したPVCをマウントするようにLogstashのDeploymentの設定を書き換えます。

```
kubectl edit deploy -n kube-system logging-elk-logstash
```

```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "1"
  creationTimestamp: null
  generation: 1
  labels:
    app: logging-elk-elasticsearch
    chart: ibm-icplogging-1.0.0
    component: logstash
    heritage: Tiller
    release: logging
    role: logstash
  name: logging-elk-logstash
  selfLink: /apis/extensions/v1beta1/namespaces/kube-system/deployments/logging-elk-logstash
spec:
  minReadySeconds: 5
  progressDeadlineSeconds: 600
  replicas: 1
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: logging-elk-elasticsearch
      component: logstash
      heritage: Tiller
      release: logging
      role: logstash
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      annotations:
        productID: none
        productName: Logstash
        productVersion: 5.5.1
      creationTimestamp: null
      labels:
        app: logging-elk-elasticsearch
        chart: ibm-icplogging
        component: logstash
        heritage: Tiller
        release: logging
        role: logstash
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: management
                operator: In
                values:
                - "true"
      containers:
      - command:
        - /bin/bash
        - /scripts/entrypoint.sh
        env:
        - name: LS_JAVA_OPTS
          value: -Xmx512m -Xms512m
        - name: CFG_BASEDIR
          value: /usr/share/logstash
        image: ibmcom/logstash:5.5.1
        imagePullPolicy: IfNotPresent
        name: logstash
        ports:
        - containerPort: 5044
          protocol: TCP
        resources:
          limits:
            memory: 1Gi
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
        volumeMounts:
        - mountPath: /usr/share/logstash/pipeline
          name: pipeline-config
        - mountPath: /usr/share/logstash-config-x86
          name: logstash-config-x86
        - mountPath: /usr/share/logstash-config-ppc
          name: logstash-config-ppc
        - mountPath: /usr/share/logstash/data
          name: data
        - mountPath: /scripts
          name: entrypoint
        - mountPath: /log # 追加
          name: log       # 追加
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30
      tolerations:
      - effect: NoSchedule
        key: dedicated
        operator: Exists
      volumes:
      - configMap:
          defaultMode: 420
          items:
          - key: k8s.conf
            path: k8s.conf
          name: logging-elk-logstash-config
        name: pipeline-config
      - configMap:
          defaultMode: 420
          items:
          - key: logstash.yml-x86
            path: logstash.yml
          name: logging-elk-logstash-config
        name: logstash-config-x86
      - configMap:
          defaultMode: 420
          items:
          - key: logstash.yml-ppc
            path: logstash.yml
          name: logging-elk-logstash-config
        name: logstash-config-ppc
      - configMap:
          defaultMode: 365
          items:
          - key: logstash-entrypoint.sh
            path: entrypoint.sh
          - key: map-config.sh
            path: map-config.sh
          name: logging-elk-elasticsearch-entrypoint
        name: entrypoint
      - emptyDir: {}
        name: data
      - persistentVolumeClaim:    # 追加
          claimName: logstash-pvc # 追加
        name: log                 # 追加 
status: {}
```

### Logstash設定の変更

Logstashの設定はConfigMapにあるのでこれを書き換えます。

```
kubectl edit cm -n kube-system logging-elk-logstash-config
```

```yaml
apiVersion: v1
data:
  k8s.conf: |-
    input {
      beats {
        port => 5044
      }
    }

    filter {
      if [type] == "kube-logs" {
        mutate {
          rename => { "message" => "log" }
          remove_field => ["host"]
        }

        date {
          match => ["time", "ISO8601"]
        }

        # Sample:
        # tiller-1279707952-sgd08_kube-system_tiller-0c51282d195d1c21307f6a48b9118d690441cda498fc5a2790c043407eab235b.log
        # filebeat-j357d_default_filebeat-1a3113e374ad7937a3efa36c4bb42b46b976bcd7cd96223e6b9e6e3df08d802a.log
        # appstdoutpod_default_app-stdout-container-01c2e7a7b105d9141825ea3ae5634b580fdd20a5a4ee890cdbe0816ca002623c.log
        # unified-router-4047118581-sm913_kube-system_unified-router-ddda8a8cbcb74c45b64a4b18997b4f2928c998a37e45037cd0304eaa56d1634f.log
        dissect {
          mapping => {
            "source" => "/var/log/containers/%{kubernetes.pod}_%{kubernetes.namespace}_%{container_file_ext}"
          }
        }

        dissect {
          mapping => {
            "container_file_ext" => "%{container}.%{?file_ext}"
          }
          remove_field => ["host", "container_file_ext"]
        }

        grok {
          "match" => {
            "container" => "^%{DATA:kubernetes.container_name}-(?<kubernetes.container_id>[0-9A-Za-z]{64,64})"
          }
          remove_field => ["container"]
        }
      }
    }

    filter {
      # Drop empty lines
      if [log] =~ /^\s*$/ {
        drop { }
      }
      # Attempt to parse JSON, but ignore failures and pass entries on as-is
      json {
        source => "log"
        skip_on_invalid_json => true
      }
    }

    output {
      elasticsearch {
        hosts => "elasticsearch:9200"
        index => "logstash-%{+YYYY.MM.dd}"
        document_type => "%{[@metadata][type]}"
      }
      # 追加ここから
      file {
        path => "/log/%{kubernetes.namespace}.log"
        codec => line { format => "%{time} %{kubernetes.pod} %{log}" }
      }
      # 追加ここまで
    }
  logstash.yml-ppc: |-
    # ppc64le-only config. Must not include refs to X-Pack.
    config.reload.automatic: true
    http.host: "0.0.0.0"
    path.config: /usr/share/logstash/pipeline
    xpack.monitoring.enabled: false

    # end
  logstash.yml-x86: |-
    # x86-only config. Must include refs to X-Pack.
    config.reload.automatic: true
    http.host: "0.0.0.0"
    path.config: /usr/share/logstash/pipeline
    xpack.monitoring.enabled: false



    # end
kind: ConfigMap
metadata:
  creationTimestamp: null
  labels:
    app: logging-elk-elasticsearch
    chart: ibm-icplogging-1.0.0
    component: logstash
    heritage: Tiller
    release: logging
  name: logging-elk-logstash-config
  selfLink: /api/v1/namespaces/kube-system/configmaps/logging-elk-logstash-config
```

設定はしばらく待つと反映されます。

### 確認

Namespace別にログが出力されていることを確認します。

```shell-session
root@myicp01:/export/logstash# pwd
/export/logstash
root@myicp01:/export/logstash# ls -l
total 3880
-rw-r--r-- 1 ubuntu ubuntu 3950084 Jul  4 14:34 kube-system.log
-rw-r--r-- 1 ubuntu ubuntu   17719 Jul  4 14:33 ns1.log
root@myicp01:/export/logstash# tail -5 ns1.log
2018-07-04T14:33:43.508226495Z sample1-586dbc87fc-mx46s [AUDIT   ] CWPKI0803A: SSL certificate created in 1.913 seconds. SSL key file: /opt/ibm/wlp/output/defaultServer/resources/security/key.jks
2018-07-04T14:33:43.876014975Z sample1-586dbc87fc-mx46s [AUDIT   ] CWWKT0016I: Web application available (default_host): http://sample1-586dbc87fc-mx46s:9080/Sample1/
2018-07-04T14:33:43.879160256Z sample1-586dbc87fc-mx46s [AUDIT   ] CWWKZ0001I: Application Sample1 started in 0.314 seconds.
2018-07-04T14:33:43.916935058Z sample1-586dbc87fc-mx46s [AUDIT   ] CWWKF0012I: The server installed the following features: [jsp-2.3, ejbLite-3.2, managedBeans-1.0, servlet-3.1, jsf-2.2, beanValidation-1.1, ssl-1.0, jndi-1.0, appSecurity-2.0, jsonp-1.0, jdbc-4.1, jaxrs-2.0, el-3.0, jaxrsClient-2.0, json-1.0, jpaContainer-2.1, cdi-1.2, distributedMap-1.0, webProfile-7.0, websocket-1.1, jpa-2.1].
2018-07-04T14:33:43.927439222Z sample1-586dbc87fc-mx46s [AUDIT   ] CWWKF0011I: The server defaultServer is ready to run a smarter planet.
root@myicp01:/export/logstash#
```

## 補足

ここでは、デフォルトのicp-loggingを編集していますが、ICPをバージョンアップした場合に何が起こるかわからないので、おそらく、デフォルトをは別にicp-loggingのHelmチャートをリリースし、そちらをカスタマイズするほうがよいかもしれません。

ログ出力のフォーマットはもう少し真面目に検討した結果、以下がよいと思いました。

```ruby
      # 追加ここから
      file {
        path => "/log/%{kubernetes.namespace}/%{kubernetes.pod}-%{[node][hostname]}-%{+YYYYMMdd}.log"
        codec => line { format => "%{time} %{kubernetes.container_name} %{kubernetes.container_id} %{stream} %{log}" }
      }
      # 追加ここまで
```