Article:

最近勉強も兼ねておうちサーバーにKubernetesを構築したのですが、 おうちkubernetesクラスタあるある「特にデプロイするものがない」が当たり前のように発動してしまいました。 せっかくなら自分でアプリケーションを作ってデプロイ、したいですよね(というかその勉強も目的のうちの1つなのだった。)

最近偶然Discordbotを作ってk8sに上げる機会ががあったのでついでにチュートリアルとして記事にまとめておくことにしました。 極力複雑なことを省いて、ひとまず動く状態まで持っていくというコンセプトになります。

今回作るDiscordBot

サンプルとして、約1分ごとにランダムな犬の画像を投稿するDiscordbotを作ってみましょう。 言語はpythonを使うことにします。 最終形はGithubのレポジトリとして上げてあるので併せて参考にしてください。

ランダムな犬の画像は、Dog APIを使います。

前準備として、DiscordDevelopeprPortalでBotを発行し、テスト用のサーバーに招待を行っておいてください。

Permissionは

  • GENERAL PERMISSIONS - Messages/View Channels
  • TEXT PERMISSIONS - Send Messages の2つを許可しておきます。

また、そこでBotTokenも併せて発行します(後で使います)

コード

botのコードはこんな感じになると思います。 bottokenとchannelIDを環境変数に設定して動作確認してみてください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import os
import discord
import aiohttp
import asyncio

BOT_TOKEN  = os.getenv('BOT_TOKEN')
CHANNEL_ID = int(os.getenv('CHANNEL_ID'))

client = discord.Client(intents=discord.Intents.default())

async def post_dog(channel):
    print('Dog Time!')
    async with aiohttp.ClientSession() as session:
        response = await session.get('https://dog.ceo/api/breeds/image/random')
        dog = await response.json()

    await channel.send(dog['message'])

@client.event
async def on_ready():
    channel = client.get_channel(CHANNEL_ID)
    while True:
        await post_dog(channel)
        await asyncio.sleep(60)

client.run(BOT_TOKEN)

(pip3 install discord aiohttp asyncioするのお忘れなく!)

コンテナ化

k8s上に乗せるためにはDockerコンテナ化する必要があります。 かんたんにDockerfileを書きます

1
2
3
4
5
6
7
8
9
FROM python:3

COPY src/main.py ./

RUN pip3 install discord
RUN pip3 install aiohttp
RUN pip3 install asyncio

CMD ["python3", "main.py"]

おすすめなのはgithub actionsでコンテナ化してghcrにアップロードしてしまうことです。 Actionsを追加するときに出てくるテンプレート一覧にある「Publish Docker Container」が便利です。

Manifestの作成

次にk8sのManifestを書きます。 今回はシンプルに1つpodを作成するDeploymentにしちゃいます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
  name: discord-dogbot
spec:
  replicas: 1
  selector:
    matchLabels:
      name: discord-dogbot 
  template:
    metadata:
      labels:
        name: discord-dogbot
    spec:
      containers:
      - name: discord-dogbot
        image: ghcr.io/totegamma/discord-dogbot:master
        imagePullPolicy: Always
        envFrom:
          - secretRef:
              name: dogbot-secrets

今回はデプロイにargocdを使うので(そうじゃなければすぐkubectl applyしてしまえばおしまいですね)これもgithubに上げます。 ベストプラクティスとしてはアプリ本体のレポジトリとマニュフェストを管理するレポジトリは分けたほうがいいのですが(これは実際に 混ぜて運用してみると、明らかに分けたほうが良いことがわかると思います)、今回はシンプルなアプリということと、例ということで混ぜちゃいました。

僕はレポジトリの/manifests/deployment.yamlにこれを保存しました。

Secretのデプロイ

discordのbottokenをgithubに上げるわけにはいきません。 この辺はもっと良い仕組みを使ったほうがいいだろうな~と思いつつ、今回は手動で別途クラスタに登録することにします。 以下のmanifestでsecretを作成して、手動でkubectl applyします。

1
2
3
4
5
6
7
apiVersion: v1
kind: Secret
metadata:
  name: dogbot-secrets
stringData:
  BOT_TOKEN: "************************************************************************"
  CHANNEL_ID: "******************"

アプリ本体をArgoCDを使ってデプロイ

お待ちかねのデプロイタイムです! とは言え、argocdでポチポチするだけですね。

argocdでポチポチ

実際にポチポチしたら、アプリがちゃんとデプロイされて、discordチャンネルが犬の画像まみれになるはず!

ちゃんとデプロイされた

liveness probeの追加

このbotはとても適当なので、例えば時間が立つとセッション切れとかで動作を止めてしまいます。 そういう部分を1つ1つ直してもいいのですが(本当は直したほうがいいのですが)、それはそれとして 「今ちゃんと正常に動作しているか?」を観測して、そうでなければコンテナを再構築するという手もあります。

一見雑な荒療治のように見えますが、「ソフトウェアが永久にサービスを提供し続けること」を保証することはとても難しいのに対し、 「起動後しばらくはサービスを提供すること」かつ「今正常にサービスを提供できているかを観測すること 」というのはちょっとだけまだ着手しやすいことがあります。

k8sでもそういうことを簡単に提供してくれる機能があって、それがliveness probeですね。

今回は、livenessprobeの中でもコンテナ内で一定時間ごとにコマンドを実行するexecを使います。

discordにpostする時に合わせて特定の空ファイルを生成しておいて、逆にliveness probeではそれを削除していくことで、ちゃんとpostしてるかどうかを確認します。

空ファイルを作るコード

1
2
with open('/tmp/healthy', 'a') as fp:
    pass

livenessProbeを追加したdeployment.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apiVersion: apps/v1
kind: Deployment
metadata:
  name: discord-dogbot
spec:
  replicas: 1
  selector:
    matchLabels:
      name: discord-dogbot
  template:
    metadata:
      labels:
        name: discord-dogbot
    spec:
      containers:
      - name: discord-dogbot
        image: ghcr.io/totegamma/discord-dogbot:master
        imagePullPolicy: Always
        envFrom:
          - secretRef:
              name: dogbot-secrets
        livenessProbe:
          exec:
            command:
              - rm
              - /tmp/healthy
          initialDelaySeconds: 30
          periodSeconds: 120

livenessProbeに失敗するとコンテナを自動で殺して作り直してくれます。 これで「あれ?なんか死んでるっぽい?プロセス再起動するか…」っていう作業から開放されると思うと、ちょっと新鮮な気持ちになりますね。

振り返り

こういうことをやってみると、そろそろpip3とかをローカルで叩いている場合ではなく、最初からdockerで環境作ってやったほうがいいかもしれませんね(すでに流行に乗り遅れている感)。

ちょっとテストで動かして終わりではなく、サービスとして動かす前提の開発ができると良さそうです。