エックスサーバーのログをDocker + EFKで可視化したい!

# やりたいこと

* エックスサーバーのapacheログをもってきてEFK(Elasticsearch + Fluentd + Kibana)で可視化
  * エックスサーバーでは毎日5時にそれまでのログをgzipファイルにまとめている
  * 過去のログの保存日数はデフォルトで過去30日分
  * 詳しくはこちら https://www.xserver.ne.jp/manual/man_server_log.php
* 別途バックアップしていた30日以上前のgzipで圧縮されたログも一緒に見たい
* dockerの勉強(これが一番の目的)

## 事前準備

エックスサーバーではSSHを使用するをONにしておいて、鍵の生成をしておく。

## ファイル配置

~~~Bash
.
├── docker-compose.yaml
├── fetch
│   ├── Dockerfile
│   ├── cron.sh
│   ├── fetch.sh
│   ├── init.sh
│   ├── private.key
│   └── oldlogs
│       ├── example.com.access_log_20170726.gz
│       ├── example.com.access_log_20170727.gz
│       ├── example.com.access_log_20170728.gz
│       ├── example.com.access_log_20170729.gz
│       ├── example.com.access_log_20170730.gz
│       └── example.com.access_log_20170731.gz
└── fluentd
    ├── Dockerfile
    ├── fluent.conf
    └── plugins
~~~

## fetchコンテナ

* エックスサーバーに鍵認証、ポート10022でSSH接続してログをrsync
* cronで5分ごとに`/home/username/example.com/logs/`以下を`/var/log/apache2/`に持ってくる
* 別途バックアップしていたログはホストの`oldlogs`ディレクトリに配置しておき、コンテナの`/var/log/apache2_old/`に展開する
* 当日のログはシンボリックリンクになっているので、rsyncは`--copy-links`オプション必須

SSHの秘密鍵をどうやってコンテナに渡せばよいのか・・・
このやり方だとcommitしたときにイメージに含まれてしまう。

~~~Dockerfile:fetch/Dockerfile
FROM ubuntu

ENV XSRV_USER=username \
  XSRV_HOST=example.com \
  XSRV_PORT=10022

RUN apt-get update && \
  apt-get upgrade -y && \
  apt-get install -y openssh-client cron rsync && \
  mkdir -p /root/.ssh && \
  mkdir -p /var/log/apache2 && \
  touch /var/log/apache2/access.log && \
  touch /var/log/apache2/access.log.old

COPY "init.sh" "/root/init.sh"
COPY "cron.sh" "/root/cron.sh"
COPY "private.key" "/root/.ssh/private.key"

CMD /bin/bash /root/init.sh
~~~

最初の1回だけ実行するスクリプト

* SSH秘密鍵のパーミッションを設定
* 登録済みホストに登録
  * rsyncで使うとIPアドレスになって渡されるので、どうにかしたい
* 昨日以前のgzになっているログを全部`access.log.old`に展開
* 環境変数を`env.sh`に書き出してcronに登録(c.f. http://qiita.com/rerofumi/items/fc0126c4e985b78f769b )

~~~bash:fetch/init.sh
#!/bin/bash

cd /var/log/apache2

if [ -d /var/log/apache2_old/ ]; then
  rsync -av /var/log/apache2_old/*.gz ./
fi

if [ -f /root/.ssh/private.key ]; then
  chmod 600 /root/.ssh/private.key
  ssh-keyscan -p ${XSRV_PORT} -H ${XSRV_HOST} > /root/.ssh/known_hosts
  rsync -av --copy-links -e "ssh -p ${XSRV_PORT} -i /root/.ssh/private.key" ${XSRV_USER}@${XSRV_HOST}:/home/${XSRV_USER}/${XSRV_HOST}/log/ ./
fi

zcat *.gz > access.log.old

printenv | awk '{print "export " $1}' > /root/env.sh
echo '*/5 * * * * root /bin/bash /root/cron.sh' >> /etc/crontab
cron -f
~~~

cronで起動するスクリプト

* `env.sh`から環境変数を読み込む
* 当日分のログ(`${XSRV_HOST}.access_log`)を`access.log`にコピー

~~~bash:fetch/cron.sh
#!/bin/bash
. /root/env.sh

cd /var/log/apache2

if [ -f /root/.ssh/private.key ]; then
  rsync -av --copy-links -e "ssh -p ${XSRV_PORT} -i /root/.ssh/private.key" ${XSRV_USER}@${XSRV_HOST}:/home/${XSRV_USER}/${XSRV_HOST}/log/ ./
fi

cp -f ${XSRV_HOST}.access_log access.log
~~~

## fluentdコンテナ

* エックスサーバーの吐くapacheログのフォーマットを読み込む
* ログをElasticsearchに渡す

~~~Dockerfile:fluentd/Dockerfile
FROM fluent/fluentd

RUN gem install fluent-plugin-elasticsearch
COPY fluent.conf /fluentd/etc/fluent.conf
~~~

~~~apache:fluentd/fluent.conf
<source>
  type tail
  path /var/log/apache2/access.log,/var/log/apache2/access.log.old
  read_from_head true
  tag apache.access_log
  pos_file /home/fluent/access.log.pos
  format /^(?<virtualhost>[^ ]*) (?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^ ]*) +\S*)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>(\\\"|[^\"])*)" "(?<agent>(\\\"|[^\"])*)")?$/
  time_format %d/%b/%Y:%H:%M:%S %z
  types code:integer,size:integer
</source>

<match apache.**>
  type copy
  <store>
    type elasticsearch
    include_tag_key true
    tag_key _tag
    host elasticsearch
    port 9200
    index_name access
    logstash_format true
    logstash_prefix logstash

    buffer_type file
    buffer_path /tmp/fluentd*.buffer
#    buffer_chunk_limit 1g
#    buffer_queue_limit 256
#    flush_interval 60s
#    retry_wait 5s
  </store>
</match>
~~~

## docker-compose.yaml

* ElasticsearchとKibanaはDockerHubのものではなく公式のものを使用
* 永続化データはNamed Data Volumeに保存
* ログをfluentdからElasticsearchに送るときにキューが溢れたりしないように、Elasticsearchのパラメータを調整

~~~yaml:docker-compose.yaml
version: '2'

services:
  fetch:
    build: ./fetch
    volumes:
      - apachelog:/var/log/apache2
      - ./fetch/oldlogs:/var/log/apache2_old:ro
    restart: always

  fluentd:
    build: ./fluentd
    volumes:
      - apachelog:/var/log/apache2
      - fluentdata:/var/log/fluent
    environment:
      FLUENTD_CONF: fluent.conf
    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:
      - "5601: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:
  apachelog:
    driver: local
  fluentdata:
    driver: local
  esdata:
    driver: local
  esconfig:
    driver: local
~~~

## 起動・終了

起動

~~~bash
docker-compose up -d --build
~~~

終了

~~~bash
docker-compose down
~~~


# 感想

* SSH秘密鍵ってどうすればいいの?
  * 環境変数使うとcronからの実行時に見えないので、コンテナ内にファイルで書き出しておくのかな?
  * でもファイルに書き出すとcommitでイメージに含まれてしまう
  * docker swarmでないときにsecretって使えないの?
* `fluent.conf`で`path`にワイルドカードを指定しても認識してくれないんだけど・・・
  * しょうがないからgzipになっているログは全部1つのファイル(`access.log.old`)にまとめてしまえ。
  * ログが入れ替わった直後(毎日5時)は、ログが抜けるけど仕方ないかな。
  * 本当は前日までのログ(*.gz)と当日分のログを1つのファイルに結合させたいんだけど、inodeを変えないで差分だけ追記していく方法がうまくできなかった
* 公式のElasticsearchとKibanaコンテナの設定難しい、難しくない?