fly.io を使った Discord Bot の構築と運用（無料）

## はじめに
無料で使えるホスティングサービスである [fly.io](https://fly.io) と [Discord.py](https://github.com/Rapptz/discord.py) を用いた、Discord Bot の構築と運用についてまとめてみました。

## fly.io とは
fly.io とは、サーバレスでアプリケーションのデプロイができるプラットフォームになります。いわゆる FaaS（Function as a Service）と呼ばれるサービスに分類されます。大体の言語に対応しており、チュートリアルがかなり充実しています。また、接続した場所から一番近いデータセンターを自動で選んでくれるため、低レイテンシかつ、リアルタイム性の高いアプリケーションの構築が可能となっています。さらに、独自の CDN を持ち Edge サーバによるキャッシュによって高速なレスポンスを提供してくれます。また、AWS が開発しているコンテナ用のマイクロ VM である [Firecracker](https://jvns.ca/blog/2021/01/23/firecracker--start-a-vm-in-less-than-a-second/) を使用しており、Fly.io がファイル群から適切なプログラムを選択し自動生成したイメージファイル、または自作の Dokerfile を利用して高速なデプロイが可能となっています。
> Fly Machines are VMs with a fast REST API that can boot instances in about 300ms.

基本的な操作は全て CLI で行います。
fly.io の無料枠は以下の通りです。Discord Bot のような小規模なアプリであれば無料枠で十分収まると思います。

・仮想マシン：共有 CPU 1個/RAM 256MB のマシンが最大3つ
・データベース：合計 3GB まで（PostgreSQL と Redis が使用可能）
・IP アドレス：エニーキャスト IPv4 アドレス1個と無制限のエニーキャスト IPv6 アドレス
・SSL 証明書：最大10個まで
・データ転送量：日本リージョンだとアウトバウンドは合計 30GB まで/インバウンドは無制限

詳しくは以下を参考下さい。

https://fly.io/docs/about/pricing/

## 環境
* Raspberry Pi 4 Model B
    * CentOS Stream 8
        * Python 3.11.0
            * discord.py 2.1.1
* [fly.io](https://fly.io)

## 導入

### Bot アカウントの作成
<a href="https://discord.com/developers">Discord Developer Portal</a> より Bot アカウントを作成しましょう。詳細な手順については既にまとまっている記事があるため省きます。以下の記事を参考にして下さい。
実装においてアクセストークンが必要になります。メモしておきましょう。

https://qiita.com/1ntegrale9/items/cb285053f2fa5d0cccdf

### fly.io アカウントの作成
fly.io では CLI を使ってデプロイしていきます。WebCLI とローカルコンソールを使う二通りの方法があります。まずはローカルコンソールを用いた構築について説明していきます。

#### ローカルコンソールを用いた fly.io の構築
始めに `flyctl` コマンドをインストールします。使用している OS に合わせて適宜コマンドを実行して下さい。Windows ではコマンドプロンプトではなく Powershell を使用します。[Window] ボタンを右クリックして一覧から Powershell を起動して下さい。

```console:MAC OS（Homebrew 導入済み）の場合
brew install flyctl
```
```console:MAC OS, Linux の場合
curl -L https://fly.io/install.sh | sh
```
```console:Windows（Powershell）
iwr https://fly.io/install.ps1 -useb | iex
```
インストール時、MAC OS と Linux の場合以下のようなメッセージが出てくると思います。
```console:flyctl install
flyctl was installed successfully to /home/web/.fly/bin/flyctl
Manually add the directory to your $HOME/.bash_profile (or similar)
  export FLYCTL_INSTALL="/home/web/.fly"
  export PATH="$FLYCTL_INSTALL/bin:$PATH"
Run '/home/web/.fly/bin/flyctl --help' to get started
```
Linux では `.bash_profile` に MAC OS では `.zshrc` に以下の行を追加して下さい（ここではユーザ名が web になっていますが各自のユーザ名に置き換えて下さい）。
```bash
export FLYCTL_INSTALL="/home/web/.fly"
export PATH="$FLYCTL_INSTALL/bin:$PATH"
```
追加したら `source ~/.bash_profile` コマンドを実行し設定を反映して下さい（MAC OS の場合 `.zshrc`）。
以降の説明では Powershell を使用していますが、他の OS でも実行するコマンドは同じです。`flyctl` がインストールできたらアカウントを作成します。以下のコマンドを実行して下さい。

```console
flyctl auth signup
```

コマンドを実行するとブラウザが開きます。以下のような画面が表示されるので [Sign up with Github] で GitHub と連携して下さい。メールアドレスでアカウントを作成しても構いませんが GitHub 連携しておいた方が便利だと思います。[Sign up with Github] を選択すると、`Fly.io by fly.io would like permission to:` と表示されるので [Authorize Fly.io] を選択して下さい。
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/a8f90ae3-4527-0cc1-6c62-f1bd0ab6bcec.png"><img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/f79c8326-580c-dac9-2ea7-95154390f941.png">
これでアカウント作成完了です。連携後 `Your FlyCTL should be connected now
Feel free to close this tab` と表示され、コンソールの方でも `successfully logged in as hogehoge@gmail.com` と表示されていれば問題ありません。ただしこのままでは fly.io でデプロイすることはできません。ブラウザに `We need a way to verify your account as being legitimate before you can start deploying apps.` と表示されているはずなので [Add credit card] より支払い方法を追加して下さい（~~PayPal に対応しろ~~）。

#### デプロイ

アカウント作成ができたので、デプロイに必要なファイルを用意していきます。適当なディレクトリを作り、そこに必要なファイルを作成するか GitHub からプルしてきて下さい。必要なファイルは `main.py`, `requirements.txt`, `Dockerfile` になります。この時注意点として、`main.py` のエンコードは UTF-8 にして下さい。UTF-8 でない場合実行時に以下のようなエラーが吐かれます。

```console:encode error
 [info] SyntaxError: Non-UTF-8 code starting with '\x83' in file /workspace/main.py on line 5, but no encoding declared; see https://python.org/dev/peps/pep-0263/ for details
```

main.py としては Discord のリアクションに反応し通知してくれる Bot を例に挙げています。詳細は以下記事を参考にして下さい。各々動かしたい Bot のソースコードに置き換えて下さい。

https://qiita.com/Pumila/items/cf447c24538e13994a38

```python:main.py
import discord
import os

intents=discord.Intents.none()
intents.reactions = True
intents.guilds = True
client = discord.Client(intents=intents)

@client.event
async def on_ready():
    print('ログインしました')

@client.event
async def on_raw_reaction_add(payload):
    txt_channel = client.get_channel(payload.channel_id)
    message = await txt_channel.fetch_message(payload.message_id)
    user = payload.member
    if (message.author == user):
        return
    channel = client.get_channel(CHANNEL_ID)
    msg = f"{message.author.mention} {payload.emoji}\nFrom:{user.display_name} \
          \nMessage:{message.content}\n{message.jump_url}"
    await channel.send(msg)

TOKEN = os.getenv("DISCORD_TOKEN")
CHANNEL_ID = int(os.getenv("CHANNEL_ID"))
client.run(TOKEN)
```

`requirements.txt` ファイルについては `pip freeze` コマンドより必要なライブラリを記載して下さい。

```text:requirements.txt
discord.py==2.1.1
```

最後に `Dockerfile` になります。以下のように作成して下さい。

```text:Dockerfile
FROM python:3.11
WORKDIR /bot
COPY requirements.txt /bot/
RUN pip install -r requirements.txt
COPY . /bot
CMD python main.py
```
ちなみに `Dockerfile` ファイルを用意しなくても fly.io が標準で用意してくれる Builder（paketobuildpacks/builder:base）がありますが、pip でエラーを吐いてデプロイできませんでした。2023/04/09 現在、問題なくデプロイできました。
```console:Build error
Paketo Buildpack for Pip 0.16.4
  Resolving Pip version
    Candidate version sources (in priority order):
      <unknown> -> ""

    Selected Pip version (using <unknown>): 22.3.1

  Executing build process
    Installing Pip 22.3.1
failed to configure pip:

error: exec: "python": executable file not found in $PATH
ERROR: failed to build: exit status 1
Error failed to fetch an image or build from source: executing lifecycle: failed with status code: 51
```

ファイルが用意できたら以下のような状態になっていると思います。

```console
PS C:\Users\hoge\work> ls


    ディレクトリ: C:\Users\hoge\work


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2023/03/12     17:24            129 Dockerfile
-a----        2023/03/12     17:24           1389 main.py
-a----        2023/03/12     17:24             17 requirements.txt
```

ファイルを用意したディレクトリで `flyctl launch` コマンドを実行します。app name として適当な名前を、region として Tokyo, Japan (nrt) を、データベースは使用しないので No を、今すぐデプロイするかと聞かれるので No を選択します。

```console:flyctl launch
PS C:\Users\hoge\work> flyctl launch
Creating app in C:\Users\hoge\work
Scanning source code
Detected a Dockerfile app
? Choose an app name (leave blank to generate one): hoge

? Choose an app name (leave blank to generate one): hoge
automatically selected personal organization: Pumila
Some regions require a paid plan (fra, maa).
See https://fly.io/plans to set up a plan.

? Choose a region for deployment: Tokyo, Japan (nrt)
Created app hoge in organization personal
Admin URL: https://fly.io/apps/hoge
Hostname: hoge.fly.dev
? Would you like to set up a Postgresql database now? No
? Would you like to set up an Upstash Redis database now? No
Wrote config file fly.toml
? Would you like to deploy now? No
Your app is ready! Deploy with `flyctl deploy`
```
上記のように実行すると `fly.toml` ファイルが作成されます。ただし、デフォルトの `fly.toml` ファイルでは http service を使用する設定になっているので以下のように変更します。

```toml:fly.toml
app = "hoge"
kill_signal = "SIGINT"
kill_timeout = 5
processes = []

[env]
```

続いて、環境変数を設定します。以下のようにコマンドを実行して設定して下さい。

```console:環境変数の設定
flyctl secrets set DISCORD_TOKEN=YourAccessToken CHANNEL_ID=1111
```

環境変数の確認には `flyctl secrets list` コマンドを使用します。

```console:環境変数の確認
PS C:\Users\hoge\work> flyctl secrets list
NAME            DIGEST         CREATED AT
CHANNEL_ID      32cebc3        1h21m ago
DISCORD_TOKEN   a481998        1h21m ago
```
最後に `flyctl deploy` コマンドでデプロイして完了です。

#### Web CLI を用いた fly.io の構築

続いて Web CLI を利用した fly.io の構築方法について説明します。基本的な流れはローカルコンソールを用いた手順と同様です。[fly.io Dashboard](https://fly.io/dashboard) より、[Launch on App] を選択し、[Web CLI] を選択します。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/1b140efa-e3df-588c-3108-9abe0d4e64ed.png)
GitHub と連携しておくと以下のようにリポジトリが表示されるのでデプロイしたいものを選択して下さい。
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/5d6c76a7-3195-2657-cf3c-843fc10e9beb.png">
選択すると Web CLI が起動します。ファイルの状態は以下の通りです。`Dockerfile` と `requirements.txt` ファイルの中身はローカルコンソール時のものと同じです。

```console:ファイルの状態
/app/bin/ReactionDetect $ ls
Dockerfile  main.py  README.md  requirements.txt
```

`flyctl launch` コマンドを実行し、app name として適当な名前を、データベースは使用しないので No を、今すぐデプロイするかと聞かれるので No を選択します（region は自動で nrt（Tokyo, Japan）が選択されているみたいです）。

```console:flyctl launch
/app/bin/ReactionDetect $ flyctl launch
Creating app in /app/bin/ReactionDetect
Scanning source code
Detected a Dockerfile app
? Choose an app name (leave blank to generate one): hoge
automatically selected personal organization: Pumila
Created app hoge in organization personal
Admin URL: https://fly.io/apps/hoge
Hostname: hoge.fly.dev
? Would you like to set up a Postgresql database now? No
? Would you like to set up an Upstash Redis database now? No
Wrote config file fly.toml
? Would you like to deploy now? No
Your app is ready! Deploy with `flyctl deploy`
```

上記のように実行すると `fly.toml` ファイルが作成されます。今回も http service は使用しないのでその部分を編集したいのですが、なんとこの Web CLI 何故か `vim` が入っていません（`emacs` も入っていませんでしたが~~それはどうでもいいです~~）。じゃあどうやってファイル編集するんだよとなるのですが、ここで `sed` コマンドの登場です。`cat -n fly.toml` コマンドを実行し削除したい行数を把握します。今回の場合は 9 行目以降を削除したいので `sed -i '9,$d' fly.toml` とコマンドを実行することで編集が可能です。`fly.toml` ファイルが編集できたら、環境変数を設定します。以下のようにコマンドを実行して設定して下さい。
```console:環境変数の設定
flyctl secrets set DISCORD_TOKEN=YourAccessToken CHANNEL_ID=1111
```
環境変数が設定できたら、あとはデプロイするだけです。`flyctl deploy` コマンドを実行して下さい。`--> v0 deployed successfully` と表示されればデプロイ完了です。

## ダウンタイムの検証
続いてダウンタイムの検証に移っていきます。検証期間は約3日間になります。検証に用いたファイル内容については以下記事を参考にして下さい。10秒以上応答しなかったらダウンタイムと見なしています。

https://qiita.com/Pumila/items/29f26fb349d5592046ae

ソースコードは以下になります。

https://github.com/Pumi1a/Downtimetest

結果として、比較的頻繁にダウンタイムがあることが分かりますね（検証が正しければですが…）。長いと10分以上もダウンタイムがあることが分かりました。
![sample-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/6cff93ff-dfd6-983d-25b2-7f0d81c20739.png)
ダウンタイム検証時の Discord チャンネルの様子は以下の通りです。2秒に1回 "test" メッセージを送っているので、10分以上もダウンタイムがあった割には少ないような気がしますね（う～ん、ラズパイが正常に動作していることを信じたいですが、どうなんでしょう。特にサービスのログではエラーがなかったので問題はないと思いますが…）。
<details><summary> Discord チャンネルの様子 </summary><div>

![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/b0eaa924-02e2-84bd-25b8-fb0b87449627.png)

</div></details>


ダウンタイムの検証中、以下のようなエラーが吐かれることがありました。ネットワークトラブルのため、discord.com にアクセスできないエラーですね。

<details><summary> エラーログ </summary><div>

```console:error
 [2023-02-28 17:27:52] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 14.5s behind.
 [2023-02-28 17:27:57] [ERROR ] discord.client: Ignoring exception in on_message
 Traceback (most recent call last):
 File "/usr/local/lib/python3.11/site-packages/aiohttp/connector.py", line 1152, in _create_direct_connection
 hosts = await asyncio.shield(host_resolved)
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/usr/local/lib/python3.11/site-packages/aiohttp/connector.py", line 874, in _resolve_host
 addrs = await self._resolver.resolve(host, port, family=self._family)
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/usr/local/lib/python3.11/site-packages/aiohttp/resolver.py", line 33, in resolve
 infos = await self._loop.getaddrinfo(
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/usr/local/lib/python3.11/asyncio/base_events.py", line 867, in getaddrinfo
 return await self.run_in_executor(
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/usr/local/lib/python3.11/concurrent/futures/thread.py", line 58, in run
 result = self.fn(*self.args, **self.kwargs)
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/usr/local/lib/python3.11/socket.py", line 962, in getaddrinfo
 for res in _socket.getaddrinfo(host, port, family, type, proto, flags):
 File "/usr/local/lib/python3.11/site-packages/aiohttp/client.py", line 536, in _request
 File "/usr/local/lib/python3.11/site-packages/aiohttp/connector.py", line 540, in connect
 [2023-02-28 17:27:57] [ERROR ] discord.client: Ignoring exception in on_message
 File "/usr/local/lib/python3.11/site-packages/aiohttp/resolver.py", line 33, in resolve
 File "/usr/local/lib/python3.11/asyncio/base_events.py", line 867, in getaddrinfo
 File "/usr/local/lib/python3.11/site-packages/aiohttp/connector.py", line 1166, in _create_direct_connection
 Traceback (most recent call last):
 hosts = await asyncio.shield(host_resolved)
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 socket.gaierror: [Errno -3] Temporary failure in name resolution
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/usr/local/lib/python3.11/site-packages/aiohttp/connector.py", line 1152, in _create_direct_connection
 await message.add_reaction(emoji)
 File "/usr/local/lib/python3.11/site-packages/discord/message.py", line 1060, in add_reaction
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 aiohttp.client_exceptions.ClientConnectorError: Cannot connect to host discord.com:443 ssl:default [Temporary failure in name resolution]
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/usr/local/lib/python3.11/site-packages/aiohttp/connector.py", line 874, in _resolve_host
```
```console:error
[2023-03-01 18:49:03] [ERROR ] discord.client: Ignoring exception in on_message
Traceback (most recent call last):
File "/usr/local/lib/python3.11/site-packages/discord/client.py", line 409, in _run_event
await coro(*args, **kwargs)
File "/bot/main.py", line 17, in on_message
await msg.delete()
File "/usr/local/lib/python3.11/site-packages/discord/message.py", line 805, in delete
await self._state.http.delete_message(self.channel.id, self.id)
File "/usr/local/lib/python3.11/site-packages/discord/http.py", line 742, in request
raise DiscordServerError(response, data)
discord.errors.DiscordServerError: 503 Service Unavailable (error code: 0): upstream connect error or disconnect/reset before headers. reset reason: connection failure
```
</div></details>

また API の制限に引っかかっているエラーも吐かれていました。

```console:delete_error
[WARNING ] discord.http: We are being rate limited. DELETE https://discord.com/api/v10/channels/1080105238033338459/messages/1080361559802531862 responded with 429. Retrying in 0.59 seconds.
```

最後に検証で用いた `send.py` と `chk.py` のサービスログを載せておきます。`journalctl -u サービス名`でログの確認ができます。特に問題もなく稼働していることが分かりますね。
<details><summary>サービスのログ</summary><div>

```:send.py log
Feb 28 21:48:31 localhost.localdomain systemd[1]: Started fly.
Feb 28 21:48:32 localhost.localdomain send.py[1532343]: [2023-02-28 21:48:32] [INFO    ] discord.client: logging in using static token
Feb 28 21:48:38 localhost.localdomain send.py[1532343]: [2023-02-28 21:48:38] [INFO    ] discord.gateway: Shard ID None has connected to Gateway (Session ID: 3d19a36b34915a587d2d0db525ccb3f7).
Feb 28 23:14:28 localhost.localdomain send.py[1532343]: [2023-02-28 23:14:28] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Feb 28 23:38:49 localhost.localdomain send.py[1532343]: [2023-02-28 23:38:49] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 01 00:05:25 localhost.localdomain send.py[1532343]: [2023-03-01 00:05:25] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 01 00:41:07 localhost.localdomain send.py[1532343]: [2023-03-01 00:41:07] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 01 01:23:32 localhost.localdomain send.py[1532343]: [2023-03-01 01:23:32] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 01 04:02:36 localhost.localdomain send.py[1532343]: [2023-03-01 04:02:36] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 01 05:28:06 localhost.localdomain send.py[1532343]: [2023-03-01 05:28:06] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 01 07:12:43 localhost.localdomain send.py[1532343]: [2023-03-01 07:12:43] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 01 08:16:33 localhost.localdomain send.py[1532343]: [2023-03-01 08:16:33] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 01 10:42:34 localhost.localdomain send.py[1532343]: [2023-03-01 10:42:34] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 01 13:00:16 localhost.localdomain send.py[1532343]: [2023-03-01 13:00:16] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 01 13:53:23 localhost.localdomain send.py[1532343]: [2023-03-01 13:53:23] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 01 15:19:41 localhost.localdomain send.py[1532343]: [2023-03-01 15:19:41] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 01 19:00:35 localhost.localdomain send.py[1532343]: [2023-03-01 19:00:35] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 01 20:50:09 localhost.localdomain send.py[1532343]: [2023-03-01 20:50:09] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 01 21:22:10 localhost.localdomain send.py[1532343]: [2023-03-01 21:22:10] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 01 23:58:54 localhost.localdomain send.py[1532343]: [2023-03-01 23:58:54] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 02 00:07:21 localhost.localdomain send.py[1532343]: [2023-03-02 00:07:21] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 02 01:55:27 localhost.localdomain send.py[1532343]: [2023-03-02 01:55:27] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 02 02:17:04 localhost.localdomain send.py[1532343]: [2023-03-02 02:17:04] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 02 02:56:25 localhost.localdomain send.py[1532343]: [2023-03-02 02:56:25] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 02 04:17:40 localhost.localdomain send.py[1532343]: [2023-03-02 04:17:40] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 02 06:07:43 localhost.localdomain send.py[1532343]: [2023-03-02 06:07:43] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 02 07:09:38 localhost.localdomain send.py[1532343]: [2023-03-02 07:09:38] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 02 09:21:37 localhost.localdomain send.py[1532343]: [2023-03-02 09:21:37] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 02 12:44:19 localhost.localdomain send.py[1532343]: [2023-03-02 12:44:19] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 02 16:30:38 localhost.localdomain send.py[1532343]: [2023-03-02 16:30:38] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 02 18:50:06 localhost.localdomain send.py[1532343]: [2023-03-02 18:50:06] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 02 18:59:38 localhost.localdomain send.py[1532343]: [2023-03-02 18:59:38] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 02 19:52:59 localhost.localdomain send.py[1532343]: [2023-03-02 19:52:59] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 02 20:55:40 localhost.localdomain send.py[1532343]: [2023-03-02 20:55:40] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 02 22:49:09 localhost.localdomain send.py[1532343]: [2023-03-02 22:49:09] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 03 01:41:10 localhost.localdomain send.py[1532343]: [2023-03-03 01:41:10] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 03 02:52:38 localhost.localdomain send.py[1532343]: [2023-03-03 02:52:38] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 03 06:12:17 localhost.localdomain send.py[1532343]: [2023-03-03 06:12:17] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 03 08:28:32 localhost.localdomain send.py[1532343]: [2023-03-03 08:28:32] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 03 12:09:15 localhost.localdomain send.py[1532343]: [2023-03-03 12:09:15] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 03 14:25:35 localhost.localdomain send.py[1532343]: [2023-03-03 14:25:35] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 03 14:50:52 localhost.localdomain send.py[1532343]: [2023-03-03 14:50:52] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 03 16:00:40 localhost.localdomain send.py[1532343]: [2023-03-03 16:00:40] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 03 17:06:36 localhost.localdomain send.py[1532343]: [2023-03-03 17:06:36] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 03 20:22:36 localhost.localdomain send.py[1532343]: [2023-03-03 20:22:36] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 3d19a36b34915a587d2d0db525ccb3f7.
Mar 03 22:40:22 localhost.localdomain systemd[1]: Stopping fly...
Mar 03 22:40:22 localhost.localdomain systemd[1]: fly.service: Succeeded.
Mar 03 22:40:22 localhost.localdomain systemd[1]: Stopped fly.
```

```:chk.py log
Feb 28 21:48:34 localhost.localdomain systemd[1]: Started fly2.
Feb 28 21:48:34 localhost.localdomain chk.py[1532347]: [2023-02-28 21:48:34] [INFO    ] discord.client: logging in using static token
Feb 28 21:48:40 localhost.localdomain chk.py[1532347]: [2023-02-28 21:48:40] [INFO    ] discord.gateway: Shard ID None has connected to Gateway (Session ID: b713e5fe707145f512425d152d46e0e7).
Feb 28 21:48:41 localhost.localdomain chk.py[1532347]: [2023-02-28 21:48:41] [ERROR   ] discord.client: Ignoring exception in on_raw_reaction_add
Feb 28 21:48:41 localhost.localdomain chk.py[1532347]: Traceback (most recent call last):
Feb 28 21:48:41 localhost.localdomain chk.py[1532347]:   File "/home/web/.local/lib/python3.11/site-packages/discord/client.py", line 409, in _run_event
Feb 28 21:48:41 localhost.localdomain chk.py[1532347]:     await coro(*args, **kwargs)
Feb 28 21:48:41 localhost.localdomain chk.py[1532347]:   File "/home/web/discord/downtimetest/fly.io/chk.py", line 23, in on_raw_reaction_add
Feb 28 21:48:41 localhost.localdomain chk.py[1532347]:     diff = t_add - t_remove
Feb 28 21:48:41 localhost.localdomain chk.py[1532347]:                    ^^^^^^^^
Feb 28 21:48:41 localhost.localdomain chk.py[1532347]: NameError: name 't_remove' is not defined
Feb 28 23:03:18 localhost.localdomain chk.py[1532347]: [2023-02-28 23:03:18] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 01 00:11:32 localhost.localdomain chk.py[1532347]: [2023-03-01 00:11:32] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 01 02:39:15 localhost.localdomain chk.py[1532347]: [2023-03-01 02:39:15] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 01 06:05:38 localhost.localdomain chk.py[1532347]: [2023-03-01 06:05:38] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 01 07:13:51 localhost.localdomain chk.py[1532347]: [2023-03-01 07:13:51] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 01 09:40:02 localhost.localdomain chk.py[1532347]: [2023-03-01 09:40:02] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 01 10:39:05 localhost.localdomain chk.py[1532347]: [2023-03-01 10:39:05] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 01 11:45:58 localhost.localdomain chk.py[1532347]: [2023-03-01 11:45:58] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 01 12:50:13 localhost.localdomain chk.py[1532347]: [2023-03-01 12:50:13] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 01 14:29:27 localhost.localdomain chk.py[1532347]: [2023-03-01 14:29:27] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 01 17:06:46 localhost.localdomain chk.py[1532347]: [2023-03-01 17:06:46] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 01 21:06:48 localhost.localdomain chk.py[1532347]: [2023-03-01 21:06:48] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 01 22:08:49 localhost.localdomain chk.py[1532347]: [2023-03-01 22:08:49] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 02 00:36:52 localhost.localdomain chk.py[1532347]: [2023-03-02 00:36:52] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 02 01:22:47 localhost.localdomain chk.py[1532347]: [2023-03-02 01:22:47] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 02 02:06:41 localhost.localdomain chk.py[1532347]: [2023-03-02 02:06:41] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 02 02:46:57 localhost.localdomain chk.py[1532347]: [2023-03-02 02:46:57] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 02 04:32:27 localhost.localdomain chk.py[1532347]: [2023-03-02 04:32:27] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 02 05:31:35 localhost.localdomain chk.py[1532347]: [2023-03-02 05:31:35] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 02 05:50:22 localhost.localdomain chk.py[1532347]: [2023-03-02 05:50:22] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 02 08:06:17 localhost.localdomain chk.py[1532347]: [2023-03-02 08:06:17] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 02 08:20:37 localhost.localdomain chk.py[1532347]: [2023-03-02 08:20:37] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 02 09:45:01 localhost.localdomain chk.py[1532347]: [2023-03-02 09:45:01] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 02 11:09:00 localhost.localdomain chk.py[1532347]: [2023-03-02 11:09:00] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 02 14:31:38 localhost.localdomain chk.py[1532347]: [2023-03-02 14:31:38] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 02 15:44:57 localhost.localdomain chk.py[1532347]: [2023-03-02 15:44:57] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 02 19:12:02 localhost.localdomain chk.py[1532347]: [2023-03-02 19:12:02] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 02 21:14:30 localhost.localdomain chk.py[1532347]: [2023-03-02 21:14:30] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 03 00:02:18 localhost.localdomain chk.py[1532347]: [2023-03-03 00:02:18] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 03 02:17:04 localhost.localdomain chk.py[1532347]: [2023-03-03 02:17:04] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 03 03:33:10 localhost.localdomain chk.py[1532347]: [2023-03-03 03:33:10] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 03 04:59:11 localhost.localdomain chk.py[1532347]: [2023-03-03 04:59:11] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 03 07:48:47 localhost.localdomain chk.py[1532347]: [2023-03-03 07:48:47] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 03 09:42:01 localhost.localdomain chk.py[1532347]: [2023-03-03 09:42:01] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 03 10:18:31 localhost.localdomain chk.py[1532347]: [2023-03-03 10:18:31] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 03 11:35:28 localhost.localdomain chk.py[1532347]: [2023-03-03 11:35:28] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 03 14:59:23 localhost.localdomain chk.py[1532347]: [2023-03-03 14:59:23] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 03 17:17:04 localhost.localdomain chk.py[1532347]: [2023-03-03 17:17:04] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 03 18:27:50 localhost.localdomain chk.py[1532347]: [2023-03-03 18:27:50] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 03 19:29:29 localhost.localdomain chk.py[1532347]: [2023-03-03 19:29:29] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 03 19:44:28 localhost.localdomain chk.py[1532347]: [2023-03-03 19:44:28] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 03 21:50:48 localhost.localdomain chk.py[1532347]: [2023-03-03 21:50:48] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session b713e5fe707145f512425d152d46e0e7.
Mar 03 22:40:24 localhost.localdomain systemd[1]: Stopping fly2...
Mar 03 22:40:24 localhost.localdomain systemd[1]: fly2.service: Succeeded.
Mar 03 22:40:24 localhost.localdomain systemd[1]: Stopped fly2.
```
</div></details>


## 最後に
結構ダウンタイムが頻繁に起こっていて、安定した Discord Bot の運用はしにくいのではと思いました。Railway の方がはるかに安定していますし、今後 fly.io を利用することはないと思います。また、CLI で操作するの面倒くさいなと思いました。

https://qiita.com/Pumila/items/29f26fb349d5592046ae

## 参考
[Hosting a Python Discord Bot for Free with Fly.io](https://jonahlawrence.hashnode.dev/hosting-a-python-discord-bot-for-free-with-flyio)
[Fly.io Documents](https://fly.io/docs/)
[App Configuration (fly.toml)](https://fly.io/docs/reference/configuration/)