HubotをKubernetesにデプロイしてPodの状況をSlackから確認する

- [PythonのslackbotをKubernetesにデプロイしてPodの状況を確認する](https://qiita.com/sotoiwa/items/a82c49e4d5031aa271fa)
- [PythonのslackbotのPodからkubectlコマンドを実行させる](https://qiita.com/sotoiwa/items/1e3065daf4710ad8399f)

ということをやってみたが、Pythonの`slackbot`モジュールではなく、Botとしてはよりメジャーであるらしい[Hubot](https://hubot.github.com/)を試す。

## Slackの準備

以下からHubutのインテグレーションを追加する。

- https://slack.com/apps/A0F7XDU93-hubot

<img width="994" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/86449/dcff784c-9680-6df0-c92e-e9822dff00c4.png">

後でトークンを使用するので確認しておく。

チャンネルにBotを招待する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/e353413b-96ed-78a3-78e9-ffc13b73951d.png)

## ローカル実行の準備

ローカルでHubotを実行するために[Node.js](https://nodejs.org/)の環境を整える。

### nodenvのインストール

Node.jsのバージョン管理ツールは、[nvm](https://github.com/creationix/nvm)、[nodebrew](https://github.com/hokaccha/nodebrew)などいろいろあり、[ndenv](https://github.com/riywo/ndenv)を入れようと思ったが、[Deprecated]となっており代わりにおすすめされていた[nodenv](https://github.com/nodenv/nodenv)を入れる。

```shell
brew install nodenv
```

以下を`.bash_profile`に追加する。

```shell
eval "$(nodenv init -)"
```

ターミナルを再起動する。

### Node.jsのインストール

インストール可能なNode.jsのバージョンを確認する。

```shell
nodenv install -l
```

LTSリリースの最新バージョンをインストールする。

```shell
nodenv install 10.15.0
```

Globalに使用するバージョンを指定する。

```shell
nodenv global 10.15.0
```

npmをアップデートしておく。

```shell
npm install -g npm
```

バージョンを確認。

```shell-session
$ node -v
v10.15.0
$ npm -v
6.7.0
```

### Hubotのインストール

以下に従い`yo`と`generator-hubot`をインストールする。

- https://hubot.github.com/docs/

```shell
npm install -g yo generator-hubot
```

ターミナルを再起動。

## ローカルでHubotを試す

まずはローカルでHubotを動かしてみる。

### Botの作成

ディレクトリを作成し、雛形を作成する。

```shell
mkdir myhubot-shell
cd myhubot-shell
yo hubot
```

とりあえずローカルで動かすためアダプターは`shell`を指定する。

- https://hubot.github.com/docs/adapters/shell/

<script id="asciicast-jwqPBff2E4Cvap0e5Bx8JLuFZ" src="https://asciinema.org/a/jwqPBff2E4Cvap0e5Bx8JLuFZ.js" async></script>

### Ping

まずはHubotを起動してPingしてみる。起動時に警告などでているがとりあえず無視する。

```shell-session
$ bin/hubot
audited 179 packages in 0.765s
found 0 vulnerabilities

body-parser deprecated undefined extended: provide extended option node_modules/hubot/src/robot.js:446:21
No history available
myhubot-shell> [Fri Feb 01 2019 11:31:01 GMT+0900 (GMT+09:00)] WARNING Loading scripts from hubot-scripts.json is deprecated and will be removed in 3.0 (https://github.com/github/hubot-scripts/issues/1113) in favor of packages for each script.

Your hubot-scripts.json is empty, so you just need to remove it.
[Fri Feb 01 2019 11:31:01 GMT+0900 (GMT+09:00)] INFO hubot-redis-brain: Using default redis on localhost:6379
[Fri Feb 01 2019 11:31:01 GMT+0900 (GMT+09:00)] ERROR hubot-heroku-keepalive included, but missing HUBOT_HEROKU_KEEPALIVE_URL. `heroku config:set HUBOT_HEROKU_KEEPALIVE_URL=$(heroku apps:info -s | grep web.url | cut -d= -f2)`

myhubot-shell> myhubot-shell ping
myhubot-shell> PONG

myhubot-shell>
```

### スクリプトの作成

`scripts`ディレクトリにサンプルがある。とりあえずスクリプトを作ってみる。以下のURLにも解説がある。

- https://hubot.github.com/docs/scripting/

```coffeescript:mybot.coffee
module.exports = (robot) ->

  robot.hear /hello/i, (res) ->
    res.send "Hello World!"
  
  robot.respond /how are you/i, (res) ->
    res.reply "I am fine!"
```

投稿をウォッチする`hear`と、呼びかけに応答する`respond`がある。Pythonの`slackbot`とよく似ている。

`myhubot-shell`と呼びかけるのはやや長いので、引数で名前を`hubot`と指定して起動する。

```shell
bin/hubot --name hubot
```

投稿、呼びかけをそれぞれ試してみる。

```shell-session
hubot> hello
hubot> Hello World!

hubot> hubot How are you?
hubot> Shell: I am fine!

hubot>
```

### コミュニティースクリプトを試す

Hubotはコミュニティースクリプトがたくさんあるようなので、Kubernetes関連のパッケージを探してみる。

```shell-session
$ npm search hubot-scripts kubernetes
NAME                      | DESCRIPTION          | AUTHOR          | DATE       | VERSION  | KEYWORDS
rk295-hubot-kubernetes    | Hubot Kubernetes…    | =rk295          | 2018-08-10 | 0.0.4    | hubot hubot-scripts kuberne
hubot-k8s                 | A hubot script for…  | =kevin_schmidt  | 2018-12-25 | 0.1.0    | hubot hubot-scripts k8s kub
hubot-sbueringer-kubernet | Hubot Kubernetes…    | =sbueringer     | 2017-06-02 | 0.4.2    | hubot hubot-scripts kuberne
es                        |                      |                 |            |          |
hubot-kubernetes          | Hubot Kubernetes…    | =canthefason    | 2016-01-18 | 0.3.0    | hubot hubot-scripts kuberne
$ npm home hubot-k8s
$
```

いくつかあるが、比較的メンテされていそうな`hubot-k8s`を試してみる。
`npm home hubot-k8s`コマンドでパッケージのホームページが開ける。

- https://github.com/kevinschmidt/hubot-k8s

パッケージをインストールする。

```shell
npm install --save hubot-k8s
```

インストールしたパッケージを`external-scripts.json`に追記する。

```json:external-scripts.json
[
  "hubot-diagnostics",
  "hubot-help",
  "hubot-heroku-keepalive",
  "hubot-google-images",
  "hubot-google-translate",
  "hubot-pugme",
  "hubot-maps",
  "hubot-redis-brain",
  "hubot-rules",
  "hubot-shipit",
  "hubot-k8s"
]
```

`hubot-k8s`からの接続を試すため、Minikubeを起動する。

```shell
minikube start
```

`hubot-k8s`では、kube-apiserverへの接続情報は以下の環境変数で渡す必要がある。

- `HUBOT_K8S_CONTEXTS`
- `HUBOT_K8S_DEFAULT_CONTEXT`
- `HUBOT_K8S_DEFAULT_NAMESPACE`

`HUBOT_K8S_CONTEXTS`は以下のようなjson。

```json
{
  "prod": {
    "server": "https://kubernetes.cluster.io",
    "ca": "./ca.crt",
    "dashboardPrefix": "https://kubernetes.cluster.io",
    "token": "<kubernetes token>"
  }
}
```

Minikubeへの接続に必要な情報を確認する。

```shell-session
$ kubectl config view
apiVersion: v1
clusters:
- cluster:
    certificate-authority: /Users/sotoiwa/.minikube/ca.crt
    server: https://192.168.99.104:8443
  name: minikube
contexts:
- context:
    cluster: minikube
    user: minikube
  name: minikube
current-context: minikube
kind: Config
preferences: {}
users:
- name: minikube
  user:
    client-certificate: /Users/sotoiwa/.minikube/client.crt
    client-key: /Users/sotoiwa/.minikube/client.key
$
```

Minikubeは鍵で認証しているが、`hubot-k8s`では鍵は指定できず、トークンが必要なようなので、ServiceAccountを作成してそのトークンを使うことにする。

あとでHubotをコンテナ化してPodとしてMinikubeにデプロイしたいので、そのときにもServiceAccountを流用できるように、デプロイ先のNamespaceを作成し、自動的に作成されるそのNamespaceの`default`のServiceAccountを使う。このServiceAccountに強い権限をもつ`cluster-admin`のClusterRoleをバインドする。

```shell
kubectl create ns hubot
kubectl create clusterrolebinding hubot \
  --clusterrole=cluster-admin \
  --serviceaccount=hubot:default
```

このServiceAccountのトークンを取得する。

```shell
SECRET_NAME=$(kubectl get sa default -n hubot -o jsonpath='{.secrets[].name}')
echo $SECRET_NAME
TOKEN=$(kubectl get secret $SECRET_NAME -n hubot -o jsonpath={.data.token} | base64 --decode)
echo $TOKEN
```

`HUBOT_K8S_CONTEXTS`は以下のようなjsonとなる。

```json
{
  "minikube": {
    "server": "https://192.168.99.104:8443",
    "ca": "/Users/sotoiwa/.minikube/ca.crt",
    "dashboardPrefix": "https://kubernetes.cluster.io",
    "token": "$TOKEN"
  }
}
```

1行にして、ダブルクオートをエスケープして、環境変数としてエクスポートしてから、Hubotを起動する。

```shell
export HUBOT_K8S_CONTEXTS="{\"minikube\":{\"server\":\"https://192.168.99.104:8443\",\"ca\":\"/Users/sotoiwa/.minikube/ca.crt\",\"dashboardPrefix\":\"https://kubernetes.cluster.io\",\"token\":\"$TOKEN\"}}"
export HUBOT_K8S_DEFAULT_CONTEXT=minikube
export HUBOT_K8S_DEFAULT_NAMESPACE=default
bin/hubot --name hubot
```

`hubot-k8s`パッケージのコマンドでPodやDeploymentの情報が取得できることを確認する。

```shell-session
hubot> hubot k8s ns
hubot> Shell: Your current kubernetes namespace is: `default`

hubot> hubot k8s ns kube-system
hubot> Shell: Your current kubernetes namespace is changed to `kube-system`

hubot> hubot k8s po
hubot> Shell: Here is the list of *pods* running in namespace *kube-system* with context *minikube*
>*<https://kubernetes.cluster.io/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy/#!/pod/kube-system/coredns-86c58d9df4-htjgd?namespace=kube-system|coredns-86c58d9df4-htjgd>* - pods `1/1` with status `Running` and restart count `0` since `12 minutes ago`
>*<https://kubernetes.cluster.io/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy/#!/pod/kube-system/coredns-86c58d9df4-znckf?namespace=kube-system|coredns-86c58d9df4-znckf>* - pods `1/1` with status `Running` and restart count `0` since `12 minutes ago`
>*<https://kubernetes.cluster.io/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy/#!/pod/kube-system/etcd-minikube?namespace=kube-system|etcd-minikube>* - pods `1/1` with status `Running` and restart count `0` since `12 minutes ago`
>*<https://kubernetes.cluster.io/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy/#!/pod/kube-system/kube-addon-manager-minikube?namespace=kube-system|kube-addon-manager-minikube>* - pods `1/1` with status `Running` and restart count `0` since `12 minutes ago`
>*<https://kubernetes.cluster.io/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy/#!/pod/kube-system/kube-apiserver-minikube?namespace=kube-system|kube-apiserver-minikube>* - pods `1/1` with status `Running` and restart count `0` since `12 minutes ago`
>*<https://kubernetes.cluster.io/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy/#!/pod/kube-system/kube-controller-manager-minikube?namespace=kube-system|kube-controller-manager-minikube>* - pods `1/1` with status `Running` and restart count `0` since `12 minutes ago`
>*<https://kubernetes.cluster.io/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy/#!/pod/kube-system/kube-proxy-jrjsx?namespace=kube-system|kube-proxy-jrjsx>* - pods `1/1` with status `Running` and restart count `0` since `12 minutes ago`
>*<https://kubernetes.cluster.io/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy/#!/pod/kube-system/kube-scheduler-minikube?namespace=kube-system|kube-scheduler-minikube>* - pods `1/1` with status `Running` and restart count `0` since `12 minutes ago`
>*<https://kubernetes.cluster.io/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy/#!/pod/kube-system/kubernetes-dashboard-ccc79bfc9-vc6r9?namespace=kube-system|kubernetes-dashboard-ccc79bfc9-vc6r9>* - pods `1/1` with status `Running` and restart count `0` since `12 minutes ago`
>*<https://kubernetes.cluster.io/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy/#!/pod/kube-system/storage-provisioner?namespace=kube-system|storage-provisioner>* - pods `1/1` with status `Running` and restart count `0` since `12 minutes ago`


hubot> hubot k8s deploy
hubot> Shell: Here is the list of *deployments* running in namespace *kube-system* with context *minikube*
>*<https://kubernetes.cluster.io/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy/#!/deployment/kube-system/coredns?namespace=kube-system|coredns>* - desired `2`, current `2`, updated `2`, available `2`
>*<https://kubernetes.cluster.io/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy/#!/deployment/kube-system/kubernetes-dashboard?namespace=kube-system|kubernetes-dashboard>* - desired `1`, current `1`, updated `1`, available `1`


hubot>
```

### kubectlを直接実行する

`hubot-k8s`パッケージではできることが限られているので、`kubectl`コマンドを直接実行できるようにしてみる。以下を参考に`mybot.coffee`スクリプトに`kubectl`というメンションへの反応を追記する。`kubectl`以下を引数としてkubectlコマンドを実行する。このときexecだとシェルを介して任意のコマンドを実行されてしまうリスクがあるので、execFileを使うようにした。

- [SlackからHubot経由で、サーバのシェルを実行してみよう！](https://tech.sanwasystem.com/entry/2015/04/30/213521)
- [Child Process](https://nodejs.org/api/child_process.html)
- [node.jsのchild_processのexecとexecFileとspawnの違い](http://www.axlight.com/mt/sundayhacking/2014/03/nodejschild-processexecexecfilespawn.html)

```coffeescript:mybot.coffee
module.exports = (robot) ->

  robot.hear /hello/i, (res) ->
    res.send "Hello World!"
  
  robot.respond /how are you/i, (res) ->
    res.reply "I am fine!"

  robot.respond /kubectl (.*)/, (msg) ->
    args = msg.match[1]
    @exec = require("child_process").execFile
    command = "kubectl"
    msg.send "Command: #{command} #{args}"
    @exec command, args.split(" "), (error, stdout, stderr) ->
      msg.send error if error?
      msg.send stdout if stdout?
      msg.send stderr if stderr?
```

Hubotを起動し、`kubectl`が実行できることを確認する。

```shell-session
hubot> hubot kubectl get ns
hubot> Command: kubectl get ns
NAME          STATUS   AGE
default       Active   169m
hubot         Active   165m
kube-public   Active   169m
kube-system   Active   169m



hubot>
```

## Slack連携

続いてShellアダプターではなく、Slackアダプターを試す。

### Hubotの作成

アダプターを変更するやり方がわからなかったので、Botの雛形を新規に作成する。

```shell
mkdir myhubot-slack
cd myhubot-slack
yo hubot
```

<script id="asciicast-w5w6cd7CV0OR6pUBx3T58l1cx" src="https://asciinema.org/a/w5w6cd7CV0OR6pUBx3T58l1cx.js" async></script>

先ほど作成した`mybot.coffee`スクリプトと、`hubot-k8s`パッケージを先ほどと同じように導入する（省略）。

### 稼働確認

Slackのトークンをエクスポートし、アダプターを指定してBotを起動する（ターミナルを再起動した場合は`hubot-k8s`をMinikubeに接続させるために必要な環境変数をエクスポートすること）。

```shell
export HUBOT_SLACK_TOKEN=hogehoge
bin/hubot --name hubot --adapter slack
```

`hubot-k8s`でPodの状況確認できることを確認。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/69d62951-368e-66ef-188e-07d3b67d4980.png)

直接`kubectl`も実行可能なことを確認。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/7509a061-0b3e-2b85-206a-2ab6b98b220f.png)

## Kubernetesにデプロイ

一般的にはHerokuで動かすことが多いようだが、コンテナにしてMinikubeにデプロイしてみる。

### イメージのビルド

以下を参考に、`Dockerfile`を作成する。

- https://nodejs.org/en/docs/guides/nodejs-docker-webapp/

Podから`kubectl`を実行する方法については以下を参照。コンテナ内に`kubectl`のバイナリを配置し、Podを実行するServiceAccountに適切な権限を与えるだけでよい。

- [Kubernetesクラスター内のPodからkubectlを実行する](https://qiita.com/sotoiwa/items/aff12291957d85069a76)

```dockerfile:Dockerfile
FROM node:10.15.0-alpine

WORKDIR /usr/src/app

RUN wget https://storage.googleapis.com/kubernetes-release/release/v1.13.0/bin/linux/amd64/kubectl \
  && mv kubectl /usr/local/bin/kubectl \
  && chmod +x /usr/local/bin/kubectl

COPY bin/ bin/
COPY scripts/ scripts/
COPY *.json ./

RUN npm install

CMD [ "./bin/hubot", "--name", "hubot", "--adapter", "slack" ]
```

ディレクトリ構成は以下のような状態。

```
myhubot-slack
├── Dockerfile
├── Procfile
├── README.md
├── bin
│   ├── hubot
│   └── hubot.cmd
├── external-scripts.json
├── hubot-scripts.json
├── node_modules
├── package-lock.json
├── package.json
└── scripts
    ├── example.coffee
    └── mybot.coffee
```

イメージをビルドしてPushする。

```shell
docker build -t sotoiwa540/myhubot-slack:1.0 .
docker push sotoiwa540/myhubot-slack:1.0
```

### Kubernetesへのデプロイ

Podから`kubectl`を実行させるには、Podを実行するServiceAccountに適切な権限を割り当てる必要があるが、既に`hubot`というNamespaceの`default`のServiceAccountに、`cluster-admin`のClusterRoleをバインド済みなため、`hubot`というNamespaceにPodをデプロイすれば`kubectl`が実行できる。

`hubot-k8s`パッケージのほうは、kube-apiserverへの認証情報を`HUBOT_K8S_CONTEXTS`という環境変数で渡す必要があり、しかもjsonの形式で渡す必要がある。

Slackへの接続のためのトークンと、`hubot-k8s`の`HUBOT_K8S_CONTEXTS`以外の環境変数はSecretに入れてPodに渡すようにする。

```shell
kubectl create secret generic hubot-secret -n hubot \
  --from-literal=HUBOT_SLACK_TOKEN=${HUBOT_SLACK_TOKEN} \
  --from-literal=HUBOT_K8S_DEFAULT_CONTEXT=${HUBOT_K8S_DEFAULT_CONTEXT} \
  --from-literal=HUBOT_K8S_DEFAULT_NAMESPACE=${HUBOT_K8S_DEFAULT_NAMESPACE}
```

`HUBOT_K8S_CONTEXTS`については、Kubernetesによって証明書やServiceAccountのトークンがコンテナの`/var/run/secrets/kubernetes.io/serviceaccount`以下に自動的にマウントされているのでこれを使って、起動前に環境変数を`export`してから、Hubotを起動するようにする。この処理は直接マニフェストに書く。なお、`hubot`コマンドでNode.jsのプロセスを起動するとき、`exec`を使わないと、`sh`のプロセスとその子プロセスの`node`のプロセスが2つ上がることになる。

Deploymentのマニフェストは以下のようにする。

```yaml:hubot-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
  labels:
    app: hubot
  name: hubot
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hubot
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: hubot
    spec:
      containers:
      - name: hubot
        image: sotoiwa540/myhubot-slack:1.0
        imagePullPolicy: Always
        envFrom:
        - secretRef:
            name: hubot-secret
        command:
        - sh
        - -c
        - |
          export CA=$(cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt)
          export TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
          export HUBOT_K8S_CONTEXTS=$(echo "{\"minikube\":{\"server\":\"https://kubernetes.default:443\",\"ca\":\"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\",\"dashboardPrefix\":\"https://kubernetes.cluster.io\",\"token\":\"$TOKEN\"}}")
          exec /usr/src/app/bin/hubot --name hubot --adapter slack
```

Hubotをデプロイする。

```shell
kubectl apply -f hubot-deployment.yaml -n hubot
```

Podを確認する。

```shell-session
$ kubectl get po
NAME                     READY   STATUS    RESTARTS   AGE
hubot-68cc597477-8kt4n   1/1     Running       0          22s
$
```

### 稼働確認

稼働確認する。はじめに`hubot-k8s`パッケージが動作することを確認する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/9d92ac32-d71c-7c40-e898-203061a6b2b7.png)

続いて`kubectl`を直接実行できることを確認する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/8b068905-40cf-7d0e-2f6c-f4301d6e7afe.png)

以上。Pythonの`slackbot`モジュールとほとんど同じ印象。