morikawa

久しぶりの投稿になります。インフラグループの Morikawa です。
Labs の執筆推進のメンバーなのですがここ数ヶ月更新が滞っておりすいません。

前回の kamo さんの記事投稿で昨年 6 月頃にスタートした Labs 再開プロジェクトが一周しまして、また次の一周をスタートする準備を進めているところです。

今回は再び Zabbix による監視ネタになります。大したものではないのですが、意外に検索しても見つからない情報だったので役に立つ方には役に立つのではないかと。

今回監視したいのは 特定の時間が経過したプロセスの数 です。デーモンなど長期間動作していて当然のプロセスは問題ないのですが、バッチ系の処理を行っているサーバ上で、そのバッチ処理プロセスが残ったままになってしまう、という経験はおありの方もいるのではないでしょうか。もちろんバッチ処理を見なおして残ったままにしないよう改修するのが根本的な良い解決方法となりますが、とりあえずそういった状況を自動検知したいというニーズはあるかと思います。今回はそれを Zabbix にやらせようという話です。

とりあえずサクッと導入できちゃう設定編と、その内容の解説編とに分けて紹介します。

設定編

監視されるサーバ側での設定

Zabbix Agent の設定ファイルである zabbix_agentd.conf か、もしくはそのファイルからインクルードされるファイルなどに以下の UserParameter を記載しましょう。ちなみにこれの動作確認は CentOS で行ってます。

UserParameter=custom.proc.elapsed.num[*],/bin/ps -eo user:32,etime,command | grep -F "$3" | awk '$(1) == "$2" || "$2" == "" {print $(2)}' | sed 's/:\|-/ /g;' | awk '{print $(4)" "$(3)" "$(2)" "$(1)}' | awk 'BEGIN { x=0 ; if ("$1" == "") {min=-1} else {min="$1"+0} } {if ($(1)+$(2)*60+$(3)*3600+$(4)*86400 > min) x+=1 } END {print x}'

Zabbix Server 側でのアイテムの設定

Zabbix Server 側のアイテムの設定では、キーに以下を設定します。

custom.proc.elapsed.num[<ELAPSED_SEC>,<USER>,<CMDLINE>]

ELAPSED_SEC がプロセスの経過時間、USER がプロセスの実行ユーザ、CMDLINE がコマンドラインに含まれる文字列となり、最初の ELAPSED_SEC で指定する秒数よりも長く動作しているプロセスの数が整数値として得られます。

具体的には以下のようになりますね。root で起動していて “sshd:” という文字列を含み 86400 秒以上経過したプロセスの数が取得されます。

custom.proc.elapsed.num[86400,root,"sshd:"]

ちなみに ELAPSED_SEC, USER, CMDLINE はそれぞれ省略可能で、ELAPSED_SEC は -1 秒以上 (つまり全てのプロセスが対象)、USER は全てのユーザが対象、CMDLINE はあらゆるコマンドを対象とすることになります。

Zabbix Server 側でのトリガーの設定

後はトリガーの設定さえ行えば自動検知が可能になります。閾値は対象に応じて様々ですが、例えば上記の例に出したアイテムで取得した数が 1 以上の場合にアラートを上げたいのであれば、トリガーには以下のように設定すれば OK です。

{custom.proc.elapsed.num[86400,root,"sshd:"].last(0)}>0

以上で設定は完了です。

解説編

さて、こちらは特に UserParameter の内容についての解説編になります。上記で UserParameter で指定した内容を改行で見やすく表示してみます。

UserParameter=custom.proc.elapsed.num[*],
  /bin/ps -eo user:32,etime,command 
  | grep -F "$3" 
  | awk '$(1) == "$2" || "$2" == "" {print $(2)}' 
  | sed 's/:\|-/ /g;' 
  | awk '{print $(4)" "$(3)" "$(2)" "$(1)}' 
  | awk '
    BEGIN 
    { 
      x=0 ; 
      if ("$1" == "") {
        min=-1
      } else {
        min="$1"+0
      } 
    } 
    {
      if ($(1)+$(2)*60+$(3)*3600+$(4)*86400 > min) x+=1 
    } 
    END 
    {
      print x
    }'

ps コマンドに上記オプション -eo user:32,etime,command をつけると、以下の様なフォーマットでプロセス情報が取得されます。

apache     5-17:24:45 /usr/sbin/httpd
root     120-02:31:51 smbd
postfix         52:05 pickup -l -t fifo -u

grep で CMDLINE に指定した内容で絞込み、その次の awk で USER で指定した内容で絞り込んで、時間のみ表示させて次に渡します。なお、 USER を省略した際に全てのユーザを対象にするように || “$2” == “” が入ってます。

次に sed で時刻部分の “-” および “:” を空白にすることで、例えば上記の出力は以下のようになります。

  5 17 24 45
120 02 31 51
       52 05

それを awk の “{print $(4)” “$(3)” “$(2)” “$(1)}” で左右をひっくり返して表示させます。

45 24 17   5
51 31 02 120
05 52

最後に、awk の “$(1)+$(2)*60+$(3)*3600+$(4)*86400” で、秒数に置き換えます。空文字は awk は 0 扱いするため、結果は正しくなります。後はその置き換えた数値と ELAPSED_SEC を比較し、前者のほうが大きいものをカウントして数値として返すという挙動になります。BEGIN の中身は、主に ELAPSED_SEC 省略時に文法エラーを生じさせないための処理です。

ちなみに、このひっくり返してから秒・分・時・日を秒に換算するという計算方法はもちろん(?)私のアイディアではなく、Parse ps etime output into seconds にて紹介されていた内容です。見た時は目からウロコでした。

なお、気になる方は気になったかと思いますが、一般に awk では引数を $1, $2, … と表現しますが、$(1), $(2) としていますね。これは $1, $2 が Zabbix の UserParameter の引数としても扱われるため、普通に $1 などと書くとそちらで置換されてしまうためです。awk の引数は Zabbix 側に置換されない $(1) という表記にしています。

まとめ

さて、今回は簡単でしたが、経過時間で判定させたプロセス数の監視について記事にしてみました。awk、grep、sed などと監視ツールを組み合わせるとかなり柔軟に何でも監視できちゃいますね。要望がどれくらいあるかは分かりませんが、お役に立てば幸いです。

ちょっと早いですが、皆さん良いお年を!

Tags: , , ,

kamo

AnsibleLogo_transparent_web

はじめまして、入社1年と数ヶ月のkamoです。初投稿になります。

最近、巷で噂のRaspberry Pi B+を買ってしまいました。

え?この記事を書かずにそんなことしてん?何してんの? 他にもやることあるよね、kamo?
という心の声が聞こえてきましたが、学生の頃、テスト期間中に勉強しなければいけないのに、机の上や部屋の掃除をしてしまう感じで買ってしまいました。すみません。
Raspberry Pi B+で何をするかは、まだ考えていません。色々と片付いたら弄ろうと思っていますので、ご勘弁を。

さて、最近はサーバを構成管理ツールで管理するのが流行っていますが、トライコーンでは Ansible を使っての構成管理を試してみています。

その中でハマったのが shellcommand モジュールを使いながら冪等性を保つ方法です。VagrantとAnsibleで開発環境を構築 – テストとハマったこと でも紹介されていますが、これらのモジュールは普通に使っていると冪等性を保った挙動をしてくれません。

もちろん用意されているモジュールで事足りれば良いのですが、そうでない場合も多々あります。

例えば、Linux で Windows とファイル共有などする場合に重宝する samba を構築する場合を考えてみます。

ざっくり Playbook は以下のようになります。(smb.conf の変更なども実際には必要ですが割愛しています)。shell モジュールはユーザの追加の際に必要になります。コマンド自体は pdbedit を用い、-t オプションを使うことでパスワードを標準入力から読み込むようにしてワンライナーで書いています。

- name: be sure samba is installed
  yum: pkg=samba4,samba4-common,samba4-client state=latest

- name: Linux User Setting for samba
  user: name=smbuser

- name: Add samba user
  shell: echo -e "passwd\npasswd\n" | pdbedit -t -a smbuser

ではこの Playbook を用いて ansible-playbook を実行してみましょう。

$ ansible-playbook -v -i inventory site.yml
PLAY [servers] ************************************************************

GATHERING FACTS ***************************************************************
ok: [samba-server]

TASK: [servers | be sure samba is installed] ******************************
changed: [samba-server] => {"changed": true, "item": "",
      "msg": "", "rc": 0, "results": ["Loaded plugins: fastestmirror,
      presto\nLoading mirror speeds from cached ...
      \n\nComplete!\n"]}

TASK: [servers | Linux User Setting for samba] ****************************
changed: [samba-server] => {"changed": true, "comment": "",
      "createhome": true, "group": 1000, "home": "/home/smbuser", "item": "",
      "name": "smbuser", "shell": "/bin/bash", "state": "present", "stderr": ...
      "system": false, "uid": 1000}

TASK: [Add samba user] ****************************************
changed: [samba-server] => {"changed": true,
      "cmd": "echo -e \"passwd\\npasswd\\n\" | pdbedit -t -a smbuser ",
      "delta": "0:00:00.046766", "end": "2014-10-10 17:24:40.143402", "item": "",
      "rc": 0, "start": "2014-10-10 17:24:40.096636", "stderr": "",
      "stdout": ... }

PLAY RECAP ********************************************************************
samba-server: ok=0    changed=3    unreachable=0    failed=0

無事に samba を利用するためのパッケージのインストールと samba でのアクセス用ユーザ smbuser が作成されたようです。確認してみましょう。

[root@samba-server ~]# rpm -qa | grep samba
samba4-common
samba4-libs
samba4
samba4

[root@samba-server ~]# pdbedit -L
smbuser:1000:

インストールとユーザの作成、無事完了していますね。

さて次に、ansible-playbook コマンドを再度実行してみましょう。
本来 Ansible に期待するのは、冪等性が保たれて何も実行されない、つまり ok=3, change=0 となることです。

$ ansible-playbook -v -i inventory site.yml
PLAY [servers] ************************************************************

GATHERING FACTS ***************************************************************
ok: [samba-server]

TASK: [servers | be sure samba is installed] ******************************
ok: [samba-server] => {"changed": false, "item": "", "msg": "",
      "rc": 0, "results": ["All packages providing samba4 are up to date",
      "All packages providing samba4-common are up to date",
      "All packages providing samba4-client are up to date"]}

TASK: [servers | Linux User Setting for samba] ****************************
ok: [samba-server] => {"append": false, "changed": false, "comment": "",
      "group": 1000, "home": "/home/smbuser", "item": "", "move_home": false, 
      "name": "smbuser", "shell": "/bin/bash", "state": "present", "uid": 1000}

TASK: [servers | Add samba user] ****************************************
changed: [samba-server] => {"changed": true,
      "cmd": "echo -e \"passwd\\npasswd\\n\" | pdbedit -t -a smbuser ",
      "delta": "0:00:00.046766", "end": "2014-10-10 17:24:40.143402",
      "item": "", "rc": 0, "start": "2014-10-10 17:24:40.096636",
      "stderr": "", "stdout": ... }

PLAY RECAP ********************************************************************
samba-server: ok=2    changed=1    unreachable=0    failed=0

おや、おかしいですね。changed=1 が表示されてしまいました。まあ当然は当然で、shell や command モジュールは検査の機構が無く、現状と結果の差を確認せずにコマンドを実行してしまうためです。

pdbedit での同名のユーザを追加しているだけなので実質的には実行前後で同じではあるのですが、本来調べればわかるのに pdbedit で実際にユーザ追加のコマンド叩いちゃってますし、changed=1 って表示もされちゃっているので、Ansible で期待される冪等性は保てていないと言って良いでしょう。

この Playbook を単体で使っているだけであれば「ああ分かってることだし」とスルーもできますが、多数の Playbook からなるインストール処理を行っている際、本来 changed=0 が期待される所、shell や command モジュールを使った箇所の数だけ changed が出てしまうのはやっぱり微妙ですね。

要するに条件判定を自作すればいいわけなのですが、以下のようなタスクを追加すればそれが実現できます。

- name: Samba User List
  shell: pdbedit -L | awk -F":" '{print $1}'
  register: samba_user_list
  changed_when: false
  always_run: yes

実行すると以下のような結果が得られます。

TASK: [servers | Samba User List] *****************************************
ok: [samba-server] => {"changed": false,
      "cmd": "pdbedit -L | awk -F\":\" '{print $1}' ", 
      "delta": "0:00:00.021991", "end": "2014-10-10 17:24:39.250869", 
      "item": "", "rc": 0, "start": "2014-10-10 17:24:39.228878", 
      "stderr": "", "stdout": "smbuser", "stdout_lines":["smbuser"]}

“always_run: yes” のため常に実行され、”changed_when: false” のため常に ok と表示されるようになっています。”register: samba_user_list” によって、pdbedit -L | awk -F":" '{print $1}'の結果が、samba_user_list.stdout (テキスト形式) および samba_user_list.stdout_lines (配列形式) に格納されます。

これを利用して Add samba user タスクを書き直してみましょう。

- name: be sure samba is installed
  yum: pkg=samba4,samba4-common,samba4-client state=latest

- name: Linux User Setting for samba
  user: name=smbuser

- name: Samba User List
  shell: pdbedit -L | awk -F":" '{print $1}'
  register: samba_user_list
  changed_when: false
  always_run: yes

- name: Add samba user
  shell: echo -e "passwd\npasswd\n" | pdbedit -t -a smbuser
  when: samba_user_list.stdout_lines.count("smbuser") < 1

最後の行に、配列 samba_user_list.stdout_lines の中から “smbuser” の数を数え、これが 1 以下 (要するに見つからなかった場合) に実行するよう条件分岐が追加されました。

では再度 ansible-playbook を実行してみましょう。

$ ansible-playbook -v -i inventory site.yml
PLAY [servers] ************************************************************

GATHERING FACTS ***************************************************************
ok: [samba-server]

TASK: [servers | be sure samba is installed] ******************************
ok: [samba-server] => {"changed": false, "item": "", "msg": "",
      "rc": 0, "results": ["All packages providing samba4 are up to date",
      "All packages providing samba4-common are up to date",
      "All packages providing samba4-client are up to date"]}

TASK: [servers | Linux User Setting for samba] ****************************
ok: [samba-server] => {"append": false, "changed": false, "comment": "",
      "group": 1000, "home": "/home/smbuser", "item": "", "move_home": false, 
      "name": "smbuser", "shell": "/bin/bash", "state": "present", "uid": 1000}

TASK: [servers | Samba User List] *****************************************
ok: [samba-server] => {"changed": false,
      "cmd": "pdbedit -L | awk -F\":\" '{print $1}' ", 
      "delta": "0:00:00.021991", "end": "2014-10-10 17:24:39.250869", 
      "item": "", "rc": 0, "start": "2014-10-10 17:24:39.228878", 
      "stderr": "", "stdout": "smbuser", "stdout_lines":["smbuser"]}

TASK: [servers | Add samba user] ****************************************
skipping: [samba-server]

PLAY RECAP ********************************************************************
samba-server: ok=3    changed=0    unreachable=0    failed=0

Add samba user のタスクが skipping となっている事がわかります。残念ながら skipping のタスクは ok のカウントには入りませんが、changed=0 にはなっていることが分かりますね。また不要な pdbedit によるユーザ追加も実施されていません。もちろん “smbuser” ユーザが未作成の場合はユーザの作成が行われ、changed のカウントが一つ増えます。

このように、一手間加えれば shell や command モジュールでも冪等性を保った Playbook を書くことが可能です。いかがでしょうか? 今回は samba を例に紹介しましたが iptables の設定であったりその他いろいろ応用が効くやり方かと思います。

さて、本題は以上なのですが、実際にこのようなタスクを書くにあたり、register で指定した変数にどのような値が格納されているのかが気になりますね。その場合は是非 debug モジュールを使ってみてください。Samba User List タスクの下に以下のような debug モジュールタスクを追加すると、変数の内容が表示されます。

- debug: var=samba_user_list

ansible-playbook を実行すると以下のように表示されます。

【”smbuser” ユーザ作成前】

TASK: [servers | debug var=samba_user_list] *******************************
ok: [samba-server] => {
    "item": "",
    "samba_user_list": {
        "changed": false,
        "cmd": "pdbedit -L | awk -F\":\" '{print $1}' ",
        "delta": "0:00:00.024101",
        "end": "2014-10-12 13:20:44.921479",
        "invocation": {
            "module_args": "pdbedit -L | awk -F\":\" '{print $1}'",
            "module_name": "shell"
        },
        "item": "",
        "rc": 0,
        "start": "2014-10-12 13:20:44.897378",
        "stderr": "",
        "stdout": "",
        "stdout_lines": []
    }
}

【”smbuser” ユーザ作成後】

TASK: [servers | debug var=samba_user_list] *******************************
ok: [samba-server] => {
    "item": "",
    "samba_user_list": {
        "changed": false,
        "cmd": "pdbedit -L | awk -F\":\" '{print $1}' ",
        "delta": "0:00:00.024101",
        "end": "2014-10-12 13:20:44.921479",
        "invocation": {
            "module_args": "pdbedit -L | awk -F\":\" '{print $1}'",
            "module_name": "shell"
        },
        "item": "",
        "rc": 0,
        "start": "2014-10-12 13:20:44.897378",
        "stderr": "",
        "stdout": "smbuser",
        "stdout_lines": [
            "smbuser"
        ]
    }
}

黄色で記載した部分が差分になります (見やすさのためにここでそのように色分けしただけで、ansible-playbook 実行時にこのような色で表示されるわけではありません)。このように実際に取得する情報の前後の差分を確認することで、実際に何を判定に用いればよいかが分かりますね。

なお、Ansible ドキュメントの Conditionals (条件分岐) では、register した文字型の変数に対して find 関数を使っていますが、find は対象の文字列に対して特定の文字列が含まれているかどうかを判定するものであるため、今回の場合だと仮に “smbuser2” というユーザがいると “smbuser” というユーザは作らないことになります。

配列に対して使用する count 関数は配列内から完全に一致するもののみの数を返してくれるため、今回はこちらを採用しました。

これらの関数は Ansible というよりは Python の関数なので、条件判定などをより細かく行いたい場合は Python の関数をいろいろ調べてみると良いのかもしれません。

一番便利なのは正規表現でマッチさせる方法なのですがその方法は調べてみましたがわかりませんでした。Ansible 内の register で保存した変数に対して正規表現でマッチをかける方法をご存じの方は教えて頂けると幸いです。

記事が書き終わりましたので冒頭に書きました Raspberry Pi B+ をどうしようかとさっそく考えてます。
家電をスマホで操作できるようにしようか、消費電力の少ない音楽配信サーバにしようか、
うちのお犬様がケージから脱出する姿を世の中に披露しようか。。。
悩んでるだけで、何もしなくなりそうなので、次の記事にRaspberry Piを掲載できるようにいじり倒します。

Tags: , , ,