AmazonLinux1(CentOS6世代)×fluentd GemでCloudWatchにログを転送する

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417736/e791b072-2d84-96b8-9aa8-50424d79e77e.png" alt="fluentd" width="80">

`AmazonLinux1(CentOS6世代)` のlatest`td-agent3(embedded ruby2.4)` において、CloudWatch連携プラグイン`fluent-plugin-cloudwatch-logs`のgem依存関係がruby>=2.5を必要とするようになったため、システムのrubyランタイムを使って`fluentd`gem直でCloudWatchにサーバーログを送信するためのロードマップ。

## Ruby環境
[AWS Amazon EC2にrbenvでRuby環境を構築する](https://qiita.com/hytkgami/items/5cc393c4327bdc4ea0ef)

rbenvで2.5.1を入れてglobalをスイッチ

```bash:sh
# rbenvバイナリをgithubからクローン
git clone https://github.com/sstephenson/rbenv.git /opt/rbenv

# ログインプロフィールにrbenvの環境設定
echo 'export RBENV_ROOT="/opt/rbenv"' > /etc/profile.d/rbenv.sh
echo 'export PATH="${RBENV_ROOT}/bin:${PATH}"' >> /etc/profile.d/rbenv.sh
echo 'eval "$(rbenv init -)"' >> /etc/profile.d/rbenv.sh

# ログインプロフィールをローディング
source /etc/profile.d/rbenv.sh

# ruby 2.5.1をインストールする
rbenv install -v 2.5.1
rbenv rehash

# インストールバイナリ確認
rbenv versions

* system
2.5.1 (set by /opt/rbenv/version)

# システムバージョンを2.5.1に変更
rbenv global 2.5.1

# システムバージョンを確認
rbenv versions
  system
* 2.5.1 (set by /opt/rbenv/version)

```

## fluentdパッケージをインストール

rbenvで環境を縛る

```bash:sh
rbenv exec gem install fluentd

# yajl-rubyがrequire ruby >= 2.6.0
ERROR:  Error installing fluentd:
        The last version of yajl-ruby (~> 1.0) to support your Ruby & RubyGems was 1.4.1. Try installing it with `gem install yajl-ruby -v 1.4.1` and then running the current command again
        yajl-ruby requires Ruby version >= 2.6.0. The current ruby version is 2.5.0.

# yajl-rubyがrequire ruby >= 2.Xなv1.4.1に落としてインストール
rbenv exec gem install yajl-ruby -v 1.4.1

rbenv exec gem install fluentd

# bundlerがrequire ruby >= 3.0.0
ERROR:  Error installing fluentd:
        The last version of bundler (>= 0) to support your Ruby & RubyGems was 2.3.27. Try installing it with `gem install bundler -v 2.3.27` and then running the current command again
        bundler requires Ruby version >= 3.0.0. The current ruby version is 2.5.0.

# bundlerをrequire ruby >= 2.Xなv2.3.27に落としてインストール
rbenv exec gem install bundler -v 2.3.27

# 成功
rbenv exec gem install fluentd
```

## fluentdプラグインをインストール

### fluent-plugin-cloudwatch-logsプラグイン

CloudWatchに送信するために`fluent-plugin-cloudwatch-logs`プラグインをインストールする

```bash:sh
rbenv exec gem install fluent-plugin-cloudwatch-logs
```

### fluent-plugin-record-reformerプラグイン

ログにホストのメタ情報を添加するために`fluent-plugin-record-reformer`プラグインをインストールする

```bash:sh
rbenv exec gem install fluent-plugin-record-reformer
```

## fluentdのセットアップ
### fluent.conf

setupコマンドでconfを作成する

```bash:sh
fluentd --setup /etc/fluentd
> Installed /etc/fluentd/fluent.conf.
```

## fluentdの制御

### ネイティブ制御

フロント起動させて動作確認するときに

fluentdのフロント実行コマンド
```bash:sh
fluentd -c /path/to/fluent.conf -vv &
```

終了は直接プロセスを落とす
```bash:sh
pkill fluentd
```

### init.d制御

`/etc/init.d/fluentd`に下記のinit.dスクリプトを作成する

- コンフィグファイル: `/etc/fluentd/fluent.conf`
- ログファイル: `/var/log/fluentd/fluentd.log`

```bash:/etc/init.d/fluentd
#!/bin/sh
# chkconfig: 2345 99 01
# description: Fluentd
### BEGIN INIT INFO
# Provides:          fluentd
# Required-Start:    $local_fs $remote_fs
# Required-Stop:     $local_fs $remote_fs
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Start daemon at boot time
# Description:       Enable service provided by daemon.
### END INIT INFO
# USER=fluent
USER=root
# DAEMON=/usr/local/bin/fluentd
DAEMON=/opt/rbenv/shims/fluentd

DAEMON_ARGS="-c /etc/fluentd/fluent.conf -o /var/log/fluentd/fluentd.log --daemon /var/run/fluentd.pid"
PID=/var/run/fluentd.pid
case "$1" in
 start)
       echo -n "Starting Fluentd: "
       su - $USER -c "$DAEMON $DAEMON_ARGS"
       echo "done"
       ;;
 stop)
       echo -n "Stopping Fluentd: "
       kill `cat $PID`
       echo "done"
       ;;
 restart)
       $0 stop
       $0 start
       ;;
 *)
       echo "Usage: $0 {start|stop|restart}"
       exit 1
esac
exit 0
```

制御方法

```bash:sh
# init.dスクリプトに実行権限あてる
chmod +x /etc/init.d/fluentd
# 起動
/etc/init.d/fluentd start
# 終了
/etc/init.d/fluentd stop
# 再起動
/etc/init.d/fluentd restart
```

自動起動の設定

```bash:sh
chkconfig --add fluentd
chkconfig fluentd on

chkconfig | grep fluentd
> fluentd         0:off   1:off   2:on    3:on    4:on    5:on    6:off
```
## 設定

`/etc/fluentd/fluent.conf`

apacheのカスタムアクセスログの構成例 )
- 調整にRuby実装が可能
- formatディレクティブの正規表現の検証は[フォーマットデバッガ](http://fluentular.herokuapp.com/)でテストできる。

```xml:fluent.conf
# sourceディレクティブ
# pathに定義したログをトラッキングしてformatに合致したとき、tagとともに、<...>のキャプチャ内容をmatchフェーズに送る
<source>
  @type tail
  format /^(?<xforwarded>[^ ]*) (?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^ ]*) +\S*)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")? "(?<sess>[^\"]*)"$/
  time_format %d/%b/%Y:%H:%M:%S %z
  path /etc/httpd/logs/access.log
  pos_file /var/log/fluentd/log.httpd.access.idt.pos
  # メタ情報を添加するために.reformタグをつける
  tag this.tagname.reform
</source>

# matchディレクティブ
# sourceから送られてきたtag識別子をもとにmatchディレクティブのタグとスクリーニングし、それをどうするかを定義

# 1. はじめに.reformタグをスクリーニングして、ホストのメタ情報を添加して、matchフェーズに再入させる
<match **.reform>
  # record-reformerプラグイン
  @type record_reformer
  renew_record false
  enable_ruby true

  # 添加後、スクリーニング再入で本来のmatchアクションディレクティブに補足させるためにreformタグをクレンジングして、tagディレクティブでmatchフェーズに再入する
  # クレンジングしないと永遠に<match **.reform>にマッチして、無限ループになるので要注意
  tag ${(tag_parts - ["reform"]).join(".")}

  # ホスト名とタイムスタンプを添加する
  <record>
    hostname ${hostname}
    servertime ${time.iso8601(3)}
  </record>
</match>

# 2. <match **.reform>による再入後、データをアクションする
<match this.tagname.**>
  # cloudwatch-logsプラグイン
  @type cloudwatch_logs

  region ap-northeast-1
  log_group_name path.to.loggroup

  # ロググループ内に{タグ名}で自動的にログストリームを作成する
  auto_create_stream true
  use_tag_as_stream true
</match>

```

sourceディレクティブに入力されるapacheアクセスログ

```plaintext
XXX.XXX.XXX.XXX YYY.YYY.YYY.YYY - - [23/Mar/2024:05:45:00 +0900] "GET /path/to/page HTTP/1.1" 200 138792 "https://host.example.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36" "abcdefg"
```

フォーマット後のjson

```json:json
{
    "xforwarded": "XXX.XXX.XXX.XXX",
    "host": "YYY.YYY.YYY.YYY",
    "user": "-",
    "method": "GET",
    "path": "/path/to/page",
    "code": "200",
    "size": "687368",
    "referer": "https://host.example.com/",
    "agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36",
    "sess": "abcdefg",
	# record_reformerで追加したメタ情報
    "hostname": "fluent-host",
    "servertime": "2024-03-23T05:45:03.000+09:00"
}

```

## AWS

### EC2/IAM

EC2のロールに**CloudWatchLogsFullAccess**ポリシーを追加する

### CloudWatch

ロググループ`path.to.loggroup`を作成しておく
※ログストリームは`auto_create_stream`オプションで自動的に作成してくれる

### httpdとアプリログのスニペット

```xml:fluent.conf
############################################
# sourceディレクティブ 
############################################

# アプリケーションログ
<source>
  @type tail
  format /^\[(?<timestamp>.*)\]\[(?<level>[^ ]*)\] \[(?<ip>[^ ]*) - (?<session_id>[^ ]*).*\] (?<message>.*)$/
  path /var/www/project/log/*.log
  pos_file /var/log/fluentd/log.app.pos
  tag log.app.reform
  read_from_head true
</source>

# httpdアクセスログ
<source>
  @type tail
  format /^(?<xforwarded>[^ ]*) (?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^ ]*) +\S*)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")? "(?<sess>[^\"]*)"$/
  time_format %d/%b/%Y:%H:%M:%S %z
  path /etc/httpd/logs/access.log
  pos_file /var/log/fluentd/log.httpd.access.pos
  tag log.httpd.access.reform
</source>

# httpdエラーログ
<source>
  @type tail
  format /^\[[^ ]* (?<time>[^\]]*)\] \[(?<level>[^\]]*)\](?: \[pid (?<pid>[^\]]*)\])? \[client (?<client>[^\]]*)\] (?<message>.*)$/
  path /etc/httpd/logs/error.log
  pos_file /var/log/fluentd/log.httpd.error.pos
  tag log.httpd.error.reform
</source>

# auditlog
<source>
  @type tail
  format none
  path /var/log/audit/audit.log
  pos_file /var/log/fluentd/log.audit.pos
  tag log.audit.reform
</source>

# securelog
<source>
  @type tail
  format syslog
  path /var/log/secure
  pos_file /var/log/fluentd/log.secure.pos
  tag log.secure.reform
</source>

# ホストのメタ情報を追加してmatchフェーズに再入させる
<match **.reform>
  @type record_reformer
  renew_record false
  enable_ruby true
  tag ${(tag_parts - ["reform"]).join(".")}
  <record>
    hostname ${hostname}
    servertime ${time.iso8601(3)}
  </record>
</match>

# CloudWatchLogsのlog.appロググループに送信
<match log.app.**>
  @type cloudwatch_logs
  region ap-northeast-1
  log_group_name log.app
  auto_create_stream true
  use_tag_as_stream true
</match>

# CloudWatchLogsのlog.httpd.accessロググループに送信
<match log.httpd.access.**>
  @type cloudwatch_logs
  region ap-northeast-1
  log_group_name log.httpd.access
  auto_create_stream true
  use_tag_as_stream true
</match>

# CloudWatchLogsのlog.httpd.errorロググループに送信
<match log.httpd.error.**>
  @type cloudwatch_logs
  region ap-northeast-1
  log_group_name log.httpd.error
  auto_create_stream true
  use_tag_as_stream true
</match>

# CloudWatchLogsのlog.auditロググループに送信
<match log.audit.**>
  @type cloudwatch_logs
  region ap-northeast-1
  log_group_name log.audit
  auto_create_stream true
  use_tag_as_stream true
</match>

# CloudWatchLogsのlog.secureロググループに送信
<match log.secure.**>
  @type cloudwatch_logs
  region ap-northeast-1
  log_group_name log.secure
  auto_create_stream true
  use_tag_as_stream true
</match>
```