ようへいの日々精進XP

よかろうもん

hubot を Kubernetes クラスタ上で動かす一例

この記事は

YAMAP エンジニア Advent Calendar 2020 の八日目になる予定です。

qiita.com

tl;dr

hubot を Kubernetes で動かそうと試行錯誤したのでメモしておきます。とりあえず、docker desktop 上の Kubernetes で動くところまで確認出来ました :tada:

尚、本記事で登場するツールのバージョンについては、以下の通りです。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H15

$ docker version
Client: Docker Engine - Community
 Version:           19.03.12
...

Server: Docker Engine - Community
 Engine:
  Version:          19.03.12
...
 runc:
  Version:          1.0.0-rc10
...
 docker-init:
  Version:          0.18.0
...

$ kubectl version
Client Version: version.Info{Major:"1", Minor:"16+", GitVersion:"v1.16.6-beta.0", GitCommit:"e7f962ba86f4ce7033828210ca3556393c377bcc", GitTreeState:"clean", BuildDate:"2020-01-15T08:26:26Z", GoVersion:"go1.13.5", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"16+", GitVersion:"v1.16.6-beta.0", GitCommit:"e7f962ba86f4ce7033828210ca3556393c377bcc", GitTreeState:"clean", BuildDate:"2020-01-15T08:18:29Z", GoVersion:"go1.13.5", Compiler:"gc", Platform:"linux/amd64"}

docker desktop のバージョンは以下の通りです。

f:id:inokara:20201205091534p:plain

hubot 側の準備

hubot の作成

hubot の準備は、こちら の記事を参考にさせて頂きました。ありがとうございます。

$ npm install -g yo generator-hubot
$ mkdir kappabot
$ cd kappabot
$ yo kappabot

Hubot と Slack の連携

Hubot と Slack を連携させます。

f:id:inokara:20201205091605p:plain

スクリプトの追加

以下のような、指定した日付が祝日かどうかを返すスクリプトを書いてみました。

holiday.js

module.exports = (robot => {
  robot.respond(/today/, async res => {
    let today = getToday()
    let holidays = 'holidays.jp から取得した日付のリスト'
    // today が holidays に含まれているかチェック
    res.send(today)
  })

  robot.respond(/holiday$/, async res => {
    let result
    if (todayIsHoliday()) {
      result = '今日は, 祝日です!'
    } else {
      result = '今日は祝日ではないですね.'
    }
    res.send(result)
  })

  robot.hear(/holiday (\d{4}-\d{2}-\d{2})$/, async res => {
    let day = res.match[1]
    let holiday = todayIsHoliday(day)
    let result
    if (holiday != '') {
      result = day + ' は `' + holiday + '` です.'
    } else {
      result = day + ' は祝日ではないですね.'
    }
    res.send(result)
  })

  const todayIsHoliday = (day) => {
    if (day != null) {
      today = day
    } else {
      today = getToday()
    }

    holidays = getHolidays()
    if (today in holidays) {
      return holidays[today]
    } else {
      return ''
    }
  }

  const getToday = () => {
    let t = new Date
    let dd = t.getDate()
    let mm = t.getMonth() + 1
    let yyyy = t.getFullYear()

    if (dd < 10) {
      dd = '0' + dd
    }

    if (mm < 10) {
      mm = '0' + mm
    }
 
    let today = yyyy + '-' + mm + '-' + dd
    return today
  }

  const getHolidays = () => {
    const req = require('sync-request')
    res = req('GET', 'https://holidays-jp.github.io/api/v1/date.json')
    if (res.statusCode == '200') {
      return JSON.parse(res.body)
    } else {
      console.log('祝日の一覧取得に失敗しました.')
    }
  }
})

このスクリプトscripts ディレクトリ以下に放り込んでおきます。ちなみに、このスクリプト、当初は他のサンプルスクリプトに倣って拡張子を .coffee にしていたら、シンタックスエラーが出てしまって hubot が起動しませんでしたが、.js だと動きました。

尚、このスクリプトでは、以下のような感じのやりとりを楽しむことが出来ます。

f:id:inokara:20201205091624p:plain

動作確認

以下のように hubot を起動します。

fish> env HUBOT_SLACK_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ./bin/hubot --adapter slack

起動したら、適当なチャンネルにボットを招待しましょう。

/invite @your-bot-name

あとは、下図のようにやり取りを楽しみましょう。

f:id:inokara:20201205091624p:plain

Kubernetes で動かす

Dockerfile

ここから本題です。Kubernetes 上で動かすにあたって、以下のような Dockerfile を書きました。

FROM node:15.3.0-buster
WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

CMD ["bin/hubot", "--adapter", "slack"]

docker-compose

一旦、docker-compose 起動出来るようにしましょう。

version: '3.4'

services:
  kappabot:
    env_file: .env
    container_name: kappabot
    build:
      context: .
      dockerfile: Dockerfile
    image: inokappa/kappabot
    volumes:
      - .:/app
    command:
      - bin/hubot
      - "--adapter"
      - slack

HUBOT_SLACK_TOKEN は、.env ファイル記載しました。

HUBOT_SLACK_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

あとは、コンテナを起動しましょう。

$ docker-compose build && docker-compose up -d

deployment.yml

引き続き、Kubernetes で動かしてみたいと思いますので、docker desktop で Kubernetes が利用出来る状態にしておきましょう。

以下のように deployment.yml を書きました。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-kappabot
  labels:
    app: kappabot
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kappabot
  template:
    metadata:
      labels:
        app: kappabot
    spec:
      containers:
        - name: kappabot
          image: inokappa/kappabot
          imagePullPolicy: IfNotPresent
          command:
          env:
            - name: HUBOT_SLACK_TOKEN
              valueFrom:
                secretKeyRef:
                  name: kappabot-secret
                  key: kappabot_hubot_slack_token

まだまだ、ちゃんと Kubernetes を理解出来ていませんが、上記程度のマニフェストであれば、なんとなく雰囲気で書けるようになったはず。各項目の説明については割愛 (ちゃんと説明出来ない) させていただきます。

一点だけ。

HUBOT_SLACK_TOKEN環境変数に仕込んでいますが、トークン自体は Secrets に保存することを想定しています。Secrets については、次のセクションで少しだけ詳しく書きます。

Secrets からの sealed-secrets

Secrets

Secrets は、パスワードやトークン等の機密情報を登録する為の機能を提供して、Secrets (以後、Secret と書きます) を作成するには、ざっくりと二つの方法があります。

一つ目は、以下のように登録したいデータをテキストファイルに書き込んでおいて、kubectl create secret を実行する方法。

$ echo -n 'admin' > ./username.txt
$ echo -n '1f2d1e2e67df' > ./password.txt
$ kubectl create secret generic db-user-pass --from-file=./username.txt --from-file=./password.txt

また、以下のようにマニフェストを用意して、kubectl apply する方法があります。

apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
data:
  username: YWRtaW4=
  password: MWYyZDFlMmU2N2Rm

尚、この際に usernamepassword の値は base64 エンコードした値を登録する必要があります。

$ echo -n 'admin' | base64
YWRtaW4=
$ echo -n '1f2d1e2e67df' | base64
MWYyZDFlMmU2N2Rm

今回は、マニフェスト自体を Github で管理したかったので、後者の方法を選びました。ただ、マニフェストに登録している秘匿情報 (上記の例だと、usernamepassword) は、単純に Base64 エンコードだけされた状態なので、簡単にデコードして読み取ることが出来てしまいます。この課題を解決するのが、後述する sealed-secrets です。

sealed-secrets で暗号化

sealed-secrets は、ざっくり言うと、sealed-secrets controller が、暗号化された Secret マニフェスト (SealedSecrets) を読み取って復号化し、Pod から利用出来るようにするものです。

github.com

Secret マニフェストは、kubeseal という関連ツールと sealed-secrets コントローラによって作成される公開鍵を使って暗号化することが出来ます。暗号化した、Secret マニフェストは SealedSecrets として Github リポジトリに登録することになります。

ということで、HUBOT_SLACK_TOKENKubernetes クラスタに登録したいと思いますが、既に、sealed-secrets コントローラは作成され、公開鍵は手元にある前提で進めます。

$ kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.13.1/controller.yaml
$ kubeseal --fetch-cert \
    --controller-namespace=kube-system \
    --controller-name=sealed-secrets-controller > pub-cert.pem

まずは、HUBOT_SLACK_TOKENBase64 エンコードします。

$ echo -n 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' | base64
eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHg=

-n オプションを忘れないようにしましょう。

そして、以下のように Secret マニフェスト secret.yml を用意します。

apiVersion: v1
kind: Secret
metadata:
  name: kappabot-secret
type: Opaque
data:
  kappabot_hubot_slack_token: eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHg=

引き続き、Secret マニフェストを暗号化します。

$ kubeseal --format=yaml --cert=pub-cert.pem < secret.yml > sealedsecret.yml

SealedSecrets は、以下のような内容となります。

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  creationTimestamp: null
  name: kappabot-secret
  namespace: default
spec:
  encryptedData:
    kappabot_hubot_slack_token: AgAY//FLFic2Ga3ZrlsucZmxY/tN1k/v+CD+BrHVhWkzFjbK6tVSKgCW2PANfKAe8Zm2d/nsbVmRUOzZr3mU/kh+MppeEQnCztnVqUxflOrJqwwchWAmf5/3nwVUUjnr6ueVNTI3eLX/vECOpJlS51KyGM4vrg8eOGqLxAHvQxTqsKGQu/EqbQ3VN3AzHYVJlnK7Yni3z2nBt91uHjNm4fltVI1Jado84/R+uFYj27/O8dkq0gcvQiHttjXxUgNm+zJVXNurR6yXd++TR236rFBbsJkSkxfS70Fs5BriJ36pyvlK6vf95ZZDxH+1JNrOBWNluyLl+keDlJBpUfjnYmMzSRlBdzImAoxBk8SwP4BzMnadXoIn9Vti+DjOgMTAhPAjtHYGO8R8GLKn+6QUAUNRyLkKg5RMFctGTGJA9RWhFTON4KUNGjDJF6EfCzQMgCtOm7019wEW08Gq+L18clgruskL+LeJ4a2Z8YI7CG07QLv4zkqNVGTQYozHwOcn3+SMMQ3Ff+TN3tpGt5nMcH3Y7opbnZAaYsc5MyIvPYaOeiYR64XjznxE0kyDbNBkXQtB9opFRWH10SsM+8k6glJDihluGX+jtuigWMyRwyo9BA4Ok7ojBcMv2q3MOBlCR8dMeQX3vA0mJ3rcXeHov1HTesc3OfCDEXqI+u0PB9+p9ga/6P1Hw9cBhzBRvyzjvKVIufb69ez7xRgzn2b9Ej2EW32aikCmycG1hmCvSGQipCx8Lg==
  template:
    metadata:
      creationTimestamp: null
      name: kappabot-secret
      namespace: default
    type: Opaque

そして、最後に Secret を Kubernetes クラスタ上に作成します。

$ kubectl apply -f sealedsecret.yml

そして、hubot を Kubernetes クラスタにデプロイ

以下を実行して hubot を Kubernetes クラスタにデプロイします。

$ kubectl apply -f deployment.yml 

後は、適当にやりとりしてみましょう。

f:id:inokara:20201205091741p:plain

以上

今回は、手元の docker desktop 上の Kubernetes クラスタを利用しましたが、GKE とかにデプロイしてみたい気持ちが高まって参りました。

参考

kubernetes.io

kubernetes.io

kubernetes.io

kubernetes.io

github.com

tech.uzabase.com