OpenShift上の Pod のログを rsyslog に転送する

## はじめに

OpenShift上の Pod は、再作成されると Pod のログまで消失してしまいます。これでは運用に堪えないので、ログを外部に転送することを検討することにしました。

OpenShift上にLokiStackを構築してもいいのですが、検証環境は非力なスペックなので、syslogサーバーに転送することにしました。

検証環境は下記記事で作ったものです。
* [install_yamls で作る OpenStack in OpenShift #openshift - Qiita](https://qiita.com/xotaki/items/ca3a4a3cfafa45d84d17)

## システム構成

* **ClusterLogForwarder**
    * OpenShift では、ClusterLogForwarder リソースを使用してログを転送します
    * openshift-logging という namespace に作成します
* **rsyslog**
    * 今回は bastionサーバーに構築します
    * `/var/log/openshift/<namespace name>/<pod name>_<container name>.log` という命名規則でログを出力することを目指します

## 構築手順

下記、公式手順を参考にしながら進めていきます。

* [1.4. ロギングのインストール | Logging | OpenShift Container Platform | 4.18 | Red Hat Documentation](https://docs.redhat.com/ja/documentation/openshift_container_platform/4.18/html/logging/installing-logging-6-2)

今回は、ログをクラスタ外のみ転送し、クラスタの内部ログストアに格納しないため、内部ログストアとなる LokiStack は不要なので、 Loki Operator のインストール、 LokiStack の作成は不要です。

### OpenShift側の設定

1. openshift-logging namespace の作成
    ```bash
    oc create namespace openshift-logging
    ```
2. OperatorGroup の作成
    ```yaml:01_OperatorGroup_openshift-logging.yaml 
    apiVersion: operators.coreos.com/v1
    kind: OperatorGroup
    metadata:
      name: cluster-logging
      namespace: openshift-logging
    spec:
      upgradeStrategy: Default
    ```
    ```bash
    oc apply -f 01_OperatorGroup_openshift-logging.yaml
    ```
3. Subscription の作成
    ```yaml:02_subscription_cluster-logging.yaml
    apiVersion: operators.coreos.com/v1alpha1
    kind: Subscription
    metadata:
      name: cluster-logging
      namespace: openshift-logging
    spec:
      channel: stable-6.2
      installPlanApproval: Automatic
      name: cluster-logging
      source: redhat-operators
      sourceNamespace: openshift-marketplace
    ```
    ```bash
    oc apply -f 02_subscription_cluster-logging.yaml
    ```
    なお、 channel の候補は以下で確認できます。
    ```bash:実行例
    $ oc get packagemanifests cluster-logging -n openshift-marketplace -o jsonpath='{.status.channels[*].name}'
    stable-6.1 stable-6.2
    ```
4. インストール状況の確認
    以下を確認します。
    ```bash
    # Operatorのインストール状況確認
    oc get csv -n openshift-logging

    # Podの状態確認
    oc get pods -n openshift-logging

    # CRD（Custom Resource Definition）の確認
    oc get crd | grep logging
    ```
    ```bash:実行例
    $ oc get csv -n openshift-logging
    NAME                                    DISPLAY                     VERSION               REPLACES                 PHASE
    cluster-logging.v6.2.2                  Red Hat OpenShift Logging   6.2.2                 cluster-logging.v6.2.1   Succeeded
    metallb-operator.v4.18.0-202505081435   MetalLB Operator            4.18.0-202505081435                            Succeeded
    openstack-operator.v0.3.0               OpenStack                   0.3.0                                          Succeeded
    $ oc get pods -n openshift-logging
    NAME                                       READY   STATUS    RESTARTS   AGE
    cluster-logging-operator-b5d867f4c-mbcss   1/1     Running   0          2m17s
    $ oc get crd | grep logging
    logfilemetricexporters.logging.openshift.io                       2025-05-31T17:07:39Z
    loggings.telemetry.openstack.org                                  2025-05-31T16:06:09Z
    ```
5. Servcice Accountの作成
    ログコレクターが使用するサービスアカウントを作成し、コレクターがログを収集して転送できるように、サービスアカウントに必要な権限を割り当てます。ここでは、コレクターにインフラストラクチャーログとアプリケーションログの両方を収集する権限を付与します。
    ```bash
    oc create sa logging-collector -n openshift-logging
    oc adm policy add-cluster-role-to-user logging-collector-logs-writer -z logging-collector -n openshift-logging
    oc adm policy add-cluster-role-to-user collect-application-logs -z logging-collector -n openshift-logging
    oc adm policy add-cluster-role-to-user collect-infrastructure-logs -z logging-collector -n openshift-logging
    ```
6. ClusterLogForwarder の作成
    ClusterLogging が稼動したら、ClusterLogForwarderを作成します。
    ```yaml:04_clusterlogforwarder.yaml
    apiVersion: observability.openshift.io/v1
    kind: ClusterLogForwarder
    metadata:
      name: instance
      namespace: openshift-logging
    spec:
      serviceAccount:
        name: logging-collector
      outputs:
      - name: syslog-output
        type: syslog
        syslog:
          url: udp://172.18.0.253:514
          facility: local0
          severity: informational
          tag: "openshift"
          rfc: RFC5424
      pipelines:
      - name: application-logs
        inputRefs:
        - application
        outputRefs:
        - syslog-output
    ```

### rsyslog側の設定

1. rsyslogの設定ファイルの編集
    /etc/rsyslog.conf は最終的には以下のようになりました。試行錯誤の過程は後述します。
    ```diff
    --- /etc/rsyslog.conf.default   2025-05-07 22:14:04.000000000 +0900
    +++ /etc/rsyslog.conf   2025-06-01 02:45:21.909801001 +0900
    @@ -29,14 +29,19 @@

     # Provides UDP syslog reception
     # for parameters see http://www.rsyslog.com/doc/imudp.html
    -#module(load="imudp") # needs to be done just once
    -#input(type="imudp" port="514")
    +module(load="imudp") # needs to be done just once
    +input(type="imudp" port="514")

     # Provides TCP syslog reception
     # for parameters see http://www.rsyslog.com/doc/imtcp.html
     #module(load="imtcp") # needs to be done just once
     #input(type="imtcp" port="514")

    +#### TEMPLATES ####
    +template(name="DynaPodFile" type="string" string="/var/log/openshift/%$.extracted_hostname%/%$.extracted_namespace_name%/%$.extracted_pod_name%_%$.extracted_container_name%.log")
    +$template OpenShiftLogFormat,"%TIMESTAMP% %HOSTNAME% %.extracted_namespace_name% %.extracted_pod_name% %.extracted_container_name% | %.extracted_message%\n"
    +
     #### RULES ####

     # Log all kernel messages to the console.
    @@ -79,3 +84,33 @@
     # # Remote Logging (we use TCP for reliable delivery)
     # # remote_host is: name/ip, e.g. 192.168.0.1, port optional e.g. 10514
     #Target="remote_host" Port="XXX" Protocol="tcp")
    +
    +if $syslogfacility-text == 'local0' then {
    +    # Pod名を抽出（正規表現を使用）
    +    if re_match($msg, '.*"pod_name":"([^"]+)".*') then {
    +        set $.extracted_hostname = re_extract($msg, '"hostname":"([^"]+)"', 0, 1, "unknown");
    +        set $.extracted_namespace_name = re_extract($msg, '"namespace_name":"([^"]+)"', 0, 1, "unknown");
    +        set $.extracted_pod_name       = re_extract($msg, '"pod_name":"([^"]+)"', 0, 1, "unknown");
    +        set $.extracted_container_name = re_extract($msg, '"container_name":"([^"]+)"', 0, 1, "unknown");
    +        #set $.extracted_message        = re_extract($msg, '"message":"([^"]+)"', 0, 1, "unknown");
    +
    +        # messageの開始位置から、次の","msg_idまでを抽出
    +        set $.extracted_message = re_extract($msg, '"message":"(.*?)","msg_id"', 0, 1, "");
    +
    +        # 見つからない場合
    +        if ($.extracted_message == "") then {
    +            set $.extracted_message = "Message extraction failed";
    +        }
    +
    +        # Pod名が取得できた場合
    +        if ($.extracted_pod_name != "unknown") then {
    +            local0.* ?DynaPodFile;OpenShiftLogFormat
    +            stop
    +        }
    +    }
    +}
    ```
2. ログディレクトリの作成
    ```bash
    sudo mkdir -p /var/log/openshift
    ```
3. ログローテーション設定
    ```/etc/logrotate.d/openshift
    /var/log/openshift/*/*.log {
        daily
        rotate 30
        compress
        delaycompress
        missingok
        create 0644 syslog syslog
        postrotate
            systemctl reload rsyslog
        endscript
    }
    ```
4. ファイアウォール設定
    ```bash
    sudo firewall-cmd --permanent --add-port=514/udp
    sudo firewall-cmd --permanent --add-port=514/tcp
    sudo firewall-cmd --reload
    ```
5. SELinux設定
    ```bash
    sudo setsebool -P nis_enabled 1
    sudo semanage port -a -t syslogd_port_t -p udp 514
    sudo semanage port -a -t syslogd_port_t -p tcp 514
    ```
6. rsyslogサービスの再起動
    ```bash
    sudo rsyslogd -N1 -f /etc/rsyslog.conf
    sudo systemctl restart rsyslog
    ```
    ```bash:実行例
    $ sudo rsyslogd -N1 -f /etc/rsyslog.conf
    rsyslogd: version 8.2412.0-2.el9, config validation run (level 1), master config /etc/rsyslog.conf
    rsyslogd: End of config validation run. Bye.
    ```
7. 出力されたログの確認
    ```bash:実行例
    $ cd /var/log/openshift
    $ sudo tree .
    .
    └── crc
        ├── baremetal-operator-system
        │   ├── baremetal-operator-ironic-8677964577-lbsjj_ironic-httpd.log
        │   ├── baremetal-operator-ironic-8677964577-lbsjj_ironic.log
        │   └── baremetal-operator-ironic-8677964577-lbsjj_mariadb.log
        ├── cert-manager
        │   └── cert-manager-cainjector-645bd4478d-24x9q_cert-manager-cainjector.log
        ├── cert-manager-operator
        │   └── cert-manager-operator-controller-manager-698c6d7767-qr7k9_cert-manager-operator.log
        ├── hostpath-provisioner
        │   ├── csi-hostpathplugin-fp6dj_csi-provisioner.log
        │   └── csi-hostpathplugin-fp6dj_hostpath-provisioner.log
        ├── metallb-system
        │   ├── controller-764dff778f-sp6pf_controller.log
        │   ├── frr-k8s-jwx4m_controller.log
        │   ├── frr-k8s-jwx4m_frr.log
        │   ├── metallb-operator-controller-manager-564755966-6q58k_manager.log
        │   └── speaker-w2pbh_speaker.log
        ├── openstack
        │   ├── ceilometer-0_ceilometer-central-agent.log
        │   ├── ceilometer-0_ceilometer-notification-agent.log
        │   ├── ceilometer-0_proxy-httpd.log
        │   ├── cinder-api-0_cinder-api-log.log
        │   ├── cinder-api-0_cinder-api.log
    (snip)
        └── openstack-operators
            ├── barbican-operator-controller-manager-655f8448d8-59bdm_manager.log
            ├── cinder-operator-controller-manager-645dc58df8-4x97r_manager.log
            ├── glance-operator-controller-manager-669856f45d-s4f8p_manager.log
    (snip)
    ```
    以下のようなログが出力されました。
    ```
    May 31 05:39:02 crc baremetal-operator-system baremetal-operator-ironic-8677964577-lbsjj ironic-httpd | 127.0.0.1 - - [31/May/2025:05:39:01 +0000] \"GET / HTTP/1.1\" 200 474 \"-\" \"curl/7.76.1\"
    May 31 05:39:02 crc baremetal-operator-system baremetal-operator-ironic-8677964577-lbsjj ironic-httpd | [Sat May 31 05:39:01.617894 2025] [ssl:debug] [pid 146:tid 149] ssl_engine_io.c(1150): [client 127.0.0.1:50322] AH02001: Connection closed to child 193 with standard shutdown (server 192.168.122.10:6385)
    ```

---

## rsyslog.conf のチューニング

### Step1. そのまま出力
まずは local0 で受け取ったものを /var/log/openshift/openshift.log に出力してみます。
```/etc/rsyslog.conf
$template OpenShiftLogFormat,"%TIMESTAMP% %HOSTNAME% %syslogtag %msg%\n"
local0.*    /var/log/openshift/openshift.log;OpenShiftLogFormat
```
以下のようなログが出力されました。
```
May 31 17:29:40 crc openstack_memcached-0_memcached[63591bb1-0722-4505-9616-fb5fd5dbdb26] {"@timestamp":"2025-05-31T17:29:40.425489219Z","app_name":"openstack_memcached-0_memcached","facility":"local0","hostname":"crc","kubernetes":{"annotations":{"k8s.ovn.org/pod-networks":"{\"default\":{\"ip_addresses\":[\"10.217.0.101/23\"],\"mac_address\":\"0a:58:0a:d9:00:65\",\"gateway_ips\":[\"10.217.0.1\"],\"routes\":[{\"dest\":\"10.217.0.0/22\",\"nextHop\":\"10.217.0.1\"},{\"dest\":\"10.217.4.0/23\",\"nextHop\":\"10.217.0.1\"},{\"dest\":\"169.254.0.5/32\",\"nextHop\":\"10.217.0.1\"},{\"dest\":\"100.64.0.0/16\",\"nextHop\":\"10.217.0.1\"}],\"ip_address\":\"10.217.0.101/23\",\"gateway_ip\":\"10.217.0.1\",\"role\":\"primary\"}}","k8s.v1.cni.cncf.io/network-status":"[{\n    \"name\": \"ovn-kubernetes\",\n    \"interface\": \"eth0\",\n    \"ips\": [\n        \"10.217.0.101\"\n    ],\n    \"mac\": \"0a:58:0a:d9:00:65\",\n    \"default\": true,\n    \"dns\": {}\n}]","openshift.io/scc":"anyuid"},"container_id":"cri-o://b19aa1c9e60596b9ad86d6649ba2bafc3827bf49e6eca9265bb660af35058479","container_image":"quay.io/podified-antelope-centos9/openstack-memcached@sha256:83b7d261083257b382f78e0a1190dd2c8d2e46c8d14dabd0ebb81c8c9be9fb4a","container_image_id":"quay.io/podified-antelope-centos9/openstack-memcached@sha256:83b7d261083257b382f78e0a1190dd2c8d2e46c8d14dabd0ebb81c8c9be9fb4a","container_iostream":"stderr","container_name":"memcached","labels":{"app":"memcached","apps_kubernetes_io_pod-index":"0","controller-revision-hash":"memcached-8b564785b","cr":"memcached","memcached_name":"memcached","memcached_namespace":"openstack","memcached_uid":"4d668ca9-c6b3-4bbc-af33-eef0da3c8d74","owner":"infra-operator","service":"memcached","statefulset_kubernetes_io_pod-name":"memcached-0"},"namespace_id":"69c7a12e-a245-4481-b3d9-fd9dccd6f001","namespace_labels":{"kubernetes_io_metadata_name":"openstack","pod-security_kubernetes_io_enforce":"privileged","security_openshift_io_scc_podSecurityLabelSync":"false"},"namespace_name":"openstack","pod_id":"63591bb1-0722-4505-9616-fb5fd5dbdb26","pod_ip":"10.217.0.101","pod_name":"memcached-0","pod_owner":"StatefulSet/memcached"},"level":"default","log_source":"container","log_type":"application","message":"<43 new auto-negotiating client connection","msg_id":"container","openshift":{"cluster_id":"6df651c0-eb51-42db-a77c-8a60f65389bc","sequence":1748712580660089637},"proc_id":"63591bb1-0722-4505-9616-fb5fd5dbdb26","severity":"informational","timestamp":"2025-05-31T17:29:40.425489219Z"}
```
ログを整形すると以下のような構造になっています。
```json
{
  "@timestamp": "2025-05-31T17:29:40.425489219Z",
  "app_name": "openstack_memcached-0_memcached",
  "facility": "local0",
  "hostname": "crc",
  "kubernetes": {
    "annotations": {
      "k8s.ovn.org/pod-networks": "{\"default\":{\"ip_addresses\":[\"10.217.0.101/23\"],\"mac_address\":\"0a:58:0a:d9:00:65\",\"gateway_ips\":[\"10.217.0.1\"],\"routes\":[{\"dest\":\"10.217.0.0/22\",\"nextHop\":\"10.217.0.1\"},{\"dest\":\"10.217.4.0/23\",\"nextHop\":\"10.217.0.1\"},{\"dest\":\"169.254.0.5/32\",\"nextHop\":\"10.217.0.1\"},{\"dest\":\"100.64.0.0/16\",\"nextHop\":\"10.217.0.1\"}],\"ip_address\":\"10.217.0.101/23\",\"gateway_ip\":\"10.217.0.1\",\"role\":\"primary\"}}",
      "k8s.v1.cni.cncf.io/network-status": "[{\n    \"name\": \"ovn-kubernetes\",\n    \"interface\": \"eth0\",\n    \"ips\": [\n        \"10.217.0.101\"\n    ],\n    \"mac\": \"0a:58:0a:d9:00:65\",\n    \"default\": true,\n    \"dns\": {}\n}]",
      "openshift.io/scc": "anyuid"
    },
    "container_id": "cri-o://b19aa1c9e60596b9ad86d6649ba2bafc3827bf49e6eca9265bb660af35058479",
    "container_image": "quay.io/podified-antelope-centos9/openstack-memcached@sha256:83b7d261083257b382f78e0a1190dd2c8d2e46c8d14dabd0ebb81c8c9be9fb4a",
    "container_image_id": "quay.io/podified-antelope-centos9/openstack-memcached@sha256:83b7d261083257b382f78e0a1190dd2c8d2e46c8d14dabd0ebb81c8c9be9fb4a",
    "container_iostream": "stderr",
    "container_name": "memcached",
    "labels": {
      "app": "memcached",
      "apps_kubernetes_io_pod-index": "0",
      "controller-revision-hash": "memcached-8b564785b",
      "cr": "memcached",
      "memcached_name": "memcached",
      "memcached_namespace": "openstack",
      "memcached_uid": "4d668ca9-c6b3-4bbc-af33-eef0da3c8d74",
      "owner": "infra-operator",
      "service": "memcached",
      "statefulset_kubernetes_io_pod-name": "memcached-0"
    },
    "namespace_id": "69c7a12e-a245-4481-b3d9-fd9dccd6f001",
    "namespace_labels": {
      "kubernetes_io_metadata_name": "openstack",
      "pod-security_kubernetes_io_enforce": "privileged",
      "security_openshift_io_scc_podSecurityLabelSync": "false"
    },
    "namespace_name": "openstack",
    "pod_id": "63591bb1-0722-4505-9616-fb5fd5dbdb26",
    "pod_ip": "10.217.0.101",
    "pod_name": "memcached-0",
    "pod_owner": "StatefulSet/memcached"
  },
  "level": "default",
  "log_source": "container",
  "log_type": "application",
  "message": "<43 new auto-negotiating client connection",
  "msg_id": "container",
  "openshift": {
    "cluster_id": "6df651c0-eb51-42db-a77c-8a60f65389bc",
    "sequence": 1748712580660089637
  },
  "proc_id": "63591bb1-0722-4505-9616-fb5fd5dbdb26",
  "severity": "informational",
  "timestamp": "2025-05-31T17:29:40.425489219Z"
}
```

今回は ELK などに送信したい訳ではないので、人間が見るにはかなり冗長です。JSONデータを parse して、message だけ出力するようにしてみます。また、namespace_name、container_name が kubernetes の中にあるので、その値も抽出する必要があります。

### Step2. rsyslog の mmjsonparse を使ってみる

rsyslog.conf を以下のようにしてみました。

```/etc/rsyslog.conf
module(load="mmjsonparse")
template(name="OpenShiftLogFormat" type="string" string="%TIMESTAMP% %HOSTNAME% %syslogtag% [%parsesuccess%] %msg%\n")
if $syslogtag contains "fluentd" then {
    # JSONを解析
    action(type="mmjsonparse")
    action(type="omfile" File="/var/log/openshift/openshift.log" template="OpenShiftLogFormat")
}
```

残念ながら、 `%parsesuccess` の値が FALSE になってしまい、 mmjsonparse では parse することはできませんでした。

### Step3. 正規表現で必要なパラメータを抽出する

rsyslog.conf を以下のようにしてみました。

```/etc/rsyslog.conf
#### TEMPLATES ####
template(name="DynaPodFile" type="string" string="/var/log/openshift/%$.extracted_hostname%/%$.extracted_namespace_name%/%$.extracted_pod_name%_%$.extracted_container_name%.log")
$template OpenShiftLogFormat,"%TIMESTAMP% %HOSTNAME% %.extracted_namespace_name% %.extracted_pod_name% %.extracted_container_name% | %.extracted_message%\n"

if $syslogtag contains "fluentd" then {
    # Pod名を抽出（正規表現を使用）
    if re_match($msg, '.*"pod_name":"([^"]+)".*') then {
        set $.extracted_hostname = re_extract($msg, '"hostname":"([^"]+)"', 0, 1, "unknown");
        set $.extracted_namespace_name = re_extract($msg, '"namespace_name":"([^"]+)"', 0, 1, "unknown");
        set $.extracted_pod_name       = re_extract($msg, '"pod_name":"([^"]+)"', 0, 1, "unknown");
        set $.extracted_container_name = re_extract($msg, '"container_name":"([^"]+)"', 0, 1, "unknown");

        # messageの開始位置から、次の","msg_idまでを抽出
        set $.extracted_message = re_extract($msg, '"message":"(.*?)","msg_id"', 0, 1, "");

        # 見つからない場合
        if ($.extracted_message == "") then {
            set $.extracted_message = "Message extraction failed";
        }

        # Pod名が取得できた場合
        if ($.extracted_pod_name != "unknown") then {
            local0.* ?DynaPodFile;OpenShiftLogFormat
            stop
        }
    }
}
```

`message` の内容に「"」が含まれていると、`namespace_name` などのように parse すると途中で切れてしまうことが分かったので、`message` の次の `msg_id` が現れるまでを抽出することにしました。

## 参考

* [1.4. ロギングのインストール | Logging | OpenShift Container Platform | 4.18 | Red Hat Documentation](https://docs.redhat.com/ja/documentation/openshift_container_platform/4.18/html/logging/installing-logging-6-2)