あきろぐ

いろいろめもするよ🐈🐈🐈

RubyでZabbixアラートをSlackに通知する

概要

Rubyの勉強をかねて、今までシェルスクリプトでZabbixのアラート通知をしていたものをRubyで書き換えてみました。
設定方法とつまずいたところをまとめたいと思います。

環境

  • CentOS7.6
  • zabbix4.0
  • Ruby2.4

設定方法

slackのWebhook URLを取得する

通知したいチャンネルのWebhook URLをまず取得します。
対象のワークスペースの対象チャンネルにIncoming Webhookインテグレーションを追加します。
f:id:akngo22:20191030161303j:plain
表示されたWebhook URLをコピーし、スクリプト内でURLを指定します。

zabbixアラート用スクリプトを作成する

今回は、"zabbix_notify_to_slack.rb"というスクリプトを作成しました。
作成したスクリプトは、githubに置いています。
github.com

zabbixアラート通知用ディレクトリにスクリプトを移動する

zabbixでは、アラートがあがったときに対象のディレクトリにスクリプトを配置しておくと自動的に実行されるので、対象ディレクトリを調べて移動させます。
Zabbixアラート用ディレクトリは、以下のように設定ファイルに書かれているので確認します。

# zabbixアラート用ディレクトリパスを確認
$ sudo cat /etc/zabbix/zabbix_server.conf | grep alertscripts
# AlertScriptsPath=${datadir}/zabbix/alertscripts
AlertScriptsPath=/usr/lib/zabbix/alertscripts
# 対象ディレクトリに移動
$ mv zabbix_notify_to_slack.rb /usr/lib/zabbix/alertscripts
# 忘れずにスクリプトに実行権限をつける
$ chmod +x /usr/lib/zabbix/alertscripts/zabbix_notify_to_slack.rb

zabbixで対象スクリプトが実行されるように設定する

Zabbixにログイン後、[Administration]>[Media Types]>[Create media type]をクリックし、以下のように、対象スクリプトスクリプトのパラメータを設定します。
f:id:akngo22:20191030145940j:plain

スクリプトパラメータに設定しているのは、Zabbixがサポートしているマクロです。今回は、{ALERT.SENDTO}、{ALERT.SUBJECT}、{ALERT.MESSAGE}の3つを使います。これらは、スクリプトを実行する際に使用するコマンドライン引数になります。
Zabbixのマクロ一覧は下記から確認することができます。
www.zabbix.com

続いて、[Administration]>[Users]>[Admin]をクリックし、メディアを追加します。[Add]をクリックします。
f:id:akngo22:20191030151654j:plain

Typeに登録したメディアタイプを選択し、Send toにはSlackのチャンネル名、アラートを通知したい深刻度にチェックを入れます。今回は、深刻度が[Warning]以上のものをSlackに通知させたいと思います。
f:id:akngo22:20191030151729j:plain

最後にアクションの設定をします。[Configuration]>[Actions]>[Create action]をクリックします。
[Operations]の[Operations]>[New]から
f:id:akngo22:20191030152736j:plain

"Send to User"に先ほどメディアを追加したユーザーを選択し、今回はSlackにのみ通知したいので"Send only to"でslackのメディアを選択しました。
f:id:akngo22:20191030153012j:plain
これで設定完了です!

意図的にZabbixアラートを発生させる

今回、ZabbixのトリガーでCPU使用率が80%超えたらアラートを出すようにテストアラートを設定したので、意図的にCPUの負荷を上げます。yesコマンドでサーバーのCPU負荷が80%超えるようにします。

# CPU負荷を上げる(今回はyesコマンドをバックグラウンドで5回実行することで負荷が80%を超えました)
$ yes > /dev/null &
$ yes > /dev/null &
$ yes > /dev/null &
$ yes > /dev/null &
$ yes > /dev/null &
# cpu使用率を確認する
$ top
# アラートが発生し、slackに通知が飛ぶことを確認したらkillコマンドでジョブを止める
$ kill %1 %2 %3 %4 %5
[1]   Terminated              yes > /dev/null
[2]   Terminated              yes > /dev/null
[3]   Terminated              yes > /dev/null
[4]-  Terminated              yes > /dev/null
[5]+  Terminated              yes > /dev/null
# ジョブが止まっていることを確認する
$ jobs
[1]   Terminated              yes > /dev/null  (wd: /usr/lib/zabbix/alertscripts)
[2]   Terminated              yes > /dev/null  (wd: /usr/lib/zabbix/alertscripts)
[3]   Terminated              yes > /dev/null  (wd: /usr/lib/zabbix/alertscripts)
[4]-  Terminated              yes > /dev/null  (wd: /usr/lib/zabbix/alertscripts)
[5]+  Terminated              yes > /dev/null  (wd: /usr/lib/zabbix/alertscripts)

slackにアラートが飛んでいるか確認する

対象チャンネルに通知が来ているか確認します。ちゃんとアラートが飛んできていればOK!
f:id:akngo22:20191030155122j:plain

つまづいたところ

ちょっとしたミスが多いですが、つまずいたところです。

zabbixにアラートが上がったのに通知が飛ばない

Zabbixで設定したアクションが有効になっているか確認してみましょう。ステータスが"Disabled"だと通知飛ばないのでご注意。
f:id:akngo22:20191030160110j:plain

アクションが実行されているが、失敗している

エラーメッセージを確認したところ、"permission denied"が発生していました。
対象スクリプトの最初に"#!/home/akina/.rbenv/shims/ruby"とインタプリタを指定していますが、Zabbixユーザーだとパーミッションがないよ!と怒られていました。なので、Zabbixユーザーでも実行できるようにパーミッションを変更します。
今回は、/home/akinaパーミッションに実行権限を付与しました。

$ chmod +x /home/akina/

rubyコマンドライン引数を取得する方法を間違えた

今までシェルスクリプトばっかり書いていたので、rubyでも同様に"$1"とか"$2"とか書いていたんですけど、間違いでした。。。rubyでは以下のように記載するんですね。勉強になりました・・・!

# argv[引数番号]でコマンドライン引数を取得できる
notify_channel=ARGV[0]
alert_subject=ARGV[1]
alert_message=ARGV[2]

おしまい!

Ansibleでインベントリに書いたIPアドレスを変数として使いたい②

以前、Ansibleでインベントリに書いたIPアドレスを使って、それを変数として使いたい場合の変数の書き方について記事を書きました。
akng-engineer.hatenablog.com
しかし、上記の書き方だと他のパターンに適用することが難しいので、今回は別の書き方についてまとめます。

何をしたいのか

前回は、インベントリにIPアドレスのみを記載していたのですが、この書き方だと各ホストを識別し各ホストのIPアドレスをリモートサーバー上で展開させるような変数を定義することが困難でした。
今回やりたいことは、playbookで作成したシェルスクリプトに自身のIPアドレスを変数定義し、templateモジュールでリモートサーバー上で変数を展開させるような処理を記述したいので、インベントリにホスト名を記載する書き方で私のやりたいことを実現させます。

環境

  • Ansible2.7
  • CentOS7.6

実現方法

今までの書き方

今までは、インベントリを以下のように記載していました。

[compute]
192.168.10.xx
192.168.10.xx
192.168.10.xx
192.168.10.xx
192.168.10.xx

この書き方だと、どのホストのIPアドレスか識別するような変数の書き方ができません。

今回の書き方

今回は、以下のようにインベントリを書きます。
このようにインベントリを記載し、テンプレート化するシェルスクリプト内では、"{{ ansible_host }}"と記述することで各ホストのIPアドレスをリモートサーバー上で展開することが可能になります。

hosts.ini
[compute]
host01 ansible_host=192.168.10.xx
host02 ansible_host=192.168.10.xx
host03 ansible_host=192.168.10.xx
host04 ansible_host=192.168.10.xx
host05 ansible_host=192.168.10.xx

テンプレート化するシェルスクリプトでは、以下のように変数を記述することでそれぞれのホストのIPアドレスが展開されるようになります。

test-script.sh.j2
host_ip_address={{ ansible_host }}

参考ドキュメントはこちら。
docs.ansible.com

おしまい!

Ansibleでテンプレートエラーが出たときの対処法

概要

今回遭遇したエラーは、テンプレートに関する内容で以下の通り。

- Ansible error: template error while templating string missing end of comment tag.
- Ansible error: template error while templating string expected token end of print statement got '{'.

AnsibleでTemplateモジュールを使って、作成したシェルスクリプトをリモートサーバーに送ろうと思っていたところ、上記のエラーが発生しました。

原因

シェルスクリプトの中で変数以外に"{"を使っていたことが原因でした。
変数以外で"{"を使うと変数が閉じていない、つまり"{}"となっていないからエラーになったり、他には"{#}"と書いていたため、"#"以降がコメントアウトとして認識されてしまったりと、Ansibleのjinjaテンプレートのルールに従ってないからタスクを実行できないよ!とのことでした。
どんな内容のシェルスクリプトを書いてエラーになったか一部抜粋すると、下記の通り。

echo -n $hostname universal.discovery {"data":["{#VCPU}": > $output_file

とか

sed -ie 's/,]}/]}/g' $output_file

とかを書いていてひっかかりました。なんでこのように書いていたかというと、シェルスクリプト内でjsonファイルを作成するために"{"を使っていました。

環境

  • CentOS7.6
  • Ansible2.7

対処法

Ansibleのjinjaテンプレート内で変数以外で"{"を使いたい場合は、エスケープさせる必要があります。
対処法としては、2つあります。

1. エスケープさせたい処理を{{ }}で囲む

例えば、こんな感じで。

{{ echo -n $hostname universal.discovery {"data":["{#VCPU}": > $output_file }}

2. エスケープさせたい処理を{% raw %}~{% endraw %}で囲む

エスケープさせたい処理が複数行にわたるときは、そのセクションを{% raw %}~{% endraw %}で囲みます。
例えば、このように。

{% raw %}
echo -n $hostname universal.discovery {"data":["{#VCPU}": > $output_file 
{% endraw %}

参考にした文献は以下です。
docs.ansible.com
jinja.palletsprojects.com

上記の2つでエラーを回避することはできますが、囲んでいるセクションの中にAnsibleの変数が含まれていると展開されないので、注意しましょう。

おしまい!

Ansibleでインベントリに書いたIPアドレスを変数として使いたい

概要

最近Ansibleを頻繁に使っていて、こういう使い方ないかなと思って調べたのでそのやり方を整理。
やりたいことは、「インベントリファイルにグループ別にIPアドレスを記載しているので、そのIPアドレスを変数として使いgroup_varsディレクトリ配下のグループ別変数ファイルにIPアドレスをべた書きしないようにする」です。

環境

  • CentOS7.6
  • Ansible2.8.5

解決方法

作成したインベントリファイルは、以下の通りです。

[compute]
192.168.13.xx
192.168.13.xx
192.168.13.xx
192.168.13.xx
192.168.13.xx

[zabbix]
192.168.13.xx
192.168.13.xx

computeグループに属するIPアドレス一覧を取得したい

以下のようにgroup_vars配下の変数ファイルを作成します。

compute_ip_list: "{{ groups['compute']|join('/n') }}"

上記のようにjoinを使って書いてTemplateファイルで使用すると、computeグループのIPアドレスが改行された状態でリモートホスト内で展開されます。

例えば、以下のようなtemplateファイル、playbookを作成します。

compute_ip.txt.j2
{{ compute_ip_list }}

playbook_copy_file.yml
---
- hosts: compute
  user: "{{ login_user }}"
  tasks:
     - name: copy ip list
       template:
          src: compute_ip.txt.j2
          dest: "{{ remote_dir }}/compute_ip.txt"

上記のように設定して、Ansibleを実行すると以下のようにリモートホスト内で展開されます。

compute_ip.txt
192.168.13.xx
192.168.13.xx
192.168.13.xx
192.168.13.xx
192.168.13.xx

こちらのサイトを参考にしました。
codeday.me

Zabbixグループの1番目に記載したIPアドレスを取得したい

グループに属するIPアドレスを全て取得する方法は分かったけど、グループに属する1つのIPアドレスを取得したいときはリストを使って変数ファイルに記載します。

# zabbixグループに属する1番目のIPアドレスを取得する
zabbix_ip_1: "{{ groups['zabbix'][0] }}"

このように書けば、グループに属する1つのIPアドレスを指定することができます。
しかし、リスト番号にマッチするものがないとplaybook実行時にエラーを吐くので注意してください。
こちらの解決策は、Ansibleユーザー会のSlackで教えていただきました!(ありがとうございます!)
ansible-users.connpass.com

一応、グループに1つのホストしか属していない場合でも以下の書き方は使うことができます。

compute_ip_list: "{{ groups['compute']|join('/n') }}"

まとめ

  • インベントリファイルのIPアドレスを使って変数を作成できる
  • 例えばjoinメソッドやリストを使うことができる
  • Ansibleおもしろい

おしまい!