出先でサーバに対してコマンド打てないのがストレスだったのでslackから本番サーバにコマンド打てるようにしました
今回はbotkitを使っています
hubotは有名ですが今更cofeescriptを覚える気にならなかったので他のものを探していたところちょうど手頃な感じのbotkitを見つけたので使ってみました
事前にslackのintegrationページでbotを作成しtokenを用意します
要件
- 任意のコマンドが実行できる
- ボットが起動しているサーバから各サーバにSSHが出来る前提
- 特定ユーザのみにコマンドの実行権限を与える
install
npmでインストールします。簡単
npm install --save botkit
スクリプトの用意
適当なサンプルを取ってきて用意します
- bot.js
const Botkit = require('botkit');
if (!process.env.SLACK_TOKEN) {
console.log('Error: Specify SLACK_TOKEN in environment');
process.exit(1);
}
controller = Botkit.slackbot({
¦ debug: false,
});
controller.spawn({
token: process.env.SLACK_TOKEN
}).startRTM(function(err){
if (err) {
throw new Error(err);
}
});
controller.hears('hi', ['direct_message','direct_mention','mention'],(bot,message) => {
bot.reply(message,"hi!");
});
起動
事前に用意したtokenをここで使います
SLACK_TOKEN=${api_token} node bot.js
簡単ですね
特定の発言に反応させる
基本はhearsで発言を拾ってきて処理をする
返答はreplyで行います
上記のサンプルをコードの最後の3行のように実装することでメンションかダイレクトメッセージ+hiという文言に反応してリプライしてくれるようになります

正規表現によるマッチ
特定文言以外にも正規表現を使ってマッチしたものを使うことが出来ます
controller.hears('hi Im (.*)',['direct_message','direct_mention','mention'], (bot,message) => {
bot.reply(message,`hi! ${message.match[1]}.`);
});
hi Imが先頭にあってメンションかダイレクトメッセージを受けているときに反応します
正規表現でマッチしてる箇所は名前を想定

コマンド実行
nodeのExecでshellコマンドを実行できます
これと組み合わせることでサーバに任意のコードを実行できるようになります
Execの引数にsshなどを使えばssh可能なサーバ全てに対してコマンドが実行できそうですね
const Exec = require('child_process').exec;
controller.hears('cmd (.*)',['direct_message','direct_mention','mention'],(bot,message) => {
Exec(message.match[1], (err,stdout,stderr) => {
if ( err ) {
bot.reply(message,`Error!!!\n${err}`);
}
else {
bot.reply(message,`\`\`\`${stdout}\`\`\``);
}
});
});

- sshしてみた

データの保存
botkitにはユーザー、チャンネル、チームなどの単位でデータを保存することができます
データの保存先はredisなどのデータストアを指定することもできますが今回は一番簡なファイルに保存します
controllerを宣言する際にオプションを指定してあげるだけで簡単なデータを扱えるようになります
controller = Botkit.slackbot({
debug: false,
json_file_store: './storage'
});
保存するときはidをプロパティにもつハッシュ(他は自由)を渡してあげるだけです
idにはmessage.userで取得した発言者のユーザーIDを使っています
ファイルもユーザーIDをベースに作られるようです
controller.hears('my name is (.*)',['direct_message','direct_mention','mention'], (bot,message) => {
controller.storage.users.save({id: message.user, name: message.match[1], role: 'staff' }, (err)=>{controller.log(err)});
bot.reply(message,`hi! ${message.match[1]}. user_id: ${message.user}`);
});
cat storage/users/Uxxxxxxx.json
{"id":"Uxxxxxxx","name":"hoge","role":"staff"}
getの他にもsave,allメソッドもあるので簡単な操作であればサクッと実装できますね
詳しい使い方などは下記エントリから参照できます
- 参考
さすがBotkit……!Slack botにデータを持たせる方法 - Storage -
ユーザー管理
コマンド実行はすることができるようになりました。だたこのままだと誰でもサーバに対してコマンドを実行できてしまうので危険ですね
そこで簡単ではありますがユーザーごとに権限を持たせてボットに話しかけるユーザーによってコマンドを実行できるか判定して制御するようにします
発言者のユーザーIDからデータを取得しコマンド実行時はユーザーのroleがadminでないと実行できないようにしました(roleは独自に設定した項目です)
const Exec = require('child_process').exec;
controller.hears('cmd (.*)',['direct_message','direct_mention','mention'],(bot,message) => {
controller.storage.users.get(message.user,(err,data) => {
if (err){console.log(err);return}
else if ( !data.role || data.role != 'admin' ) {
bot.reply(message,'permission denied.');
return;
}
Exec(message.match[1], (err,stdout,stderr) => {
controller.log(`${message.user} : ${message.match[1]} \n STDERR: ${err} \n STDOUT: ${stdout}`);
if ( err ) {
bot.reply(message,`Error!!!\n${err}`);
}
else {
bot.reply(message,`\`\`\`${stdout}\`\`\``);
}
});
});
});
ファイルの分割
ここまででも良かったのですが、コマンドが増えてきたときに対応するためにscript以下のjsファイルを自動で読み込めるようにします
ほぼ下記のエントリーをそのまま使わせていただきました
BotkitでHubotみたいにscriptsを読み込む - Qiita
- app.js
const Fs = require('fs');
const Path = require('path');
// load commands in scripts/*
const load = (path, file) => {
const ext = Path.extname(file);
const full = Path.join(path, Path.basename(file, ext));
try {
const script = require(full);
if (typeof script === 'function') {
script(this);
}
} catch(error) {
console.log(error);
process.exit(1);
}
};
const path = Path.resolve('.', 'scripts')
loadFiles = Fs.readdirSync(path).filter((file)=>{return !file.match(/\..*.swp/)})
loadFiles.sort().forEach((file) =>{
load(path, file);
});
- script/useradd.js
controller.hears('my name is (.*)',['direct_message','direct_mention','mention'], (bot,message) => {
controller.storage.users.get(message.user, (err,data) => {
if (err) {
controller.storage.users.save({id: message.user, name: message.match[1], role: 'staff' }, (err)=>{controller.log(err)});
bot.reply(message,`hi! ${message.match[1]}. user_id: ${message.user}`);
}
else {
if ( data ) {
controller.storage.users.save({id: message.user, name: message.match[1], role: data.role }, (err)=>{controller.log(err)});
bot.reply(message,`hi! ${message.match[1]}. user_id: ${data.id}`);
}
}
});
});
まとめ
最終的にできたのが下記
tokenさえ用意すれば簡単に実行できると思います
おまけとしてdocker-composeでの起動と、supervisorによるデーモンまで構築できるansibleのplaybookも作っておきました
構築にあたってはsupervisor3の実行ユーザだったり、環境変数だったりでちょっとつまづいたくらいでかなり簡単に実装することができました
これでslackからでもコマンドを打つことが出来るのでPCもってない時でもある程度対応できそうですね
だたコマンドを実行でき危険なので使い道は社内などの限られたチーム内になりそうですが...
botkit自体はslack特化のようですが簡単なボットであれば十分な気がします
これから色々機能を追加していく予定