動機
サーバで何かが起こればシステム管理者にメールで通知するのが一般的?かと思いますが、家庭内サーバで起きるイベントは家族で共有したいものもあります。イベントの内容を家庭用Slackチームに投稿できれば便利だと思い立ちました。
まず今回は、特定のホストの特定のポートに接続し、接続できなかったら(何か異常があるとして)Slackに投稿してみます。
Node.jsを動かすDockerコンテナを作成する
スクリプト類をNode.jsで記述する前提で、公式リポジトリのnodeイメージを利用してDockerコンテナを作成します。npmで監視と投稿に必要なパッケージをインストールし、Slackの incoming webhook のURIを環境変数として渡しておきます。
FROM node:latest
MAINTAINER flny flny@example.com
RUN npm install node-netcat \
slack-node \
request
ENV WEBHOOK_URI https://hooks.slack.com/services/xxxxxx...
CMD /bin/bash
node.jsで記述するスクリプトはデータボリュームコンテナ(scripts:)に置いて永続化することにしました。
monitor:
image: monitor
container_name: monitor
tty: true
volumes:
- /mnt/pub:/mnt/pub
volumes_from:
- scripts
command: "/bin/bash"
scripts:
image: busybox
container_name: scripts
tty: true
volumes:
- /mnt/scripts
$ docker build -t monitor <Dockerfileの場所>
$ docker-compose up -d monitor
Creating monitor
ポート監視の結果をファイルに書き出す
- ポートスキャンの結果がNGだったら そのレスポンスをファイルに書き込む
- ファイルの内容を読んでSlackに投稿する
の2つにスクリプトを分けました。
前者のscanPort.jsでは、node-netcatのportscanで var portsで指定したポートをスキャンしています。node-netcatのportscanは 必ずtcp/udp両方スキャンするので、udpでエラーが出たときにはそれを無視するようにしています。1
tcpのスキャンでエラーが出たら、system_port_<unix時間>というファイル名でエラーのレスポンス内容を保存します。2
#!/usr/bin/env node
var host = 'example.com';
var ports = ['22', '53', '445', '8200', '80'];
var udpErrorStr = 'protocol \"udp\" with message';
var msgPath = "/mnt/pub/misc/monitor/"
var scanner = require('node-netcat').portscan();
var fs = require('fs');
var msgFile = msgPath + "system_port_" + (new Date()).getTime();
ports.forEach(function(port) {
scanner.run(host, port, function(err, res) {
if(err && err.indexOf(udpErrorStr) == -1) {
fs.appendFile(msgFile, err + "\n");
}
});
});
ファイルの内容を読んで Slackに投稿する
scanPort.jsがファイルに出力したエラーの内容を、sendSlack.jsがSlackに投稿します。
- 特定のディレクトリ(/mnt/pub/misc/monitor/)にある ファイル名が"system_port_" で始まるファイルを列挙する
- ファイルの中身を読んで webhookのデフォルトチャネルに アイコンで投稿する
- うまくいったら ファイルを投稿済みディレクトリ(/mnt/pub/misc/monitor/done/)に移動する
という流れです。slack-nodeのおかげで投稿部分はシンプルに記述できますが、全体として可読性が今ひとつ...。js力が低いのでもっとがんばる必要がありますね。
#!/usr/bin/env node
var msgPath = "/mnt/pub/misc/monitor/"
var donePath = "/mnt/pub/misc/monitor/done/"
var filePrefix = "system_port_"
var SlackNode = require('slack-node');
var slack = new SlackNode();
slack.setWebhook(process.env.WEBHOOK_URI);
var fs = require('fs');
fs.readdir(msgPath, function(err, files) {
if(err) throw err;
files.filter(function(file) {
return fs.statSync(msgPath + file).isFile() &&
file.startsWith(filePrefix);
}).forEach(function (file) {
fs.readFile(msgPath + file, 'utf8', function(err, data) {
if(err) {
throw err;
}else{
slack.webhook({
icon_emoji: ":warning:",
text: data
}, function(err, res) {
if(err) {
throw err;
}else{
console.log(res);
fs.renameSync(msgPath + file, donePath + file);
}
})
}
})
})
});
結果を確認する
コンテナ上でスクリプトを実行します。80番ポートは開けてないのでポートスキャンがエラーになり、投稿されるはずです。
$ /mnt/scripts/scanPort.js
$ /mnt/scripts/sendSlack.js
{ status: 'ok',
statusCode: 200,
headers:
{ 'content-type': 'text/html',
'transfer-encoding': 'chunked',
(略)
response: 'ok' }
うまく投稿できました!
あとはcronか何かでこれらのスクリプトを定期的に呼び出せば完成です。
以上、次回は別の何かを監視対象に加えてみようと思います。