この記事は
YAMAP エンジニア Advent Calendar 2020 の八日目になる予定です。
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 のバージョンは以下の通りです。
hubot 側の準備
hubot の作成
hubot の準備は、こちら の記事を参考にさせて頂きました。ありがとうございます。
$ npm install -g yo generator-hubot $ mkdir kappabot $ cd kappabot $ yo kappabot
Hubot と Slack の連携
Hubot と Slack を連携させます。
スクリプトの追加
以下のような、指定した日付が祝日かどうかを返すスクリプトを書いてみました。
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
だと動きました。
尚、このスクリプトでは、以下のような感じのやりとりを楽しむことが出来ます。
動作確認
以下のように hubot を起動します。
fish> env HUBOT_SLACK_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ./bin/hubot --adapter slack
起動したら、適当なチャンネルにボットを招待しましょう。
/invite @your-bot-name
あとは、下図のようにやり取りを楽しみましょう。
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
尚、この際に username
と password
の値は base64 エンコードした値を登録する必要があります。
$ echo -n 'admin' | base64 YWRtaW4= $ echo -n '1f2d1e2e67df' | base64 MWYyZDFlMmU2N2Rm
今回は、マニフェスト自体を Github で管理したかったので、後者の方法を選びました。ただ、マニフェストに登録している秘匿情報 (上記の例だと、username
と password
) は、単純に Base64 エンコードだけされた状態なので、簡単にデコードして読み取ることが出来てしまいます。この課題を解決するのが、後述する sealed-secrets です。
sealed-secrets で暗号化
sealed-secrets は、ざっくり言うと、sealed-secrets controller が、暗号化された Secret マニフェスト (SealedSecrets) を読み取って復号化し、Pod から利用出来るようにするものです。
Secret マニフェストは、kubeseal という関連ツールと sealed-secrets コントローラによって作成される公開鍵を使って暗号化することが出来ます。暗号化した、Secret マニフェストは SealedSecrets として Github リポジトリに登録することになります。
ということで、HUBOT_SLACK_TOKEN
を Kubernetes クラスタに登録したいと思いますが、既に、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_TOKEN
を Base64 エンコードします。
$ 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
後は、適当にやりとりしてみましょう。
以上
今回は、手元の docker desktop 上の Kubernetes クラスタを利用しましたが、GKE とかにデプロイしてみたい気持ちが高まって参りました。