Zabbixの通知をLINEに送る

背景

ちょっと前にLINE NotifyというLINEのサービスが話題になりました。 developers.linecorp.com

これを使うと非常に簡単にシステムからLINEにメッセージを送れるのですが、Zabbixのような監視サーバのアラートをLINEに送るっていう使い方は結構需要があるのではないかと思い、LINE Notifyを使ってZabbixのアラートを送るalertscriptを実装してみました。

前準備

最初にLINE Notifyの設定をする必要があります。
下記のページにアクセスし、LINEのIDでログインすると「トークンを発行する」ボタンがあります。
https://notify-bot.line.me/my/

トークンを取得し、どこかにメモしておいてください。

Zabbixの設定

詳しく知りたい方は公式ドキュメントを参照してください。 5 Custom alertscripts [Zabbix Documentation 3.0]

alertscriptの配置

トークンとメッセージのタイトル、メッセージの内容を引数に取るスクリプトを作成し、/usr/lib/zabbix/alertscriptsに配置します。
LINE Notifyとの通信にはcurlを使います。詳しくは以下のサイトを参照してください。

以下はスクリプトのサンプルです。
curlからのレスポンスをjqで読み取って終了ステータスをきちんと返すようにしましたが、Zabbixは終了ステータスを見てくれないみたいです。。

#!/bin/bash

token="$1"
subject="$2"
body="$3"

LINE_API_URI='https://notify-api.line.me/api/notify'
LOG_FILE=${0%.*}.log

function write-log() {
  echo "$(date '+%Y/%m/%d %H:%M:%S') $1" | tee -a ${LOG_FILE}
}

write-log "token=${token}"
write-log "subject=${subject}"
write-log "body=${body}"

if [ -n "${subject}" ] && [ -n "${body}" ]; then
  message=\
"${subject}

${body}"
elif [ -n "${subject}" ] || [ -n "${body}" ]; then
  message="${subject}${body}"
else
  exit 1
fi

cmd="curl -X POST -H 'Authorization: Bearer ${token}' -F 'message=${message}' ${LINE_API_URI}"
write-log "executing: ${cmd}"

response=$(eval "${cmd}" | jq -Mc ". +  {"exit_code": $?}")
write-log "response: ${response}"

exit_code=$(echo "${response}" | jq -Mc '.exit_code')
response_status=$(echo "${response}" | jq -Mc '.status')
response_message=$(echo "${response}" | jq -Mc '.message')

if [ ${exit_code} -gt 0 ]; then
  exit ${exit_code}
fi

if [ ${response_status} -ne 200 ]; then
  echo "status : ${response_status}"
  echo "message: ${response_message}"
  exit 1
fi

exit 0

メディアタイプにカスタムスクリプトを登録

  1. Zabbixにブラウザでアクセスし、[管理] - [メディアタイプ] を開き、[メディアタイプの作成]をクリックします。
  2. 以下のように項目を入力し、[追加]ボタンを押します。
項目 入力値 説明
名前 LINE Notify メディアタイプの名前
タイプ スクリプト
スクリプト line_notify.sh alertscriptフォルダ配下のスクリプトのファイル名
スクリプトパラメータ {ALERT.SENDTO} 第一引数。ユーザ設定で送信先に指定した値が入る。
{ALERT.SUBJECT} 第二引数。メッセージのタイトル。
{ALERT.MESSAGE} 第三引数。メッセージの内容。

ユーザーの設定

  1. Zabbixで[管理] - [ユーザー]を開き、LINE Notifyで通知を送りたいユーザーの設定画面を開きます。
  2. [メディア]タブを開き、[追加]をクリックします。
  3. [タイプ]に新しく追加したメディアタイプの名前を指定します。
    [送信先]にLINE Notifyのトークンを指定します。 その他の項目は適宜設定してください。
  4. 更新ボタンをクリックします。

後はアクション等の設定が入っていれば、これでLINEに通知が行くようになっているはずです。

まとめ

ZabbixのアラートをLINEに飛ばす方法でした。
私が以前勤めていた会社では携帯にメールで飛ばす感じだったんですが、今の世の中だとLINEに送ったほうがやりやすい職場も多いのではないかと思います。そのうちLINE Business Centerを使ったやり方も試してみたいです。

シェルスクリプトでパスワードを暗号化して保持する

背景

先日バックアップのスクリプトを書いていたときにバックアップ先サーバのログイン情報の置き方に困ったので、平文で置かなくても良いやりかたを探したところ、下記のページがヒットしました。 tkuchiki.hatenablog.com

(暗号化には全然詳しくないんですが)なんかこいつ弱そうなのでもっとちゃんとしたやり方をするべきなんですが、暗号化でバイナリじゃなくて文字列が出て来るところがシェルスクリプト的に大変扱いやすいので、平文で置いておくよりはマシだろうということで採用しました。

暗号化・復号化

リンク先にあった通りなんですが下記のようにして暗号化、復号化ができます。

# 暗号化
echo "$plain_password" | openssl enc -e -des -base64 -k "$pass_phrase"

# 復号化
echo "$encrypted_password" | openssl enc -d -des -base64 -k "$pass_phrase"

パスフレーズをどうするかという話

実際にシェルスクリプトを作って運用するときにはパスフレーズを何にするかが重要です。ジョブ管理サーバからスクリプトを転送して実行するような場合を考えて、

  • スクリプトが実行されるサーバに固有の情報で、
  • 簡単に変わることがなく、
  • 外部から読み取ることができない

という3つを条件として考えます。sshか何かでサーバに入られたらそもそも仕方がないので、とりあえずこれでいいんじゃないでしょうか。

最初に思いついたのがMACアドレスなんですが、同じセグメントに侵入できていれば見えちゃうのでボツでした。。
ホスト名は結構いい線いっていると思うんですが、簡単すぎる。いろいろ調べてたらdmidecodeというコマンドに行き着きました。

kanjuku-tomato.blogspot.jp

system-uuidというのは使えそうな感じなので、これとdmidecodeから取れる他の値やホスト名とかを組み合わせたら、そこそこ強力なパスフレーズになるのではないでしょうか。 とりあえず現状は以下のようにしています。

#!/bin/bash
encrypt() {
  echo "$1" | openssl enc -e -des -base64 -k "$(hostname)$(dmidecode -s system-uuid)"
}

decrypt() {
  echo "$1" | openssl enc -d -des -base64 -k "$(hostname)$(dmidecode -s system-uuid)"
}

これで、(脆弱性を突かれない限り)スクリプトを実行したいサーバ以外の場所では見えない形でパスワードを書くことができるはずです。まあ今回の私の場合は公開鍵認証でsftpとかrsyncを使うべき案件っていう気がしてますが。

ただ、暗号の強度がどの程度のものかよく分からないのが不安です。詳しい方いたら教えてもらえると嬉しいです。

おまけ

そもそも、rsync+sshでパスワード入力が必要になるっていうところから調査を始めたのですが、対話の自動化にはsshpassを使いました。

sshpassを使ってパスワード指定のSSH接続を行ってみる(パスワード指定自動ログイン) | レンタルサーバー・自宅サーバー設定・構築のヒント

これがあればexpectを使ってごちゃごちゃやる必要はないかもしれません。
とりあえず以下の使い方ではちゃんと動きました。

sshpass -p $password ssh user@host 'echo "$(hostname) is up"'

sshpass -p $password rsync /local/directory/ user@host:/remote/directory/

sshpass -p $password sftp user@host << EOF
put /path/to/local/file
EOF

シェルスクリプトの設定ファイルをyamlで書く

背景

今までシェルスクリプトの設定ファイルは変数の定義を別ファイルに外出しして、sourceで読んでいました。↓みたいな感じ。

SRC='/path/to/src/directory/'
DEST='/path/to/dest/directory/'
#!/bin/bash
source variables.conf
rsync -av ${SRC} ${DEST}

多分かなり一般的な方法だと思うんだけど、これだと設定に自由度出そうと思うと設定ファイルの書き方が難しくなるし、設定ファイルから任意のコマンドが実行できちゃうしと色々気に入らない点がありました。 最近jqというコマンドを覚えて、設定をjsonで渡してjqで処理したら設定の自由度も出るし内部でも扱いやすいなと思っていたんですが、jsonは設定ファイルに向いてないのでどうしたものかと。色々探していたらpythonワンライナーyaml -> jsonの変換ができることが判明したので試してみました。

jqに関しては以下のサイトがわかりやすいです。

yaml -> jsonの変換は以下のサイトにあったコードを丸パクリしました。

Converting YAML to JSON with Python: <block end> found - Stack Overflow

前提

以下のパッケージがインストールされている必要があります。好きにパッケージを入れられる環境ばかりだと良いんですが。。

yamlで書いた設定ファイルを読んで処理を流す

- name: backup1
  src: /path/to/src/dir/
  dest: user@host:/path/to/dest/dir/

- name: backup2
  src: /another/src/dir/
  dest: user@host:/another/dest/dir/

上記のようなyamlをもとにrsyncする処理は以下のように書けます。

#!/bin/bash
# yamlで書かれた設定ファイルをもとにjsonファイルを作成
python -c 'import sys, yaml, json; json.dump(yaml.load(sys.stdin), sys.stdout, indent=2)' < config.yml > config.json

# jsonを読み込んで変数に格納
config=$(jq -Mc '.' config.json)

# for文で順次バックアップ処理を実行
num_backup=$(echo "${config}" | jq '. | length')
for ((i=0; i < ${num_backup}; i++)); do
  # パラメータを取り出して変数に格納
  name=$(echo "${config}" | jq -r ".[$i].name | select(. != null)")
  src=$(echo "${config}" | jq -r ".[$i].src | select(. != null)")
  dest=$(echo "${config}" | jq -r ".[$i].dest | select(. != null)")

  # パラメータのチェック(ここでは値が入っている確認のみ)
  if [ -z "${name}" ] || [ -z "${src}" ] || [ -z "${dest}" ]; then
    continue
  fi

  echo "starting ${name}"
  rsync rsync -av --copy-unsafe-links ${src} ${dest}
done

とりあえず以上でyamlの設定ファイルを読んで処理が回せます。。

解説

上記のyamljsonにすると以下のような感じです。

[
  {
    "dest": "user@host:/path/to/dest/dir/",
    "src": "/path/to/src/dir/",
    "name": "backup1"
  },
  {
    "dest": "user@host:/another/dest/dir/",
    "src": "/another/src/dir/",
    "name": "backup2"
  }
]

連想配列の配列ですね。プログラム的に扱いやすいデータが人に優しく書けることが分かると思います。以下でjsonにしたあとの処理を解説します。

jqを雑に説明

jqコマンドでは、keyを指定して値を取り出します。keyは階層構造になっていて、.がルートで以降.で区切りながらkeyを記述していく感じになっています。また、配列に対しては[<index>]でアクセスできます。 例として、以下のようなjsonを考えます。

{"manager" : {"name" : "太郎", "tel" : "####"}, "members" : [{"name" : "次郎"}, {"name" : "三郎"}]}
  • managerのnameが欲しいときには識別子は.manager.nameとなります。
  • 2人目のmemberのnameが欲しいときには.members[1].nameです。
echo '{"manager" : {"name" : "太郎", "tel" : "####"}, "members" : [{"name" : "次郎"}, {"name" : "三郎"}]}' | jq '<識別子>'

とすると確認できます。

スクリプトの中のjqの使い方

以上を踏まえて、上のシェルスクリプトの例を見ていきます。

  1. jq -Mc '.' config.jsonとすると、jsonがコンパクトに一行になって標準出力に出てくるので、これを変数に格納します。複数行の文字列ってシェルだとなんか扱いにくいので。
  2. jqのコマンドの中で、オブジェクトをパイプで繋いでstream的な処理が書けます。. | lengthでは取り出した配列をlengthに渡して、配列の長さを取ってきています。
  3. .[$i].name | select(. != null)では、.で出てくる配列から$i番目の連想配列を取り出し、さらにnameを取ったあとで、それをselectに渡してnullの場合をはじいています。nameが空だと、nullっていう文字列が返ってきちゃうので。

まとめ

シェルスクリプトを書くときに設定ファイルの作りや内部でのパラメータの管理がいまいちやりにくくてストレスなので、yamlでお手軽に設定ファイルを書きつつjqで設定を参照するようにすると捗るんじゃないかというお話でした。

今のところjqみたいに便利にyamlを読む方法がなくてjsonに変換しないといけないのが難点です。何かいいやり方知っている人がいたら教えてもらえるとうれしいです。