PythonのslackbotをKubernetesにデプロイしてPodの状況を確認する

ChatOps的なことを試してみたかったのと、Pythonのお勉強のため、PythonでSlackのBotを作成し、Kubernetes上にデプロイして、Podの状況を確認できるようにしたメモ。BotといえばHubotらしいけれども、今回はPythonの`slackbot`モジュールで作成。

## 参考リンク

以下の先人の情報を参考に作成。

- [PythonのslackbotライブラリでSlackボットを作る](https://qiita.com/sukesuke/items/1ac92251def87357fdf6)
- [Pythonを使ったSlackBotの作成方法](https://qiita.com/kunitaya/items/690028e33ba5c666f3e2)
- [Python3系でSlack Botの作成〜基礎的な対話を実装する](https://qiita.com/croissant1028/items/8d6334b76576762df349)

## 準備

以下のリンクよりBotsアプリのSlackインテグレーションを追加する。APIトークンを確認し、Botの名前やアイコンなど設定する。

https://slack.com/apps/A0F7YS25R-bots

<img width="986" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/86449/98030964-7605-8a0d-4a71-2cdf3a32eca4.png">

Botをチャンネルに招待して追加する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/a0c3319f-4442-306d-0f99-953049cda216.png)

## Python

`slackbot`モジュールの使い方は以下の公式リポジトリと上の参考リンクを参照。

- https://github.com/lins05/slackbot

今回Pythonからkube-apiserverにアクセスしてPodとNamespaceの情報を取得したいので、PythonのKuberentes Clientである`kubernetes`モジュールも使う。

- https://github.com/kubernetes-client/python

### コード作成

以下の3つのファイルを作成する。→[GitHub](https://github.com/sotoiwa/python_slackbot)

```
python_slackbot
├── mybot.py
├── run.py
└── slackbot_settings.py
```

`slackbot_setting.py`にはデフォルトのリプライや読み込むプラグインなどの設定を記述する。
トークンは`API_TOKEN`としてここで書いてもよいが、環境変数`SLACKBOT_API_TOKEN`でも渡せるので今回は環境変数で渡す。
エラーの通知先のユーザーを指定する場合は`ERRORS_TO`を指定する。ユーザーがいない場合は起動エラーになるので注意。

```python:slackbot_settings.py
DEFAULT_REPLY = "何言ってんだこいつ"
PLUGINS = ['mybot']
ERRORS_TO = 'sotoiwa'
```

`run.py`は以下の通り作成。

```python:run.py
from slackbot.bot import Bot


def main():
    bot = Bot()
    bot.run()


if __name__ == "__main__":
    print('start slackbot')
    main()
```

`slackbot_settings.py`の`PLUGINS`で指定した`mybot.py`に自分の処理を書く。
`@respond_to('反応する文字列')`でメンションされたときの反応を書き、`@listen_to('反応する文字列')`でチャンネルの投稿への反応を書く。反応する文字列は正規表現で指定する。

```python:mybot.py
import os
import re

from kubernetes import client, config
import prettytable
from slackbot.bot import respond_to
from slackbot.bot import listen_to


# こんにちはに応答する
@respond_to('hello', re.IGNORECASE)
@respond_to('こんにちは|こんにちわ')
def mention_hello(message):
    message.reply('こんにちは！')


# kubernetesに反応する
@listen_to('kubernetes', re.IGNORECASE)
def listen_kubernetes(message):
    message.reply('kubernetesかっこいい！')
    message.react('+1')


# helmに反応する
@listen_to('helm', re.IGNORECASE)
def listen_helm(message):
    message.reply('helmかっこいい！')
    message.react('+1')


# get pod
@respond_to(r'^get\s+(po|pod|pods)\s+(-n|--namespace)\s+(.*)$')
def mention_get_po(message, arg2, arg3, namespace):

    # kubernetes上で動いているかを環境変数から判断する
    if os.getenv('KUBERNETES_SERVICE_HOST'):
        # ServiceAccountの権限で実行する
        config.load_incluster_config()
    else:
        # $HOME/.kube/config から読み込む
        config.load_kube_config()

    v1 = client.CoreV1Api()
    ret = v1.list_namespaced_pod(namespace=namespace, watch=False)

    table = prettytable.PrettyTable()
    table.field_names = ['name', 'phase']
    table.align['name'] = 'l'
    table.align['phase'] = 'l'

    for i in ret.items:
        table.add_row([i.metadata.name,
                       i.status.phase
                       ])

    msg = '```\n' + table.get_string() + '\n```'
    message.reply(msg)


# get ns
@respond_to(r'^get\s+(ns|namespace|namespaces)$')
def mention_get_ns(message, arg2):

    # kubernetes上で動いているかを環境変数から判断する
    if os.getenv('KUBERNETES_SERVICE_HOST'):
        # ServiceAccountの権限で実行する
        config.load_incluster_config()
    else:
        # $HOME/.kube/config から読み込む
        config.load_kube_config()

    v1 = client.CoreV1Api()
    ret = v1.list_namespace(watch=False)

    table = prettytable.PrettyTable()
    table.field_names = ['name']
    table.align['name'] = 'l'

    for i in ret.items:
        table.add_row([i.metadata.name])

    msg = '```\n' + table.get_string() + '\n```'
    message.reply(msg)
```

### ローカル実行

ローカルで実行する場合は、`HOME/.kube/config`からkube-apiserverへの認証情報を読み込むので、`kubectl config`コマンドで適切なコンテキストを設定しておく。

必要なモジュールをインストールする。

```shell
pip install kubernetes
pip install prettytable
pip install slackbot
```

APIトークンを`export`する。

```
export SLACKBOT_API_TOKEN=hogehoge
```

Botを起動する。

```shell
python run.py
```

### 稼働確認

こんにちは、こんにちは、helloというメンションに反応する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/a7e1603d-c63b-24e4-f09f-9dec4e077511.png)

kubernetes、helmに反応してリアクションする。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/5f20694e-8453-81e9-5060-cedbfa7b2711.png)

BotにメンションしてNamespaceを確認する。結果は`prettytable`モジュールで整形している。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/4453c10d-1572-83cc-547c-5d5064638ed3.png)

BotにメンションしてPodを確認する。Namespaceを指定する必要あり。以下の例ではPodが稼働していないので空。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/b86e9af0-2284-9a2d-0237-278bd09e20e6.png)

## Kubernetesへのデプロイ

Kubernetes上にBotをデプロイする。

### イメージのビルド

`requirements.txt`と`Dockerfile`を作成する。

```:requirements.txt
kubernetes
prettytable
slackbot
```

```dockerfile:Dockerfile
FROM python:3

WORKDIR /usr/src/app

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY ./*.py ./

ENTRYPOINT [ "python", "./run.py" ]
```

ファイル配置は以下の通り。

```
python_slackbot
├── Dockerfile
├── mybot.py
├── requirements.txt
├── run.py
└── slackbot_settings.py
```

イメージをビルドする。

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

### Kubernetesへのデプロイ

ローカルで実行する場合は、`HOME/.kube/config`から認証情報を読み込むが、Kubernetes上でPodとして実行する場合はPodを実行するServiceAccountの権限で認証するので、ClusterRoleとClusterRoleBindingを作成する。

今回はPodとNamespaceをリストできる権限を持つClusterRoleを作成し、`default`のNamespaceの`default`のServiceAccountにClusterRoleをバインドする。

```yaml:slackbot-clusterrole.yaml
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: slackbot
rules:
- apiGroups: [""]
  resources:
  - pods
  - namespaces
  verbs:
  - list
```

```yaml:slackbot-clusterrolebinding.yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: slackbot
subjects:
- kind: ServiceAccount
  name: default
  namespace: default
roleRef:
  kind: ClusterRole
  name: slackbot
  apiGroup: rbac.authorization.k8s.io
```

```shell
kubectl apply -f slackbot-clusterrole.yaml
kubectl apply -f slackbot-clusterrolebinding.yaml
```

APIトークンのSecretを作成する。

```shell
kubectl create secret generic slackbot-secret --from-literal=SLACKBOT_API_TOKEN=hogehoge
```

Deploymentを作成する。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
  labels:
    app: slackbot
  name: slackbot
spec:
  replicas: 1
  selector:
    matchLabels:
      app: slackbot
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: slackbot
    spec:
      containers:
      - name: slackbot
        image: sotoiwa540/slackbot:1.0
        imagePullPolicy: Always
        env:
        - name: SLACKBOT_API_TOKEN
          valueFrom:
            secretKeyRef:
              key: SLACKBOT_API_TOKEN
              name: slackbot-secret
```

```shell
kubectl apply -f slackbot-deployment.yaml
```

Podが稼働したことを確認する。

```shell-session
$ kubectl get po
NAME                        READY   STATUS    RESTARTS   AGE
slackbot-5b68f5dddf-klmpw   1/1     Running   0          8s
$
```

### 稼働確認

BotにメンションしてPodを確認する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/eb37ba05-0c66-62e8-9b10-91bd064513dc.png)

なお、今回はあくまで[Podのphase](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase)を見ているだけなので、普通に`kubectl`コマンドを実行したときの`STATUS`列とは異なる。また、Slackの投稿の文字数の制限で、Podの数が多いと表示が崩れる。

（追記）
PythonのKubernetes Clientライブラリを使った方法では、メンションに対応する操作をそれぞれ作る必要がある。複雑な処理をプログラミングしたい場合はこの方法がよいかもしれないが、単なる状況照会についてそれぞれの処理を作ったり結果を整形するのは不便なので、`kubectl`コマンドをサブプロセスで実行するように変更した。

- [PythonのslackbotのPodからkubectlコマンドを実行させる](https://qiita.com/sotoiwa/items/1e3065daf4710ad8399f)