Power Systems Virtual ServerへのOpenShift 4.7導入(6): Elasticsearch保存データのエクスポート

# はじめに
こちらの[記事](https://qiita.com/y-akio/items/7d241745125ab7145bda)でPower Systems Virtual Server(以下PowerVS)へのOpenShift 4.7の導入後作業としてinfraノードの構成を実施しました。infraノードのElasticsearchは短期的なログ保存を目的としています。長期的にログを保存するために外部ElasticsearchやFluentdをサポートする外部ログ集計ソリューションへログを転送することができますが、この記事ではテキストファイルへエクスポートします。

> OpenShift Logging Elasticsearch インスタンスは、短期 (約 7 日間) の保存について最適化され、テストされています。長期間ログを保持する必要がある場合は、データをサードパーティーのストレージシステムに移動することが推奨されます。

https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.7/html/logging/cluster-logging

> ログを他のログアグリゲーターに送信するには、OpenShift Container Platform ログ転送 API を使用します。この API を使用すると、コンテナー、インフラストラクチャーおよび監査ログをクラスター内外の特定のエンドポイントに送信できます。

https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.7/html/logging/cluster-logging-external

# 1. Elasticsearch保存データのエクスポート

## 1.1. Elasticsearch接続用ルート作成
bastionノードからElasticsearchのログストアに対して、curlで接続できるようにルートを作成します。

```bash:bastionノード
oc project openshift-logging
vi route.yaml
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: elasticsearch
  namespace: openshift-logging
spec:
  host:
  to:
    kind: Service
    name: elasticsearch
  tls:
    termination: reencrypt
    destinationCACertificate: |
### vi終了

oc extract secret/elasticsearch --to=. --keys=admin-ca
cat ./admin-ca | sed -e "s/^/      /" >> route.yaml
oc apply -f route.yaml
oc get route
### 標準出力↓
NAME            HOST/PORT                                          PATH   SERVICES        PORT    TERMINATION          WILDCARD
elasticsearch   elasticsearch-openshift-logging.apps.ocp.powervs          elasticsearch   <all>   reencrypt            None
kibana          kibana-openshift-logging.apps.ocp.powervs                 kibana          <all>   reencrypt/Redirect   None
```

https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.7/html/logging/cluster-logging-store#cluster-logging-elasticsearch-exposing_cluster-logging-store

## 1.2. インデックス一覧の取得
OpenShiftユーザーのトークンでElasticsearchに接続してインデックス一覧を取得します。
ここではユーザーとしてkubeadminを使用しています。

```bash:bastionノード
# 準備
oc login -u kubeadmin -p <kubeadminパスワード>
token=$(oc whoami -t)
domain=elasticsearch-openshift-logging.apps.ocp.powervs
header1="Authorization: Bearer ${token}"

# インデックス一覧取得
curl -sk -H "${header1}" https://${domain}/_cat/indices
### 標準出力↓
green open audit-000003                   ltFqYg6jT9qZ_wV6QRGIuA 3 1       0 0   1.5kb    783b
green open app-000077                     WGatHRvEQrSrEuUeNyEwyg 3 1       0 0   1.5kb    783b
green open app-000076                     yZ4NX2HVRHymJBSCWs4J7Q 3 1       0 0   1.5kb    783b
green open app-000084                     H-vMl6moTV6b70kF-79-Og 3 1       0 0   1.5kb    783b
green open infra-000007                   ZOD5e6BwQdSVJrGYxCSRFQ 3 1 3391227 0   3.8gb   1.9gb
green open infra-000013                   WUTnjBHMTSupphIesJf0JA 3 1 2620386 0   3.1gb   1.5gb
green open infra-000003                   NqJ0DCXMSMWD7IUrApM_Zg 3 1 3304724 0   3.9gb   1.9gb
green open app-000093                     olJZao2VS5OqmM3KpRINKQ 3 1       0 0   1.5kb    783b
green open infra-000014                   i9KXTBGOQRO2vYfulIXmcw 3 1 1227321 0   1.5gb 776.5mb
green open app-000085                     1m-sM80PQaa5ipZtvWY-PA 3 1       0 0   1.5kb    783b
green open infra-000005                   hjyO2OJBTcKwozQzF0279A 3 1 3396051 0   3.8gb   1.9gb
green open audit-000005                   _EmhHSEpQKSEL6tyVCKw1w 3 1       0 0   1.5kb    783b
green open .security                      pBEUFDweSnqDNL7icj7g2A 1 1       5 0  58.3kb  29.1kb
green open app-000095                     b6l0rD6hTnKGWgF7L1GB4A 3 1      32 0 576.8kb 266.9kb
･･･
```

## 1.3. JSON形式でのエクスポート

Elasticsearchでは一度に取得できる件数が「10,000」に制限されています。

■「10,000」件以下の場合のJSON形式でのエクスポート例

```bash:bastionノード
# 準備
oc login -u kubeadmin -p <kubeadminパスワード>
token=$(oc whoami -t)
domain=elasticsearch-openshift-logging.apps.ocp.powervs
header1="Authorization: Bearer ${token}"

# 件数確認(32件)
curl -sk -H "${header1}" https://${domain}/_cat/count/app-000095
### 標準出力↓
1621402132 14:28:52 32

# JSON形式でのエクスポート
curl -sk -H "${header1}" https://${domain}/app-000095/_search?size=32 > app-000095.json
```

■「10,000」件より多い場合のJSON形式でのエクスポート例

```bash:bastionノード
# 準備
oc login -u kubeadmin -p <kubeadminパスワード>
token=$(oc whoami -t)
domain=elasticsearch-openshift-logging.apps.ocp.powervs
header1="Authorization: Bearer ${token}"
header2="Content-Type: application/json"

# 最初の10,000件とscroll_idを取得
curl -sk -H "${header1}" -H "${header2}" -d @test.json \
  https://${domain}/infra-000007/_search?scroll=1m > infra-000007.json
scroll_id=`jq -r "._scroll_id" infra-000007.json | tr -d '\n'`

# 下記(1)～(3)を取得件数が『0』なるまで繰り返す
# (1) 次の10,000件を取得
curl -sk -H "${header1}" -H "${header2}" -d "{\"scroll\":\"1m\",\"scroll_id\":\"${scroll_id}\"}" \
  https://${domain}/_search/scroll > temp.json

# (2) 件数確認
jq -r ".hits.hits | length" temp.json

# (3) 0件でない場合に実行
cat temp.json >> infra-000007.json
```

```json:test.json
{
  "size": "10000",
  "_source": [
    "kubernetes.container_name",
    "kubernetes.namespace_name",
    "kubernetes.pod_name",
    "hostname",
    "message",
    "@timestamp"
  ]
}
```

■JSON出力例(1件)

```json
{
  "took" : 84,
  "timed_out" : false,
  "_shards" : {
    "total" : 3,
    "successful" : 3,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 32,
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "app-000095",
        "_type" : "_doc",
        "_id" : "YzBhMGZhZDctZWRiNS00YmM3LThlNDUtOTFkNzJjZDhiMTM4",
        "_score" : 1.0,
        "_source" : {
          "docker" : {
            "container_id" : "30d33fb8920008f1e48c09b7b0fd2c4c51bfcb30240dc8e93f429faba7882e53"
          },
          "kubernetes" : {
            "container_name" : "open-liberty",
            "namespace_name" : "open-liberty",
            "pod_name" : "open-liberty-0",
            "container_image" : "docker.io/library/open-liberty:21.0.0.4-full-java11-openj9",
            "container_image_id" : "docker.io/library/open-liberty@sha256:3b4da7bbf3a9298ffff3fbc9c311c65221192d10529c3a83312e9243c710e3c4",
            "pod_id" : "bfad4941-c019-44a0-98e3-a8343dac2c08",
            "host" : "worker-0",
            "master_url" : "https://kubernetes.default.svc",
            "namespace_id" : "fec969a8-4f62-4ef4-b82f-6c07919a3cf5",
            "flat_labels" : [
              "app=open-liberty",
              "controller-revision-hash=open-liberty-56dd67f46b",
              "statefulset_kubernetes_io/pod-name=open-liberty-0"
            ]
          },
          "message" : "[AUDIT   ] CWWKE0085I: The server defaultServer is stopping because the JVM is exiting.",
          "level" : "unknown",
          "hostname" : "worker-0",
          "pipeline_metadata" : {
            "collector" : {
              "ipaddr4" : "192.168.25.113",
              "inputname" : "fluent-plugin-systemd",
              "name" : "fluentd",
              "received_at" : "2021-05-20T14:56:40.664956+00:00",
              "version" : "1.7.4 1.6.0"
            }
          },
          "@timestamp" : "2021-05-20T14:56:39.647267+00:00",
          "viaq_msg_id" : "YzBhMGZhZDctZWRiNS00YmM3LThlNDUtOTFkNzJjZDhiMTM4"
        }
      }
    ]
  }
}
```

## 1.4. JSON形式からCSV形式への変換

前項でエクスポートした「app-000095.json」をCSV形式に変換します。ダブルクオーテーションを除去し、message内のコンマはシャープに置換しています。

```bash:bastionノード
cat app-000095.json | jq -r '.hits.hits[] |
[ ._source."@timestamp",
  ._source.hostname,
  ._source.kubernetes.namespace_name,
  ._source.kubernetes.container_name,
  ._source.kubernetes.pod_name,
  ._source.message] | @csv' | sed -e 's/\"//g' -e 's/,/#/6g' > app-000095.csv
```

```csv:app-000095.csv抜粋
2021-05-20T14:56:39.647267+00:00,worker-0,open-liberty,open-liberty,open-liberty-0,[AUDIT   ] CWWKE0085I: The server defaultServer is stopping because the JVM is exiting.
```

## 1.5. CSV形式ファイルの分割
「app-000095.csv」は複数のノードやネームスペースのログを集約したものなので分割します。

```bash:bastionノード
awk -F, '{out=$2"_"$3"_"$4".csv"; print $1","$5","$6 >> out}' app-000095.csv
ls -l
### 標準出力↓
-rw-r--r--. 1 root root  1400  5月 21 00:07 worker-0_open-liberty_open-liberty.csv
-rw-r--r--. 1 root root  1134  5月 21 00:07 worker-1_open-liberty_open-liberty.csv
```

```bash:worker-0_open-liberty_open-liberty.csv抜粋
2021-05-20T14:56:39.647267+00:00,open-liberty-0,[AUDIT   ] CWWKE0085I: The server defaultServer is stopping because the JVM is exiting.
```

## 1.6. シェルスクリプトによる一括処理
一連の処理をシェルスクリプトとして実行します。前項ではインデックスの全件をエクスポートしましたが、ここでは日付「2021-05-20」を範囲指定して件数を取得し、0件以上だった場合にCSVファイルに出力しています。

```bash:bastionノード
mkdir /tmp/output
./export.sh
### 標準出力↓
[2021/05/21 20:25:33] JSON取得: infra-000013, 2620386
[2021/05/21 20:36:57] JSON取得: infra-000014, 2605663
[2021/05/21 20:46:52] JSON取得: app-000095, 32
[2021/05/21 20:46:52] JSON取得: infra-000015, 1579811
[2021/05/21 20:52:02] JSON取得: infra-000012, 1035194
[2021/05/21 20:55:13] CSV変換: /tmp/app-000095.json
[2021/05/21 20:55:13] CSV変換: /tmp/infra-000012.json
[2021/05/21 20:56:12] CSV変換: /tmp/infra-000013.json
[2021/05/21 20:58:43] CSV変換: /tmp/infra-000014.json
[2021/05/21 21:01:12] CSV変換: /tmp/infra-000015.json
[2021/05/21 21:02:42] CSVファイル分割: /tmp/app-000095.csv
[2021/05/21 21:02:42] CSVファイル分割: /tmp/infra-000012.csv
[2021/05/21 21:02:55] CSVファイル分割: /tmp/infra-000013.csv
[2021/05/21 21:03:27] CSVファイル分割: /tmp/infra-000014.csv
[2021/05/21 21:03:59] CSVファイル分割: /tmp/infra-000015.csv
[2021/05/21 21:04:19] CSVファイルソート: /tmp/infra-0__.tmp
[2021/05/21 21:04:19] CSVファイルソート: /tmp/infra-0_openshift-image-registry_node-ca.tmp
･･･
```

CSVファイルの行数が各インデックスの検索件数と一致しています。

```bash:bastionノード
wc -l /tmp/*.csv
### 標準出力↓
        32 /tmp/app-000095.csv
   1035194 /tmp/infra-000012.csv
   2620386 /tmp/infra-000013.csv
   2605663 /tmp/infra-000014.csv
   1579811 /tmp/infra-000015.csv
   7841086 合計
```

```bash:export.sh
token=$(oc whoami -t)
header1="Authorization: Bearer ${token}"
header2="Content-Type: application/json"
domain=elasticsearch-openshift-logging.apps.ocp.powervs

# インデックス一覧取得
curl -sk -H "${header1}" https://${domain}/_cat/indices | egrep "app-|infra-" | awk '{print $3}' > /tmp/indices.lst

cat /tmp/indices.lst | while read index
do
  # インデックスの件数取得
  #count=`curl -sk -H "${header1}" https://${domain}/_cat/count/${index} | awk '{print $3}'`
  count=`curl -sk -H "${header1}" -H "${header2}" https://${domain}/${index}/_count -d @count.json | jq .count`
  if [ ${count}"X" = "X" ]; then
    count = -1
  fi

  # JSON取得
  if [ ${count} -gt 0 -a ${count} -le 10000 ]; then
    # レコード数10000以下
    echo [`date "+%Y/%m/%d %H:%M:%S"`] "JSON取得: ${index}, ${count}"
    curl -sk -H "${header1}" https://${domain}/${index}/_search?size=${count} > /tmp/${index}.json
  elif [ ${count} -gt 10000 ]; then
    # レコード数10000より大きい
    echo [`date "+%Y/%m/%d %H:%M:%S"`] "JSON取得: ${index}, ${count}"
    curl -sk -H "${header1}" -H "${header2}" -d @export.json \
      https://${domain}/${index}/_search?scroll=1m > /tmp/${index}.json
    scroll_id=`jq -r "._scroll_id" /tmp/${index}.json  | tr -d '\n'`

    length=-1
    while [ ${length} -ne 0 ]
    do
      curl -sk -H "${header1}" -H "${header2}" -d "{\"scroll\":\"1m\",\"scroll_id\":\"${scroll_id}\"}" \
        https://${domain}/_search/scroll > /tmp/temp.json
      length=`jq -r ".hits.hits | length" /tmp/temp.json`
      cat /tmp/temp.json >> /tmp/${index}.json
      rm -f /tmp/temp.json
    done

    # スクロールID削除
    curl -sk -H "${header1}" -H "${header2}" -XDELETE -d "{ \"scroll_id\": \"${scroll_id}\"}" \
      https://${domain}/_search/scroll > /dev/null
  fi
done

# JSONからCSVへの変換
ls -l /tmp/*.json | grep -v "temp.json" | awk '{print $9}' | while read fname
do
  echo [`date "+%Y/%m/%d %H:%M:%S"`] "CSV変換: ${fname}"
  bname=`basename ${fname} | awk -F. '{print $1}'`
  cat ${fname} | jq -r '.hits.hits[] |
  [ ._source."@timestamp",
    ._source.hostname,
    ._source.kubernetes.namespace_name,
    ._source.kubernetes.container_name,
    ._source.kubernetes.pod_name,
    ._source.message] | @csv' | sed -e 's/\"//g' -e 's/,/#/6g' > /tmp/${bname}.csv
done

# CSVファイルをコンテナ単位に分割
ls -l /tmp/*.csv | awk '{print $9}' | while read fname
do
  echo [`date "+%Y/%m/%d %H:%M:%S"`] "CSVファイル分割: ${fname}"
  awk -F, '{out="/tmp/"$2"_"$3"_"$4".tmp"; print $1","$5","$6 >> out}' ${fname}
done

# コンテナ単位に分割したCSVファイルを時系列でソート
ls -l /tmp/*.tmp | awk '{print $9}' | while read fname
do
  echo [`date "+%Y/%m/%d %H:%M:%S"`] "CSVファイルソート: ${fname}"
  bname=`basename ${fname} | sed -e 's/\.tmp$//g'`
  sort ${fname} > /tmp/output/${bname}.csv
  rm -f ${fname}
done
```

```json:count.json
{
  "query": {
    "range": {
      "@timestamp": {
        "lt": "2021-05-21",
        "gte": "2021-05-20"
      }
    }
  }
}
```

```json:export.json
{
  "size": "10000",
  "_source": [
    "kubernetes.container_name",
    "kubernetes.namespace_name",
    "kubernetes.pod_name",
    "hostname",
    "message",
    "@timestamp"
  ],
  "query": {
    "range": {
      "@timestamp": {
        "lt": "2021-05-21",
        "gte": "2021-05-20"
      }
    }
  }
}
```