Railwayが有料化するのでDiscordBotをKoyebに乗り換えた話
2024/03/01追記
昨年12月に無料プランの改定があったそうです。
現時点で本項の内容をそのまま行うと利用料金が発生します。
改定後でも無料で運用できるかは調査中です。
2024/3/18追記
新料金プランに対応しました。
該当項目に追記しています。
Railway有料化
herokuが有料化した際に移行先として挙げられていたRailwayですが、数か月前に有料化が発表されたそうです。
クレカ認証が必要だったとはいえ、無料で安定したホスティングができるため重宝していました。
今後は月に5ドルの支払いが必須になるそうです。
お金に余裕がないわけではないのですが、趣味の範囲で運用していて、加えて円安で700円月に支払うのもお財布にやさしくないと考え、代替を探しました。
そしてKoyebと呼ばれるサービスを発見しました。
Websocketに対応しているので、DiscordBotの運用が可能です。
今回はDockerで展開して運用できたので方法を記しておきます。
Koyeb
料金
一応料金系統についても(わかる範囲で)記しておきます。
クレジットカード登録は必須なようです。
ですが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
でアカウント登録します。
GitHub連携でログインします。
登録時、使い道や所属を問われます。
その後、クレジットカード認証が行われます。
上記が終わると、ようやくダッシュボードの画面に遷移します。
create app
で早速作成します。
GitHubを選択し、
さっきデプロイしたリポジトリを選択します。
builder
はDocker
service type
はweb service
を選択します。
リージョンはデフォルトでもいいです。
マシンタイプは一番下のnano
にしていますが、一つしか運用しない場合はデフォルトのmicro
でも問題ないです。
2024/3/18追記
改定後、上記の方法では料金が発生します。
無料枠はInstanceのecoを選択し、Free
を選べば運用できるそうです。
ただし、2つ以上のFree
インスタンスは立てられないようです。
下のAvanced
をクリックすると、環境変数と、公開するポート番号を設定できます。
環境変数のPORT
と公開するポート番号は一緒にしましょう!
そうしないと外部向けのサーバーが立ち上がりません。
上記のコードではポート番号を8080
にしているので、両者ともに8080
に変更します。
また、DiscordBotのトークンもここに記します。
トークンなどの機密事項は必ずtype
をsecret
にしましょう!!
設定が終わったら、Apply
をクリックしましょう。
すると自動でビルドが始まります。
実行!!
以下のようにRuntimeが起動し、Public URL
が機能すれば成功です。
(画像は私が運用しているBotのものになります)
終わり
いかがでしょうか。
感覚的にrailwayよりDockerのビルドは早い気がしますし、デプロイもしやすい方だと思うので結構おすすめです。
最後までお読みいただきありがとうございました。
Discussion