🕌

Railwayが有料化するのでDiscordBotをKoyebに乗り換えた話

2023/07/31に公開

2024/03/01追記
昨年12月に無料プランの改定があったそうです。
https://www.koyeb.com/blog/new-eco-instances-the-most-affordable-way-to-deploy-apps-globally

現時点で本項の内容をそのまま行うと利用料金が発生します。
改定後でも無料で運用できるかは調査中です。

2024/3/18追記
新料金プランに対応しました。
該当項目に追記しています。

Railway有料化

herokuが有料化した際に移行先として挙げられていたRailwayですが、数か月前に有料化が発表されたそうです。

https://blog.railway.app/p/pricing-and-plans-migration-guide-2023

クレカ認証が必要だったとはいえ、無料で安定したホスティングができるため重宝していました。
今後は月に5ドルの支払いが必須になるそうです。
お金に余裕がないわけではないのですが、趣味の範囲で運用していて、加えて円安で700円月に支払うのもお財布にやさしくないと考え、代替を探しました。
そしてKoyebと呼ばれるサービスを発見しました。

https://www.koyeb.com/

Websocketに対応しているので、DiscordBotの運用が可能です。
今回はDockerで展開して運用できたので方法を記しておきます。

Koyeb

料金

一応料金系統についても(わかる範囲で)記しておきます。

https://www.koyeb.com/docs/faqs/pricing

クレジットカード登録は必須なようです。
ですがvisa系統のデビットカードも使えるようなので「kyash」あたりのプリペイドカードでも登録は可能です。
この辺はRailwayと同じですね。

無料(Hobby)プランでは毎月5.5ドルの無料クレジットが支給され、この範囲では無料で使用できます。
マシンのスペックが選択でき、それによって時間当たりの料金が異なります。
一番性能が低いもの(NANO)にすると、月に2つまでサーバーを無料で動かせるそうです。

ビルド

GitHubかDocker Imageと連携して、Dockerかbuildfileでビルドできるらしいです。
Profileは使えなかったため、Dockerfile内で実行させる必要があります。

コーデイング

今回ホストするものです。
FastAPIとPycordでBot+Webアプリの雛形作ります。

ディレクトリ構成

$ tree
.
├── app
│   ├── cogs
│   │   └── ready_load.py  # FastAPI立ち上げ
│   ├── core
│   │   └── start.py     # DiscordBot起動用
│   ├── api
│   │   └── apiv1.py
│   └── main.py  
├── Dockerfile
└── requirements.txt  

Dockerfile

FROM python:3.10.7
USER root

# ディレクトリ ./appに移動
WORKDIR /app

RUN apt-get -y update && apt-get -y install locales && apt-get -y upgrade && \
    localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ JST-9
ENV TERM xterm

# ./root/src ディレクトリを作成 ホームのファイルをコピーして、移動
RUN mkdir -p /root/src
COPY . /root/src
WORKDIR /root/src

# Docker内で扱うffmpegをインストール
RUN apt-get install -y ffmpeg

# pipのアップグレード、requirements.txtから必要なライブラリをインストール
RUN pip install --upgrade pip
RUN pip install --upgrade setuptools
RUN pip install -r requirements.txt
# discord.pyをpy-cordにアップグレード
RUN pip install git+https://github.com/Pycord-Development/pycord

# 以下はKoyebで運用する際に必要
# ポート番号8080解放
EXPOSE 8080

# ディレクトリ /root/src/appに移動
WORKDIR /root/src/app

# DiscordBotとFastAPIのサーバ起動
CMD [ "python", "-u", "main.py" ]

requirements.txt

aiohttp==3.8.1
aiosignal==1.2.0
anyio==3.5.0
asgiref==3.5.0
async-timeout==4.0.2
attrs==21.4.0
discord.py==1.7.3
fastapi==0.73.0
frozenlist==1.3.0
h11==0.13.0
idna==3.3
multidict==6.0.2
pydantic==1.9.0
starlette==0.17.1
typing_extensions==4.1.1
uvicorn==0.17.4
yarl==1.7.2

app

main.py

from core.start import DBot
import discord
import os

from dotenv import load_dotenv
load_dotenv()

Token = os.environ['DISCORD_BOT_TOKEN']

# Bot立ち上げ
DBot(
    token=Token,
    intents=discord.Intents.all()
).run()

cogs

ready_load.py

import discord
from discord.ext import commands
import os

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

from starlette.middleware.sessions import SessionMiddleware
from fastapi.middleware.cors import CORSMiddleware

import uvicorn

from dotenv import load_dotenv
load_dotenv()

from api.apiv1.index import Index

try:
    from core.start import DBot
except ModuleNotFoundError:
    from app.core.start import DBot


class ReadyLoad(commands.Cog):
    def __init__(self, bot : DBot):
        self.bot = bot

    # DiscordからLINEへ
    @commands.Cog.listener(name='on_ready')
    async def on_message(self):

        await self.bot.change_presence(
            status=discord.Status.do_not_disturb,
            activity=discord.Activity(name='起動中...................',type=discord.ActivityType.watching)
        )

        self.app = FastAPI(
            docs_url=None,
            redoc_url=None,
            openapi_url=None
        )
        
	self.app.include_router(router=Index(bot=self.bot).router)

        if os.environ.get("PORTS") != None:
            hostname = "localhost"
            portnumber = int(os.getenv("PORTS", default=5000))
        else:
            hostname = "0.0.0.0"
            portnumber = int(os.getenv("PORT", default=8080))

        config = uvicorn.Config(
            app=self.app,
            host=hostname,
            port=portnumber,
            log_level="info"
        )
        server = uvicorn.Server(config)

        print('起動しました')

        # 終了時
        if os.environ.get("PORTS") != None:
            await server.serve()
            print("exit")
            await server.shutdown()
            await self.bot.close()
        else:
            await server.serve()
            print("exit")
            await server.shutdown()
            await self.bot.close()
	    
def setup(bot:DBot):
    return bot.add_cog(ReadyLoad(bot))

core

start.py

import discord
from discord import Intents
import os
import json
import traceback

from dotenv import load_dotenv
load_dotenv()

class DBot(discord.AutoShardedBot):
    def __init__(self, token:str, intents:Intents) -> None:
        self.token = token
        super().__init__(intents = intents)
        self.load_cogs()

    def load_cogs(self) -> None:
        for file in os.listdir("./cogs"):
            if file.endswith(".py"):
                cog = file[:-3]
                self.load_extension(f"cogs.{cog}")
                print(cog + "をロードしました")

    # 起動用の補助関数です
    def run(self) -> None:
        try:
            self.loop.run_until_complete(self.start(self.token))
        except discord.LoginFailure:
            print("Discord Tokenが不正です")
        except KeyboardInterrupt:
            print("終了します")
            #self.loop.run_until_complete(self.close())
        except discord.HTTPException as e:
            traceback.print_exc()

api

apiv1.py

from fastapi import APIRouter,Request
from starlette.requests import Request

import os

from discord.ext import commands
try:
    from core.start import DBot
except ModuleNotFoundError:
    from app.core.start import DBot
    
class Index(commands.Cog):
    def __init__(self, bot: DBot):
        self.bot = bot
        self.router = APIRouter()

        @self.router.get("/")
        async def index(request: Request):
	    return {'message':f'bot id:{self.bot.application_id}'}

ホスト

上記のプロジェクトをGitHubにデプロイしましょう。(プライベートでもパブリックでもOK)
終わったらKoyebに移動します。
sign upでアカウント登録します。

https://www.koyeb.com/

GitHub連携でログインします。

登録時、使い道や所属を問われます。
その後、クレジットカード認証が行われます。

上記が終わると、ようやくダッシュボードの画面に遷移します。
create appで早速作成します。

GitHubを選択し、

さっきデプロイしたリポジトリを選択します。

builderDocker
service typeweb serviceを選択します。

リージョンはデフォルトでもいいです。
マシンタイプは一番下のnanoにしていますが、一つしか運用しない場合はデフォルトのmicroでも問題ないです。

2024/3/18追記
改定後、上記の方法では料金が発生します。
無料枠はInstanceのecoを選択し、Freeを選べば運用できるそうです。
ただし、2つ以上のFreeインスタンスは立てられないようです。

下のAvancedをクリックすると、環境変数と、公開するポート番号を設定できます。

環境変数のPORTと公開するポート番号は一緒にしましょう!
そうしないと外部向けのサーバーが立ち上がりません。
上記のコードではポート番号を8080にしているので、両者ともに8080に変更します。

また、DiscordBotのトークンもここに記します。

トークンなどの機密事項は必ずtypesecretにしましょう!!

設定が終わったら、Applyをクリックしましょう。
すると自動でビルドが始まります。

実行!!

以下のようにRuntimeが起動し、Public URLが機能すれば成功です。
(画像は私が運用しているBotのものになります)

終わり

いかがでしょうか。
感覚的にrailwayよりDockerのビルドは早い気がしますし、デプロイもしやすい方だと思うので結構おすすめです。

最後までお読みいただきありがとうございました。

Discussion