CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: mod_wsgi のログを rsyslog と logrotate でローテーションする

今回は mod_wsgi で動作させた WSGI アプリケーションのログを syslog で飛ばして、それを rsyslog で受信して logrotate でローテーションさせてみる。 尚、Apache httpd 本体のログを syslog で飛ばす方法は以下を参照のこと。

blog.amedama.jp

検証用の環境には CentOS7 を使った。

$ cat /etc/redhat-release
CentOS Linux release 7.1.1503 (Core)
$ uname -r
3.10.0-229.11.1.el7.x86_64

必要なパッケージをインストールする

mod_wsgi など必要なパッケージをインストールする。 rsyslog と logrotate は最初から入っていると思う。

$ sudo yum -y install mod_wsgi rsyslog logrotate

WSGI アプリケーションを用意する

次に WSGI に対応したアプリケーションを用意する。 mod_wsgi の場合、application という名前のシンボルを自動的に読み込むようになっている。 ログの設定は設定ファイルから読み込むようにした。

$ cat << EOF | sudo tee /var/www/cgi-bin/myapp.wsgi > /dev/null
# -*- coding: utf-8 -*-

import os
import logging
from logging import config


logging.config.fileConfig(
    '/var/www/cgi-bin/logging.conf',
    disable_existing_loggers=False,
)
LOG = logging.getLogger(__name__)

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    LOG.info('Hello, World!')
    return ['Hello, World!\n']

EOF

WSGI アプリケーションを動作させるために mod_wsgi の設定ファイルを用意する。 先ほど作成した WSGI アプリケーションの場所を指定することになる。 また、動作させる場合はデーモンモードにすると良い。

$ cat << EOF | sudo tee /etc/httpd/conf.d/wsgi.conf > /dev/null
WSGISocketPrefix /var/run/wsgi

WSGIDaemonProcess myapp-process
WSGIScriptAlias /myapp /var/www/cgi-bin/myapp.wsgi
<Location /myapp>
WSGIProcessGroup myapp-process
</Location>
EOF

mod_wsgi の動作モードについては以下の記事に書いた。

blog.amedama.jp

WSGI アプリケーションが参照する、ロギングの設定ファイルを用意する。 ここで syslog でログを飛ばすように設定している。 ファシリティには local2 (SysLogHandler.LOG_LOCAL2) を使った。

$ cat << EOF | sudo tee /var/www/cgi-bin/logging.conf > /dev/null
[loggers]
keys=root

[handlers]
keys=syslogHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=syslogHandler

[handler_syslogHandler]
class=logging.handlers.SysLogHandler
level=DEBUG
formatter=simpleFormatter
args=(('localhost', handlers.SYSLOG_UDP_PORT), handlers.SysLogHandler.LOG_LOCAL2)

[formatter_simpleFormatter]
format=%(asctime)s - %(process)d - %(threadName)s - %(name)s - %(levelname)s - %(message)s @ %(pathname)s:%(lineno)d
EOF

ロギングの設定ファイルの詳細については以下を参照する。

16.6. logging — Python 用ロギング機能 — Python 3.5.2 ドキュメント

rsyslog を設定する

今度は syslog を受ける側の rsyslog を設定する。 まずは、syslog が UDP で転送されてくるので、それを受信できるようにする。

$ sudo sed -i.back -e "
  s:^#\(\$ModLoad imudp\)$:\1:
  s:^#\(\$UDPServerRun 514\)$:\1:
" /etc/rsyslog.conf

ファシリティ local2 のログを保存する先を指定する。

$ cat << EOF | sudo tee /etc/rsyslog.d/mod_wsgi.conf > /dev/null
local2.* /var/log/mod_wsgi/mod_wsgi.log
EOF

logrotate を設定する

rsyslog で保存したログを logrotate でローテーションする設定を用意する。

$ cat << EOF | sudo tee /etc/logrotate.d/mod_wsgi > /dev/null
/var/log/mod_wsgi/mod_wsgi.log {
    dateext
    hourly
    missingok
    rotate 3
    notifempty
    nocompress
    sharedscripts
    delaycompress
    postrotate
        /bin/kill -HUP \`cat /var/run/syslogd.pid 2> /dev/null\` 2> /dev/null || true
        /bin/kill -HUP \`cat /var/run/rsyslogd.pid 2> /dev/null\` 2> /dev/null || true
    endscript
}
EOF

動作を確認する

以上で設定がおわったので rsyslog と httpd と (再) 起動する。

$ sudo systemctl restart rsyslog
$ sudo systemctl start httpd

wget で WSGI アプリケーションにアクセスしてみる。

$ wget -qO - http://localhost/myapp
Hello, World!

すると、ログファイルが作られたことが確認できる。

$ sudo cat /var/log/mod_wsgi/mod_wsgi.log
Sep 12 23:49:16 2015-09-12 23: 49:16,399 - 4227 - MainThread - _mod_wsgi_18cb3436d24acb6e50a4d2521dd7445a - INFO - Hello, World! @ /var/www/cgi-bin/myapp.wsgi:16

動作確認のために、できたログファイルを強制的にローテーションさせてみよう。

$ sudo logrotate -fv /etc/logrotate.d/mod_wsgi
reading config file /etc/logrotate.d/mod_wsgi

Handling 1 logs

rotating pattern: /var/log/mod_wsgi/mod_wsgi.log  forced from command line (3 rotations)
empty log files are not rotated, old logs are removed
considering log /var/log/mod_wsgi/mod_wsgi.log
  log needs rotating
rotating log /var/log/mod_wsgi/mod_wsgi.log, log->rotateCount is 3
dateext suffix '-2015091223'
glob pattern '-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'
glob finding old rotated logs failed
renaming /var/log/mod_wsgi/mod_wsgi.log to /var/log/mod_wsgi/mod_wsgi.log-2015091223
running postrotate script

みるとちゃんとログファイルがローテーションされている。

$ sudo ls /var/log/mod_wsgi
mod_wsgi.log-2015091223

もちろん、再度アクセスすると新しいログファイルの方に書き込みが行われる。

$ wget -qO - http://localhost/myapp
Hello, World!
$ sudo ls /var/log/mod_wsgi
mod_wsgi.log  mod_wsgi.log-2015091223

めでたしめでたし。

まとめ

今回は mod_wsgi のログを rsyslog に飛ばして logrotate でローテーションするところを確認してみた。 尚、今回使った Python のロギング設定ファイルは、mod_wsgi に限らず使うことができる。 通常のアプリケーションのログを syslog で転送する場合にも有効なので、覚えておきたい。

Ansible: Dynamic Inventory を使ってみる

通常の Inventory は ini 形式を拡張したテキストファイルで、中には Ansible で管理したいホストと変数の情報が書き込まれている。 しかし、これだとホストの台数や名前などが動的に変化するシチュエーションでメンテナンスが難しい。 あとは、ホストの情報が別の場所で管理されているパターンも二重管理になる可能性がある。 そうした場合には Dynamic Inventory という機能を使って動的に Inventory を生成するやり方があるようだ。

Ansible をインストールする

検証用の環境には CentOS7 を使った。

$ cat /etc/redhat-release 
CentOS Linux release 7.1.1503 (Core) 
$ uname -r
3.10.0-229.11.1.el7.x86_64

EPEL 経由で Ansible をインストールする。

$ sudo yum -y install epel-release
$ sudo yum -y install ansible

Inventory

まずは通常の Inventory を使うパターンから。

まずは動作確認用の Playbook を用意する。 といっても、実行されることさえ確認できれば良いだけなのでシンプルに。

$ cat << EOF > site.yml
---
- hosts:
  - all
  tasks:
    - name: sample
      debug: msg="{{ message }}"
EOF

次に Inventory を用意する。 sample グループの中に localhost ホストを作った。 localhost なのでコネクションには ssh を使わず local で実行する。

$ cat << EOF > hosts 
[sample]
localhost ansible_connection=local

[sample:vars]
message=Hello, World!
EOF

動作確認

通常の手順通り実行すれば上手くいく。

$ ansible-playbook -i hosts site.yml -v

PLAY [all] ******************************************************************** 

GATHERING FACTS *************************************************************** 
ok: [localhost]

TASK: [sample] **************************************************************** 
ok: [localhost] => {
    "msg": "Hello, World!"
}

PLAY RECAP ******************************************************************** 
localhost                  : ok=2    changed=0    unreachable=0    failed=0   

Dynamic Inventory

通常の Inventory の動作が確認できたので、次は上記を Dynamic Inventory 化してみる。

Dynamic Inventory は実行可能ファイルとして作成する。 その際、言語は特に問わない。 ただ、API としての取り決めが幾つかある。 まず、入力に関してはホストの名前一覧を取得する '--list' を引数にした実行と、更にそこで得たホスト名を元にホスト毎の変数を取得する '--host {hostname}' を引数にした実行が行われる。 そして、出力に関しては所定のフォーマットに沿った JSON 文字列になっている必要がある。

それでは、先ほど作った Inventory を Dynamic Inventory にしてみる。

$ cat << EOF > hosts.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import absolute_import
from __future__ import print_function

import sys
import json


hosts = {}
hosts_vars = {}

hosts['sample'] = {
    'hosts': [
        'localhost',
    ]
}

hosts_vars['localhost'] = {
    'ansible_connection': 'local',
    'message': 'Hello, World!'
}

if sys.argv[1] == '--list':
    hosts_json = json.dumps(hosts)
    print(hosts_json)
else:
    hostname = sys.argv[2]
    hosts_vars_json = json.dumps(hosts_vars[hostname])
    print(hosts_vars_json)
EOF

Dynamic Inventory には実行権限を付与する。

$ chmod +x hosts.py

動作確認

まずは、出力される JSON 文字列を確認しておく。 前述した通り、引数には '--list' が付くパターンと '--host {hostname}' が付くパターンがある。 '--list' が付くパターンでは各グループに所属しているホスト名を 'hosts' 以下に羅列する。 '--host {hostname}' のパターンではホストで使用する変数を返す。 ホスト単位ということは Inventory を使うよりも細かい単位で変数を制御できるみたい。

$ ./hosts.py --list
{"sample": {"hosts": ["localhost"]}}
$ ./hosts.py --host localhost
{"message": "Hello, World!", "ansible_connection": "local"}

動作確認

実行する際に変わるところは -i で指定する Inventory を先ほど作成した hosts.py にするところだけ。

$ ansible-playbook -i hosts.py site.yml -v

PLAY [all] ******************************************************************** 

GATHERING FACTS *************************************************************** 
ok: [localhost]

TASK: [sample] **************************************************************** 
ok: [localhost] => {
    "msg": "Hello, World!"
}

PLAY RECAP ******************************************************************** 
localhost                  : ok=2    changed=0    unreachable=0    failed=0   

ばっちり動いた。

まとめ

今回は最もシンプルな形で Dynamic Inventory を試してみた。 実際に Dynamic Inventory が必要になるのは、ホストの情報が別の場所で集中管理されているパターンになるはず。 それは例えば IaaS を使っていたり、ホストの台帳が別の場所で管理されていたり、あるいは監視システムに登録済みのホストを取ってきたい場合とかとか。 むしろ、そういったパターン以外で使うと無用に複雑化してイマイチかな。