LoginSignup
1
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-10-30

前置き

題名の通り、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のページに移動、Start Buildingのボタンを押し、アプリを好きな名前で作成、対象となるワークスペースを指定。
スクリーンショット 2019-10-27 15.23.55.png

2 Botの作成

Add features and functionalityをクリックし、今回はボットを作ることが目的なので、さらにBotsをクリック。さらに、Add a Bot Userをクリック。

スクリーンショット 2019-10-27 15.28.24.png

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

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

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

スクリーンショット 2019-10-27 15.38.05.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に移動します。

スクリーンショット 2019-10-27 16.57.05.png

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

スクリーンショット 2019-10-27 17.02.00.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の画面に戻り、
スクリーンショット 2019-10-27 17.30.11.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をやってきた人には、なじみが深いかもしれません。この部分は、ただイメージを作ることだけを意識しましょう。

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"]
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の他にも様々なものがありますが、今回は最低限必要なものだけで設定していきます。

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以上にすると、なんと応答がその個数分だけ返ってきます。接続の部分をもっと別の方法で管理するまでは、この部分はそのままにしておきましょう。

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にしていきます。方法は色々あるとは思いますが、以下のコマンドが実行できる場合はそのまま実行してみると便利かもしれません。

echo -n "[API Token]" | base64

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

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は、

access/slack/Dockerfile
...
ENTRYPOINT ["flask", "run", "--host", "0.0.0.0", "--port", "5000"]

の部分で参照するflaskappの参照先を指定しています。

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なんて、訳わからんのだろうなと思ってしまいますが、少なくとも最低限の設定をする上では、恐ろしく簡単でした...

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側のコード

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は簡略化のために用いています。)

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化の際に改行を含まない方法を上で紹介していますので修正されました

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本体側のコード

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()

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

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

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が利用できることを前提としています。

# まずはminikubeを起動し、仮想空間を作成する。(場所はどこで実行してもOK)
minikube start
# 起動中のサービスの状況が確認できます。起動率100%を目指しましょう!
minikube dashboard
# 今回のプロジェクトのあるところまで移動(skaffold.yamlが見える場所)
skaffold dev
# もし、ログを詳細に確認したいときは
skaffold debug

ここまでの環境構築ができていれば起動は楽ですね。しかもこの状態で、変更を加えれば、自動的にリビルドされて反映されるようになります!(ただし設定をいじっているときは、結局skaffoldを再起動しないといけないときが多々ある。)

機能の確認

今回の実験の良い点は、ローカルで起動している段階で、Slackとの接続までチェックできる点です。
では、slackからアクセスしてみましょう!
スクリーンショット 2019-10-30 12.08.09.png
.....
スクリーンショット 2019-10-30 12.08.24.png
おぉ!
ちゃんと、繋がっているし、numeron側で書いてある文字も、反映されていますね!
あとは、GCP上に設置できれば文句なしです。

GCP上にあげる!

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

# あらかじめ、権限周りは確保しておきましょう
gcloud config set project [clusterのある、project_id名(小文字)]
gcloud config set container/cluster [cluster名(numeronとか?)]
gcloud container clusters get-credentials [cluster名(numeronとか?)]

この段階で、kubectl get nodesを実行して以下のようになっていれば大丈夫です。

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に戻しましょう。

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

これで安心ですね!

中間地点

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

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

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

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

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

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0