SlackでヌメロンをしてくれるBotを作る(前半戦)

# 前置き
題名の通り、Slackでヌメロンを楽しめるようにしようと思います。
ただし、ヌメロンそのものというよりも、Dockerやら、kubernetesを使った開発の学習をするというのが裏テーマです。
基本的には、備忘録や、同じことをしたい人への学習用等の目的で記事を作成しております。内容についておかしな点がございましたら、積極的に教えていただければと思います。

また、環境は__mac__を使用し__Docker for Mac__を利用しています。そのほかの環境については、適宜変換してご利用ください。

加えて、全ての内容を記載すると凄まじい量になるので、ここでは__設定__、__環境構築__、__最低限状態でのGCP公開__を主として取り扱い、具体的なヌメロンのロジックは次回に回したいと思います。

それでは、お時間のある方は御付き合いいただければと思います。

# 完成までの道筋
### 1. 下準備
- SlackのAPI利用のためのSecretKey取得
- サービスの構造の検討　(Webサービスとして利用とかもできるようにしたい)
- サービスを稼働させるためのGCPでの設定　(アップロード段階で行います。)

### 2. 実装
- サービス構造実現のためのファイル、フォルダの作成
- サービスの実装
- ローカルでのテスト

### 3. 公開
- GCP上での公開＆テスト
- 完成！

# 1. 下準備
## SlackのAPI利用のためのSecretKey取得
以降は、すでにSlackでチャンネルを持っていることが前提となります。

#### 1 SlackのAPIページに移動&Appの作成

[Slack API](https://api.slack.com)のページに移動、__Start Building__のボタンを押し、アプリを好きな名前で作成、対象となるワークスペースを指定。
<img width="564" alt="スクリーンショット 2019-10-27 15.23.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506583/5a81068c-0bd3-fdae-a1b3-c781c5e1f3f2.png">

#### 2 Botの作成

__Add features and functionality__をクリックし、今回はボットを作ることが目的なので、さらに__Bots__をクリック。さらに、__Add a Bot User__をクリック。

<img width="524" alt="スクリーンショット 2019-10-27 15.28.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506583/f2a4b2a6-5de3-ba19-74d5-a976687ff6a5.png">

設定は各自のお好みでどうぞ。今回は__nnumeron__という名前のボットにしてます。

#### 3 Appにアクセス権の付与&Token取得

> Oauth&Permissions > Install App to Workspace > 許可する

<img width="587" alt="スクリーンショット 2019-10-27 15.38.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506583/5e9033ee-46d6-e3db-7dff-d73caa4f51a6.png">

色々アクセスできるようになるので、そういう権限がない場合は、やらない方が懸命でしょう。

再び、__Oauth&Permissions__に移動すると、__Bot User OAuth Access Token__が現れていると思うので、それが後々使う__API Token__です。

## サービスの構造の検討
#### はじめに
サービスは、Pythonで基本的に作成していきます。理由としては、SlackBotを作るのが、簡単だったことと、対戦ロジックも高速化する必要がないので、pythonでいーやというものです。
#### 実装したい機能
今回の目標は、ユーザーごとに対戦を管理して、かつ、戦績を記録していくというものです。と、いうのも対戦ロジックの強さとか利用頻度とか気になるので...

少しややこしいのは、現在の利用者のステータスを管理しなければいけないところでしょうか。
今回のサービスでは以下のパーツに分けられます。

- Slackの通信を受け取る部分
- データ管理のためのMySQL
- 対戦管理

通信を受け取る部分を拡張していき、例えばWebでも対戦できるようにすることができるようします。なので、対戦管理の部分を切り離して作業していきます。
（この辺りの構造周りはほとんど経験ないので、意見のある方は教えて欲しいです。）

## サービスを稼働させるためのGCPでの設定
ここでも、すでに__GCP__での登録が完了していることを前提とします。コンソール上で, __gcloud__が利用できる状態であることとが前提となります。

また、僕自身も、kubernetesは初心者なのですが、この辺りの解説はここでは__致しません__。気になる方は、__kubernetes__, __minikube__, __skaffold__等で調べると、色々と出てくるかと思います。

さらに言うと、これらの話の前に、__Docker__の話がそもそも出てくるので、そちらも合わせて調べてみてください。

### 1 クラスタの作成
__GCP__の中の__Kubernetes Engine__に移動します。

<img width="1096" alt="スクリーンショット 2019-10-27 16.57.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506583/f12fe29d-e706-8b45-9cef-3aca5e05bec0.png">


__クラスタを作成__のボタンを押します。

<img width="1097" alt="スクリーンショット 2019-10-27 17.02.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506583/63fc8082-af54-1b82-dae9-473722e23ddc.png">

特にそこまで重い処理をするわけではないので、標準クラスタを選択します。また今の所、今後どうなるかわからないので、そのまま進めていきます。

その後、作成が完了するまで猫と戯れましょう。

### 2 ローカルから、クラスタへの接続
ここから、ローカルとクラスタを接続する。
クラスタとの接続を測るために以下を実行します。

    $ gcloud config configurations create numeron
    $ gcloud config set project numeron

    # 設定した地域に合わせて変更しよう 
    $ gcloud config set compute/region us-central1-a
    $ gcloud config set compute/zone us-central1-a

そして、実際に接続するために、__GCP__の画面に戻り、
<img width="775" alt="スクリーンショット 2019-10-27 17.30.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506583/632a74af-ea1c-b171-fd24-e93c2e04337f.png">
の接続を押します。その中のコードをコピーします。

これで諸々の下準備は完了です！！

# 2. 実装 (前半戦)
## サービス構造実現のためのファイル、フォルダの作成
サービスの構造の検討のところで、大まかな方針を決定しました。それを具体的な構造に反映していきます。
より良い構造等、ご意見ありましたら、教えていただけると幸いです。

    (root)
     - skaffold.yaml
     - access
        - manifest # kubernetesのマニフェストを格納
            - deployment.yaml
            - secret.yaml
        - slack    # Slackからの接続を受け取る部分
            - Dockerfile
            - run.py
            - slackbot_settings.py
            - requirements.txt
            - plugins
                - __init__.py
                - gateway.py
     - process
        - manifest # kubernetesのマニフェストを格納
            - deployment.yaml
            - service.yaml
        - database # ヌメロンのデータ管理用
        - numeron  # ヌメロンの対戦ロジック
            - gateway                     # 外部との接続窓口 slackbotや、mysqlとの接続用
                - local_api_connection.py # pod内通信で飲み使える前提で作っている
            - test                        # テスト用のフォルダ（今の所はからのまま）
            - manage.py 　　　　　　　　　　　# 最初に起動するファイル。このファイルから全てが始まる
            - Dockerfile
            - requirements.txt

この段階では、全ての構造を実現しているわけではありませんが、ここまでの段階で、__slackbot__でSlackからのメッセージを受け取って、そこから__process/numeron__に接続し、何かしらの応答を得て、それをSlackに返答する。という大筋の流れまで検証することができます。

なので、まずは、上記の流れをGCP上で再現できるようにしましょう！

## 実装:前半戦:アウトライン
- イメージを作成するためのDockerfileの記述
    - slackbot用のDockerfileの作成
    - numeron用のDockerfileの作成
- kubernetes用のmanifestの作成
    - slackbot用のdeployment, secret作成 (serviceはslackbotが担ってくれているので一先ずそのまま)
    - numeron用のdeployment, service作成
- skaffoldの設定
- コードの実装
    - slackbotのメッセージ受け取り部分の作成
    - numeronのflaskでの応対部分の作成


たったの4ステップ！楽勝ですね..

### Step1 Dockerfileの作成
Dockerfileの作成は、docker-composeをやってきた人には、なじみが深いかもしれません。この部分は、ただイメージを作ることだけを意識しましょう。

```Dockerfile:access/slack/Dockerfile
FROM python:3.7
ENV PYTHONUNBUFFERED 1

RUN mkdir /code
WORKDIR /code
ADD requirements.txt /code/
RUN pip install -r requirements.txt

COPY ./run.py /code/run.py
COPY ./slackbot_settings.py /code/slackbot_settings.py
COPY ./plugins /code/plugins

ENTRYPOINT ["python","run.py"]
```
```Dockerfile:process/numeron/Dockerfile
FROM python:3.7
ENV PYTHONUNBUFFERED 1

RUN mkdir /code
WORKDIR /code
ADD requirements.txt /code/
RUN pip install -r requirements.txt

COPY ./manage.py /code/manage.py
COPY ./gateway /code/gateway

ENTRYPOINT ["flask", "run", "--host", "0.0.0.0", "--port", "5000"]
```

細かい説明は省きますが、どちらもpythonがベースになっているので、操作はかなり似通っていますね。ただし、process/numeron/Dockerfileの方の__ENTRYPOINT__については、少し、理想と違う部分があるので、この後変更されていくかもしれません。

### Step2 kubernetes用のmanifestの作成
この部分は、kubernetesのための設定ファイルだと思ってください。Deploymentやら、Serviceの他にも様々なものがありますが、今回は最低限必要なものだけで設定していきます。

```yaml:access/manifest/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: access
spec:
  replicas: 1
  selector:
    matchLabels:
      app: access
  strategy:
    rollingUpdate:
      maxSurge: 50%
      maxUnavailable: 0%
  template:
    metadata:
      labels:
        app: access
    spec:
      containers:
        - name: access-slack
          image: [yourname]/numeron-access-slack
          env:
            - name: SLACK_API_TOKEN
              valueFrom:
                secretKeyRef:
                  name: access-slack
                  key: api_token
```
以降[yourname]には、自身のDockerHubのアカウント名を書きます。後、僕の設定のままだと、__replicas__の値を2以上にすると、なんと応答がその個数分だけ返ってきます。接続の部分をもっと別の方法で管理するまでは、この部分はそのままにしておきましょう。

```yaml:access/manifest/secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: access-slack
type: Opaque
data:
  api_token: [base64化した、slackのAPI Token]
```
[base64化した、slackのAPI Token]は文字通り、自身の__API Token__をbase64にしていきます。方法は色々あるとは思いますが、以下のコマンドが実行できる場合はそのまま実行してみると便利かもしれません。


```bash
echo -n "[API Token]" | base64
```
__*※10/30 以前紹介していた方法では、最終文字に改行が含まれてしまっていたので修正しました*__
この__API Token__が公開されちゃうとエライコッチャなので、気をつけましょう...


```yaml:process/manifest/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: process
spec:
  replicas: 1
  selector:
    matchLabels:
      app: process
  strategy:
    rollingUpdate:
      maxSurge: 50%
      maxUnavailable: 0%
  template:
    metadata:
      labels:
        app: process
    spec:
      containers:
        - name: process-numeron
          image: [yourname]/numeron-process-numeron
          ports:
          - containerPort: 5000
          env:
            - name: FLASK_APP
              value: "manage:app"
```
__env__について、これはそこのコンテナ固有の環境変数を指定できるのですが、__FLASK_APP__は、

```Dockerfile:access/slack/Dockerfile
...
ENTRYPOINT ["flask", "run", "--host", "0.0.0.0", "--port", "5000"]
```
の部分で参照するflaskappの参照先を指定しています。

```yaml:process/manifest/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: process-network
  labels:
    service: process
spec:
  selector:
    app: process
  ports:
  - port: 5000
    targetPort: 5000
```
ここに関しては、深く触れらるほど理解してはいませんが、公開しているポートはあくまで、Pod間で使えるものなので、外部からの接続はできません...

### Step3 skaffoldの設定
さて、Docker -> kubernetes -> skaffoldときたら、skaffoldなんて、訳わからんのだろうなと思ってしまいますが、少なくとも最低限の設定をする上では、恐ろしく簡単でした...

```yaml:skaffold.yaml
apiVersion: skaffold/v1beta7
kind: Config
build:
  artifacts:
  - image: [yourname]/numeron-access-slack
    context: ./access/slack
  - image: [yourname]/numeron-process-numeron
    context: ./process/numeron
deploy:
  kubectl:
    manifests:
      - ./access/manifest/*.yaml
      - ./process/manifest/*.yaml
```
おそらく、ここまで来ている方は、内容がわかってしまうかもしれませんが、一応解説。
#### artifacts
__artifacts__については、アップロードするイメージを指定する部分になります。
どこのディレクトリのものを、なんというイメージの名前で、設定するのかを決定します。
ここでの、名前は__./**/manifest/deployment.yaml__で書かれている__image__の設定と同じ名前にする必要があります。もちろん、公式のimageを使ってもよく、その場合__artifacts__にイメージの設定を書く必要はありません。
#### manifest
ここには、__kubernetes__用の__manifest__がどこに格納されているかを示す必要があります。
なお、上記のようにワイルドカードを用いた設定もできます。フォルダ構造に合わせて設定しましょう。


### Step4 コードの実装
この辺りは、微妙な部分が目立つかと思いますが、ひとまずは、最初の項目の達成を目指します。項目の深堀は後半戦に回したいと思います。
#### SlackBot側のコード
```py:access/slack/run.py
import time
from slackbot.bot import Bot

from logging import getLogger
import logging

logger = getLogger(__name__)

BORDER_END_TIME = 60


def run():
	# 基本的に止めるつもりはない
	# ログを出して、続行するただし、前回の停止から1分いないなら、終了する
	bef_time = 0
	while time.time() - bef_time > BORDER_END_TIME:
		bef_time = time.time()
		try:
			logger.debug("run!")
			bot = Bot()
			bot.run()
		except:
			import traceback
			logger.warning(traceback.format_exc())


if __name__ == "__main__":
	# 簡単なログ設定を利用する
	logging.basicConfig(level=logging.DEBUG)
	logger.debug("activate bot")
	run()
	logger.debug("deactivate bot")
```
pythonの_slackbot_の使い方をみると、大体こんな感じになるかと思います。実は、ログの書き方があまりわかっていない節があるので、詳しい方は教えていただけると助かります。（ただし、__basicConfig__は簡略化のために用いています。）

```py:access/slack/slackbot_settings.py
# -*- coding: utf-8 -*-
import os

# トークンの指定
API_TOKEN = os.environ["SLACK_API_TOKEN"]

# 認識できない入力の際の応答の方法
DEFAULT_REPLY = "ごめんなさい、こちらで認識できませんでした..."

# 利用するスクリプトのプラグイン
PLUGINS = ['plugins']
```
APITOKENをそのまま書かないようにしようと、環境変数を利用して設定しました...~~したのですが、なぜか__API Token__の終端に必ず改行が入ってしまい、認証が通らなかったので、__strip()__で取り除いています。~~
__*10/30 base64化の際に改行を含まない方法を上で紹介していますので修正されました*__

```py:access/slack/plugins/gateway.py
from slackbot.bot import respond_to
from slackbot.bot import listen_to
from slackbot.bot import default_reply

from logging import getLogger

import os
import requests
import socket
import json

logger = getLogger(__name__)

SERVICE_HOST = os.environ["PROCESS_NETWORK_SERVICE_HOST"]
@respond_to(r"ヌメロン[!！]")
def start_numeron(message):
	logger.debug("request to start numeron!")
	reply_message = ["気が早いねぇ　kubernetesのデプロイチェック中だよ！"]
	# ヌメロンサーバーに接続
	resp = requests.get("http://"+SERVICE_HOST+":5000")
	logger.debug("start connection to numeron")
	resp = json.loads(resp.text)
	# コメントを追加
	reply_message.append(resp["message"])
	message.reply("\n".join(reply_message))
	message.react("+1")
```
この部分はとてつもなく苦労しました。特に何に苦労したかというと、__Pod__間の通信の仕方が全然わからなかったのです。

今回、どのようにして接続を果たしたかというと、実は__Pod__を立ち上げる時、起動されている__Service__の接続のためのIPが各環境変数に代入されていることがわかりました！

IPだけわかればよかったので環境変数__"[Service名の大文字]"__+__"\_"__+__"SERVICE_HOST"__に保管されている値を取り出し、それを利用して__requests__で接続しています。(接続先は__process/numeron__のflaskのサーバーです。)

__*補足*__
[Service名の大文字]というのは、少し言葉足らずで、"-"は"\_"に変更する必要があるみたいです。
なので、Service名が__"process-network"__の場合__"PROCESS_NETWORK"__となります。(経験上)
また、もっとスマートな方法があれば教えていただきたいです...

#### numeron本体側のコード

```py:process/numeron/manage.py
from gateway import local_api_connection

# flask app の登録
app = local_api_connection.app

if __name__ == "__main__":
	# Flask の接続を行う(今は全く機能していない)
	local_api_connection.activate()

```
この部分は実は個人的に__気に入っていないのですが__、今の所はこのままにしています。(接続方法の確認に躍起になって、ほとんど意味を持たないファイルになってしましました)

なので、この部分は「あ、つなっがってるなー」の確認以上の意味を持っていないとお考えください。

```py:process/numeron/gateway/local_api_connection.py
import os
from flask import Flask, jsonify
from logging import getLogger

logger = getLogger(__name__)

app = Flask(__name__)
app.config["JSON_AS_ASCII"] = False

def activate():
	logger.debug("connection gateway activated")
	app.run()
	logger.debug("connection gateway deactivated")

@app.route("/")
def index():
	logger.debug("access detected")
	return jsonify({
		"message": "テストやで"
	})
```
__app.config["JSON_AS_ASCII"] = False__の部分を追加しないと、日本語が認識されなくなってしまうので、必ず、日本語を扱う場合は追加しておきましょう。

# 3. 公開
### ローカルで実験
#### 起動
ここまでくれば、__中身は何もないですが__、少なくとも機能を作っていくことに専念できる状態にはなっているはずです。(データベース構築の設定をのぞいて..)
さて、まずはローカルで動くことを確認しましょう。個人的には、ターミナルを複数開いておくことをお勧めします。以下、kubernetes, minikube, skaffoldが利用できることを前提としています。

```bash
# まずはminikubeを起動し、仮想空間を作成する。(場所はどこで実行してもOK)
minikube start
# 起動中のサービスの状況が確認できます。起動率100%を目指しましょう！
minikube dashboard
```

```bash
# 今回のプロジェクトのあるところまで移動(skaffold.yamlが見える場所)
skaffold dev
# もし、ログを詳細に確認したいときは
skaffold debug
```
ここまでの環境構築ができていれば起動は楽ですね。しかもこの状態で、変更を加えれば、自動的にリビルドされて反映されるようになります！(ただし設定をいじっているときは、結局__skaffold__を再起動しないといけないときが多々ある。)

#### 機能の確認
今回の実験の良い点は、ローカルで起動している段階で、Slackとの接続までチェックできる点です。
では、slackからアクセスしてみましょう！
<img width="488" alt="スクリーンショット 2019-10-30 12.08.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506583/1fd1e8ef-6182-5888-8a17-f5b276fc6132.png">
.....
<img width="503" alt="スクリーンショット 2019-10-30 12.08.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506583/beb32850-8069-462e-b636-27c83341a2df.png">
おぉ！
ちゃんと、繋がっているし、__numeron__側で書いてある文字も、反映されていますね！
あとは、GCP上に設置できれば文句なしです。

### GCP上にあげる！
ここでは、skaffoldを用いたGCP上での公開方法を紹介します。また、今の段階では、動かし続けるほどの状態でもないので、ローカルで停止すれば、そのまま停止する方法で、公開します。当然ですが、ここまでの段階で__kubernetes engine__の__cluster__が作られている前提で話を進めていきます。

```bash
# あらかじめ、権限周りは確保しておきましょう
gcloud config set project [clusterのある、project_id名(小文字)]
gcloud config set container/cluster [cluster名(numeronとか？)]
gcloud container clusters get-credentials [cluster名(numeronとか？)]
```
この段階で、__kubectl get nodes__を実行して以下のようになっていれば大丈夫です。

```bash
NAME                                     STATUS   ROLES    AGE    VERSION
gke-[cluster名?]-xxx-xxx-[いろんな文字]     Ready  <none>   xdxh   v1.13.10-gke.0
gke-[cluster名?]-xxx-xxx-[いろんな文字]     Ready  <none>   xdxh   v1.13.10-gke.0
gke-[cluster名?]-xxx-xxx-[いろんな文字]     Ready  <none>   xdxh   v1.13.10-gke.0
```
cluster作成の時に、設定が同じであれば同じような名前のものが複数個みられるかと思います。
ここまで来たら、あとは、ローカルの時と同様に__skaffold dev__を叩きつければ起動します。ただし、ここからかなり時間がかかった記憶があるので、しばらく放置しましょう。

さて、ローカルの時と同じような画面になったら、Slackから再びアクセス..同様に結果が表示されているかと思います！(味気ない)

__*補足*__
実はここまでの作業を終えてそのままでいると、面倒なことになります。何かと言いますと、今kubernetesはターゲットをgcloudにしています。なので、いざ、ローカルでの開発に戻そうと思うと、__「起動しない！？」__なんてことになります。
ターゲットを、__minikube__に戻しましょう。

```bash
# minikubeを起動してから行いましょう
kubectl config use-context minikube
```

これで安心ですね！

## 中間地点
ここまでで、一通りの最低限の設定が完了したかと思います。ここまでのやり方を踏襲すれば、__ヌメロン__にこだわらず応用が利くかと思います。今回は一旦ここまでで、この次は具体的にヌメロンの対戦ロジックの作成に移っていきたいと思います。

かなり長くなってしまいましたが、Docker周りってやっぱり複雑で、断片的な情報が多くて(もちろん、そちらの方がいいことも多々ある)実際に動かそう！となると、やるべきことが見えづらいと思いました。

今回はそんな人の、一つの手助けになれたらと思います。

また、今回の趣旨として、個人的な勉強の意味もあったので、詳しい方は「この部分はセキュリティ的にかなりまずい！」とか、「この部分はこっちを使った方が良い」等の意見がございましたら、教えていただければと思います。

お疲れ様でした！また、お会いしましょう！