fluentd
PowerShell
Elasticsearch
kibana
WindowsServer

Windowsイベントログを標準機能だけでfluentdに突っ込みたい

やりたいこと

  • Windowsサーバにソフトウェアをインストールしないでfluentdにログを渡したい
  • ファイルサーバの監査ログをKibanaで気軽に見たい

ハードウェア構成

  • 稼働中のWindowsサーバ7台 (2008R2が3台 + 2012R2が4台) ← あまりいじりたくない
  • 余っているLinuxサーバ1台 (Ubuntu Server 16.04 LTS) ← 自由にいじれる

方針

  • dockerにfluentdとElasticsearch、Kibanaコンテナを立てる
  • fluentdのin_httpプラグインでJSONを受ける
  • PowerShellスクリプトでJSONを生成してfluentdに送信

docker + fluentd + Elasticsearch + Kibana

fluentdのin_httpでJSONを待ち受けるように設定する。

以下のようにdocker-compose.yamlDockerfilefluent.confを配置。
今回はfluentdのin_httpでは9880ポート、Kibanaでは5602ポートを使う。

.
├── docker-compose.yaml
└── fluentd
    ├── Dockerfile
    └── fluent.conf
fluentd/Dockercompose
FROM fluent/fluentd

RUN gem install fluent-plugin-elasticsearch
COPY fluent.conf /fluentd/etc/fluent.conf
fluentd/fluent.conf
<source>
  @type http
  port 9880
  bind 0.0.0.0
  body_size_limit 512m
  keepalive_timeout 10s
</source>
<match **>
  type elasticsearch
  host elasticsearch
  time_key TimeCreated
  port 9200
  logstash_format true
  flatten_hashes true
  flatten_hashes_separator _

  buffer_type file
  buffer_path /tmp/fluentd*.buffer
  buffer_chunk_limit 1024m
#  buffer_queue_limit 256
#  flush_interval 60s
#  retry_wait 5s
</match>
docker-compose.yaml
version: '2'

services:
  fluentd:
    build: ./fluentd
    volumes:
      - windowslog:/var/log/windows
      - fluentdata:/var/log/fluent
    environment:
      FLUENTD_CONF: fluent.conf
    ports:
      - "9880:9880"
    restart: always
    depends_on:
      - elasticsearch

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:5.5.2
    volumes:
      - esdata:/usr/share/elasticsearch/data
      - esconfig:/usr/share/elasticsearch/config
    expose:
      - "9200"
    restart: always
    environment:
      - bootstrap.memory_lock=true
      - xpack.security.enabled=false
      - xpack.monitoring.enabled=false
      - xpack.watcher.enabled=false
      - xpack.graph.enabled=false
      - xpack.ml.enabled=false
      - http.max_content_length=1g
      - thread_pool.index.queue_size=-1
      - thread_pool.bulk.queue_size=-1
      - "ES_JAVA_OPTS=-Xms2048m -Xmx2048m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    mem_limit: 4g

  kibana:
    image: docker.elastic.co/kibana/kibana:5.5.2
    ports:
      - "5602:5601"
    restart: always
    environment:
      - "ELASTICSEARCH_URL=http://elasticsearch:9200"
      - xpack.graph.enabled=false
      - xpack.security.enabled=false
      - xpack.ml.enabled=false
    depends_on:
      - elasticsearch

volumes:
  windowslog:
    driver: local
  fluentdata:
    driver: local
  esdata:
    driver: local
  esconfig:
    driver: local

コンテナ起動。

docker-compose up -d --build

Windowsサーバ側

「ソフトウェアをインストールしない」という要件を満たすため、PowerShellのGet-WinEventを使用する。

Get-WinEventはリモートコンピュータのイベントログも取得できるので、Windowsサーバの内1台にだけスクリプトを設置。ただし、ConvertTo-Jsonを使用するので、PowerShell 3.0以降が入ったWindows 2012R2上で実行する。

下記は1日1回タスクスケジューラで実行することを前提としたスクリプトで、前日の1日分のログを収集してfluentdに送信する。

export_eventlog.ps1
$hosts = @("SERVER1", "SERVER2", "SERVER3", "SERVER4", "SERVER5", "SERVER6", "SERVER7")
$lognames = @("Application", "Security", "System")

$date = Get-Date
$dateend = $date.AddHours(-$date.Hour).AddMinutes(-$date.Minute).AddSeconds(-$date.Second)
$datestart = $dateend.AddDays(-1)

$datestartutc = [System.TimeZoneInfo]::ConvertTimeToUtc($datestart).ToString("yyyy-MM-ddTHH:mm:ssZ")
$dateendutc = [System.TimeZoneInfo]::ConvertTimeToUtc($dateend).ToString("yyyy-MM-ddTHH:mm:ssZ")
$query = "*[System[TimeCreated[@SystemTime>='$datestartutc' and @SystemTime<='$dateendutc']]]"

foreach($h in $hosts){
  foreach($l in $lognames){
    Get-WinEvent -LogName $l -ComputerName $h -FilterXPath $query | % {
      $evt = $_ | ConvertTo-Json -Depth 5 | ConvertFrom-Json
      $evt.TimeCreated = $evt.TimeCreated.ToString("yyyy-MM-ddTHH:mm:ss.fffzzzz")
      $evt.KeywordsDisplayNames = $evt.KeywordsDisplayNames -join " "
      $evt.Properties = ($evt.Properties | % { $_.Value }) -join ' '
      $evt.MatchedQueryIds = $evt.MatchedQueryIds -join " "
      $evt.Bookmark = $evt.Bookmark -join " "
      $evt.Message = $evt.Message -replace "\p{C}+", " "

      Write-Host "$h : $l : $($evt.RecordId)"

      $json = ConvertTo-Json $evt -Compress -Depth 5
      $json = $json -replace "\+", "\u002b"
      $body = [System.Text.Encoding]::UTF8.GetBytes("json=" + $json)
      try{
        $null = Invoke-WebRequest "http://(dockerホストのIP):9880/windows.eventlog" -Method Post -Body $body
      }catch{
        Write-Host $_.Exception.Message -ForegroundColor Red
      }
    }
  }
}

ConvertTo-Jsonでは日付型の値が"\/Date(1326441600000)\/"という形式になってしまいfluentdで解釈できないため、二度手間ではあるがConvertTo-JsonConvertFrom-Jsonとして複製した後、TimeCreateの値をフォーマット。
その他ネストした配列やオブジェクトはfluentdが上手く解釈できないので適当にくっつけておく。
たまにMessage中に不正な文字列が残るらしく、fluentdがエラーを返すので-replace "\p{C}+", " "でUnicode制御文字を消しておく。
URLエンコードしていないのが原因でした。$bodyは下記のようにUrlEncodeしませう。

Add-Type -AssemblyName System.Web

$body = [System.Text.Encoding]::UTF8.GetBytes("json=" + [System.Web.HttpUtility]::UrlEncode($json))

in_httpプラグインの仕様により、"+"が無視されてしまうので、"+""\u002b"エンコードする。

PowerShellの文字列をそのままPOSTすると、非ASCII文字が全部"?"になるので、UTF-8に変換する。

Invoke-WebRequestでfluentdコンテナにJSONをPOSTする。IPはホストのIP、ポートはdocker-compose.yamlで指定した9880ポート。

ある程度まとめて送ってHTTPリクエスト数を減らしたほうが若干高速化するが、下記の理由によりこのスクリプトではイベント1件ずつfluentdに送信している。

  • Get-WinEventに律速されるためあまり高速化しない
  • JSON文字列に不正な文字列が入っていた場合、全件弾かれてしまう

Kibana

ブラウザからhttp://(dockerホストのIP):5602/にアクセス。
時刻に"TimeCreated"を指定してインデックスパターンを作成。

感想

WindowsサーバにRubyなどをインストールしなくても、fluent-plugin-windows-eventlog相当の機能を実現できた。

懸念点はやはり速度で、だいたい数万件/時間でしかログを吐けない。ファイルサーバのアクセス監査ログだと生成速度に追いつけない。

速度のボトルネックはGet-WinEventなので、他にwevtutl.exeを使う方法も検討したが、出力されるXMLに情報が不足している(具体的にはMessageがない)ので断念した。なんとかできないものかなぁ。