bashスクリプトやPHPを実行できるslack botを作る

![](https://qiita-image-store.s3.amazonaws.com/0/8292/ef5f6cbc-0702-1c98-580f-786429f835e5.gif)

この記事は[aratana Advent Calendar 2017](https://qiita.com/advent-calendar/2017/aratana) 21日目の記事です

こんにちは、インデントは全角スペース派[^1]の岡元です。
前日は@aratana_tamutomoさんの「[Vim一年生による、Vimプラグイン作成指南](https://qiita.com/aratana_tamutomo/items/4f754c301fb911ff54e8)」というエントリーでした。
Vimプラグインってあんな感じで作るんですね！

今回は、bashスクリプトやPHPコードを実行するslack botを作ったのでそれについて書こうと思います。
(botのソースは[こちら](https://github.com/mokamoto12/slack-shellbot)です)

# botについて
botに対してメンションしてメッセージを送ると、メッセージをbashスクリプトとして実行してくれます。
また、メッセージに`-php`のプレフィックスをつけるとPHPのコードとして実行してくれます。

![shellbot.gif](https://qiita-image-store.s3.amazonaws.com/0/136546/ea9b96d7-26f4-e837-c150-b12be66e4ec1.gif)

スクリプト自体はdockerコンテナ内で実行するので`rm -rf /`などファイルを削除する系のコマンドは実行してもおそらく大丈夫ですが、
`:(){ :|:& };:`などのリソースを食いつぶす系のスクリプトはコンテナ内だけでなくホストにも影響があるかも知れないので実行するときは気をつけたほうがいいかもしれません。


# botを作る

botのフレームワークはpythonの[slackbot](https://github.com/lins05/slackbot)を使います。
プログラムの内容は、受け取ったメッセージをsubprocessでrunするだけです。
メッセージはクオートが全角になっていたりurlエンコードされていたりするのでrunする前に整形します。

```py3:plugins/shell.py
from slackbot.bot import default_reply
import subprocess
from plugins.utils.Modifier import Modifier


@default_reply
def shell(message):
    modifier = Modifier()
    text = modifier.modify(message.body['text'])
    print(text)
    res = subprocess.run(text, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, executable="/bin/bash")
    message.send(res.stdout.decode())
```

また、これをdockerで実行させるためにDockerfileを書きます。
ファイルはdocker hubのpythonのページにある[サンプル](https://hub.docker.com/_/python/)とほぼ同じものです。

(サンプルのDockerfileについて、はじめは何で`COPY`を二回に分けてるんだろうと思いましたが、
`COPY`を一回で纏めてしてその後に`RUN pip install`するとファイルを編集するたびにキャッシュが無効になるからだったんですね。)

## PHPコードを評価するサービスを作る

先ほど作成したpythonのイメージとは別にPHPコードを実行するイメージを作成します。

プログラムの内容は単純にhttpリクエストで渡ってきたコードを直接evalする感じです。危ないですね。

```php:php-evaluate-server/public/index.php
if (isset($_REQUEST['eval'])) {
    var_dump(eval($_REQUEST['eval']));
}
```

実際には、エラー時の出力をいい感じにしたり、入力の`return`や行末の`;`を省略しても実行されるようにしたいので[psysh](https://github.com/bobthecow/psysh)を参考に実装します。

以下はDockerfileです。
multi-stage buildsを用いることで、PHPの依存ファイルのインストールが簡単に行えます。

```bash:php-evaluate-server/Dockerfile
FROM composer:1.5.5 AS build

COPY composer.json composer.lock /app/

RUN composer install --no-dev

FROM php:7.1.12-apache-jessie

COPY --from=build /app/vendor /var/www/vendor/

COPY . /var/www/

RUN ln -s /var/www/public/* /var/www/html/
```

また、slackからメッセージを受け取りPHPコードを評価するサービスを呼び出す処理を先程のpythonイメージに追加します。

```py3:plugins/php/main.py
import re
from slackbot.bot import respond_to
from plugins.php.Client import Client as PhpClient
from plugins.utils.Modifier import Modifier


@respond_to('^-php (.*)', re.IGNORECASE)
def main(message, php_message):
    modifier = Modifier()
    client = PhpClient()
    php_code = modifier.modify(php_message)
    message.send(client.eval(php_code))
```

## docker-composeする

これまででbotを実行するpythonイメージとPHPコードを評価するphpのイメージを作成したので、docker-composeを使いこの2つを動かしてみます。

```yaml:docker-compose.yml
version: '3'
services:
  shellbot:
    build:
      context: .
    networks:
      - bot
    environment:
      - SLACKBOT_API_TOKEN=${SLACKBOT_API_TOKEN}

  php-evaluate-server:
    build:
      context: ./php-evaluate-server
    networks:
      - bot

networks:
  bot:
```

docker-composeファイルで環境変数の値を指定する際は`${...}`のように書くことで現在のシェルの変数を入れることができるみたいです。
また、デフォルトの値は`.env`ファイルを見てくれるみたいです。（詳しくは[こちら](https://docs.docker.com/compose/environment-variables/)）

```:.env
SLACKBOT_API_TOKEN=xoxb-12345XXX
```

ここではslackbotの実行に必要な`SLACKBOT_API_TOKEN`という変数を設定しています。
`SLACKBOT_API_TOKEN`にはslackのcustom integrationsでBotsを追加することで取得できるAPI Tokenを設定します。

今作成したdocker-composeファイルを使いbotを動かすには以下のコマンドを実行します。

```bash
$ docker-compose up --build
```

## rm -rf /されたら、、swarmのhealthcheckを使う

`rm -rf /`などを実行されると/bin/bashなどのファイルが削除されてbotが動かなくなってしまうのでhealthcheckでbotが動ける状態にあるか監視します。
dockerのswarmモードを用いるとstatusがunhealthyになった際にコンテナを立ち上げなおしてくれます。

swarmにサービスをデプロイする際にdocker stack deployコマンドを用いるのですが、
その際に使用するdocker-stackファイルを以下のように作成します。

```yaml:docker-stack.yml
version: '3'
services:
  shellbot:
    image: mokamoto12/slack-shellbot:0.2
    networks:
      - bot
    env_file: .env
    depends_on:
      - php-evaluate-server
    healthcheck:
      test: type ls

  php-evaluate-server:
    image: mokamoto12/php-evaluate-server:latest
    networks:
      - bot
    healthcheck:
      test: type ls

networks:
  bot:
```

先程のdocker-composeファイルと違うのはbuildが消えimageになっているのと、environmentが消えenv_fileになり、healthcheckの項目が増えています。

### buildではなくimageを使う
swarmを用いる際には複数のマシンでコンテナを立ち上げることになるため、それぞれのマシンからアクセスできるdocker registryが必要になるみたいです。
そのため、buildの項目は使うことはできず、imageでregistryにあるイメージを指定します。

### environmentで.envファイルによる値の指定ができない

docker-composeファイルでは`${...}`のように記述することで.envで指定した変数の値を用いることができていましたが、試しにdocker stack deployしてみると値がうまく設定されていませんでした。(よく[リファレンス](https://docs.docker.com/compose/compose-file/#variable-substitution)を見ると.envは使えないことが書いてあるんですね)
代わりにenv_fileで環境変数の設定がされているファイルの指定を行います。

### healthcheck

healthcheckのtestで記述した内容はコンテナ内で定期的に実行され、その終了ステータスからコンテナがhealthyな状態かunhealthyな状態かを判断してくれます。
また、先程も書きましたがdocker stack deployした時にはコンテナがunhealthyになると勝手に新しくコンテナを立ち上げてくれます。

今回は`type ls`を実行し/bin/lsのファイルが削除されていればbotが動かない状態としています。

## botを実行する

上で作成したdocker-stackファイルを用いbotを動かします。
以下のコマンドを実行します

```bash
$ docker stack deploy -c docker-stack.yml bot
```

# まとめ

docker swarmを用いることでrm -rf /を実行されても復活するbotを作成することができました。

また、今回始めてdocker swarmを使ったのですがdocker-composeで使っていたdocker-compose.ymlをそのまま使うことができず一部試行錯誤する感じでした、
リファレンスを読むだけではわからないことも多く実際に触ってみることで色々理解できることもあったので良かったです。


明日は@okazono_erikaさんの「初対面の人に会う時のドキドキのついて」です。
ドキドキしますね！

[^1]: 冗談です