0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

私は大学のサークルのDiscordでBotを開発・運用しています。
最近、BotのサーバーをOracle Cloud Infrastructure (OCI)に移動しました。
OCIはLinuxをターミナルから操作する必要があり、苦戦したので備忘録・サークルの引き継ぎ資料としてこの記事を書きます。

👇サーバーに関する記事はこちら

目次

前書き
想定している読者
OCIの登録
AIに頼ろう
環境構築
メモリスワップ
タイムアウト対策
Dockerの作成
シングルコアの対策
push後の自動反映
push後に自動で反映させる
デプロイ準備
リポジトリのクローンと環境定数
常時起動
Botを常時起動させる
コードの変更
Unicornを消す
運用に向けて
運用・保守コマンド

解決する問題

・環境構築
・Botの常時起動
・push後の自動再起動
・キーの管理

・Dockerの作成手順
・シングルコアの対策

想定している読者

・今からDiscord Botを開発する人
・OCIの操作に迷っている人
・OCIを使っているがBotがうまく動作しない人

OCIの登録

この記事では、OCIの登録は省略します。

👇この記事が分かりやすくておすすめです。

この記事では、それらができた上で行うべき準備や環境構築などを紹介します。

AIに頼ろう

コマンドは必要事項に書き換えたらコピペでできると思います。
エラーが出た場合は、その部分をコピペしてChat GPTなどに投げるといいと思います。
人によって環境が異なる場合があるため、記事のURLをAIに投げて進めると良いかもしれません。

環境構築

メモリスワップ

OCIの無料枠はメモリが1GBしかないため、pipでもフリーズしてしまいます。
そのため、OCIの無料枠で使える200GBのストレージのうち、幾らかをメモリにスワップします。
私は10GBに拡張してBotを運用しています。

1. 既存のスワップを確認(何も出なければなし)

free -h

2. 10GBのスワップファイルを作成

sudo dd if=/dev/zero of=/swapfile bs=1M count=10240
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

3. 再起動後も有効にする

echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

パッケージの更新とインストール

Dockerコマンドを使えるようにしますが、実体は Podman がインストールされます。

sudo dnf update -y
sudo dnf install -y git nano docker
sudo systemctl enable --now docker
# ユーザーをdockerグループに追加(sudoなしで実行するため)
# ※ Oracle Linuxでは以下の手順が必要な場合がある
sudo usermod -aG docker opc
newgrp docker

タイムアウト対策

以前、OCIではなくKoyebとUptimeRobotで運用していた時と比べ、OCIではBotの動作が不安定だと感じることがあります。
そもそも、処理に3秒以上かかるとタイムアウトしてしまうため、無料で使える弱小CPUなどでは対策が必要になります。

Botのコード側での対策

これを防ぐため、コマンド処理の最初にdefer、最後にfollowupという構造で記述することをおすすめします。

@bot.slash_command(name="my_command", description="安全なコマンドのテンプレ")
async def my_command(interaction: discord.Interaction):
    # 【最初】にdefer() を呼ぶ。
    # これにより「考え中...」が表示され、猶予が15分に伸びる。
    # ephemeral=True にすると、コマンドを読んだ人にだけ見えるメッセージになる。
    await interaction.response.defer(ephemeral=False)

    try:
        # --- ここにメインの処理を書く ---

        # 結果は必ずfollowupで送信する。
        # deferの後はresponse.send_messageが使えない。
        await interaction.followup.send(content=result_text)

    except Exception as e:
        # エラー時に「考え中...」のままになることを防ぐためにtry-exceptでキャッチする。
        await interaction.followup.send(f"エラーが発生しました: {e}")

サーバー側での対策

OCIのMTUサイズ(9000)とIPv6設定が原因だったりすることもあるそうです。
(正直、効果があったのかわからない...)

現在の接続名を確認 (例: "Wired connection 1" や "ens3" など)

nmcli connection show

表示された接続名に対してMTUを設定

#例) sudo nmcli connection modify "Wired connection 1" 802-3-ethernet.mtu 1500
sudo nmcli connection modify (接続名) 802-3-ethernet.mtu 1500

変更されたことを確認する

#  接続名の行が "mtu 1500" となっているか確認する
ip addr | grep mtu

IPv6を無効にする

設定ファイルの作成

sudo bash -c 'cat <<EOF > /etc/sysctl.d/99-disable-ipv6.conf
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
EOF'

設定を適用

sudo sysctl --system

ハードウェアオフロードとDNS設定

sudo dnf install -y ethtool
sudo ethtool -K ens3 tx off rx off tso off gso off gro off

DNSをGoogle Public DNSに変更

sudo nmcli connection modify "Wired connection 1" ipv4.dns "8.8.8.8 8.8.4.4"
sudo nmcli connection modify "Wired connection 1" ipv4.ignore-auto-dns yes

設定の反映

sudo nmcli connection up "Wired connection 1"

Dockerの作成

DockerFileはBotのソースコード上に書いてください。
ここにライブラリなどの設定を書いていきます。
(詳しくない人はAIに任せた方が確実だと思います)
以下にテンプレを載せます。

FROM python:3.9

WORKDIR /bot

# --- 1. 日本語環境とタイムゾーン設定 ---
# ログの文字化け(豆腐化)を防ぎ、時間をJST(日本時間)にします
ENV LANG=ja_JP.UTF-8 \
    TZ=Asia/Tokyo \
    PYTHONUNBUFFERED=1

RUN apt-get update && apt-get install -y locales tzdata && \
    localedef -f UTF-8 -i ja_JP ja_JP.UTF-8 && \
    rm -rf /var/lib/apt/lists/*

# --- 2. 必須ツールのインストール ---
# ヘルスチェック用の curl などを入れます
# ※ TeXなど、特定の重いツールが必要な場合はここに追加してください
RUN apt-get update && apt-get install -y curl && \
    rm -rf /var/lib/apt/lists/*

# --- 3. Pythonライブラリの導入 ---
COPY requirements.txt /bot/
RUN pip install --no-cache-dir -r requirements.txt

# ソースコードのコピー
COPY . /bot

# --- 4. ヘルスチェックと起動 ---
# SystemdがBotの生存確認(死活監視)をするために必要です
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD curl --fail http://localhost:8000 || exit 1

# Webサーバー(Uvicorn)ごとBotを起動
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

シングルコア環境での最適化

OCIの無料枠ではCPUがシングルコアなこともあり、処理によっては落ちてしまうことがあります。
そのため、上に書いたdeferと、さらに別の対策を加えます。

非同期化

スプレッドシートの読み書きなどは run_in_executor を使い、イベントループを止めないようにします。

import asyncio

# 良い例:ブロッキング処理を別スレッドへ
loop = asyncio.get_running_loop()
cell_value = await loop.run_in_executor(None, lambda: worksheet.acell("A1").value)

# CPUを他のタスクに譲るための短いsleep
await asyncio.sleep(0.1)

レートリミット対策

「429 Too Many Requests」エラーが出た場合、待機してからリトライする仕組みを入れます。

async def safe_reaction(message, emoji, retry=3):
    try:
        await message.add_reaction(emoji)
    except discord.HTTPException as e:
        # 429エラー または エラー文言に "rate" が含まれる場合
        if retry > 0 and (e.status == 429 or "rate" in str(e).lower()):
            await asyncio.sleep(2.0) # 2秒待機
            await safe_reaction(message, emoji, retry - 1)

push後に自動で反映

現状、pushしてもサーバー上のBotは更新されません。
そこで、GitHub Actionを用いて、自動で再起動するようにします。
サーバーではなく、Botのコードの方に.githubを作成し、その中にworkflowsというフォルダを作成してください。そして、その中にdeploy.ymlというファイルを作成してください。
以下にdeploy.ymlのテンプレを載せます。


name: Deploy to OCI

on:
  push:
    branches:
      - main  # mainブランチにpushされたら実行

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: SSH Remote Commands
        uses: appleboy/ssh-action@v1.0.3
        with:
          # 手順3で設定したシークレットを読み込みます
          host: ${{ secrets.HOST_IP }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.PRIVATE_KEY }}
          port: 22
          timeout: 15m # Microインスタンスはビルドが遅いので長めに確保
          script: |
            # 1. リポジトリのディレクトリへ移動
            # ★重要: 下のディレクトリ名は実際の環境に合わせて書き換えてください
            cd /home/opc/Your-Repo
            
            # 2. 最新のコードを取得
            git pull origin main
            
            # 3. Dockerイメージを再ビルド
            # OCIはdockerコマンドでpodmanが動くようになっています
            docker build -t my-discord-bot .
            
            # 4. サービスを再起動して変更を反映
            sudo systemctl restart discord-bot.service
            
            # 5. お掃除 (溜まった古いイメージを削除)
            docker image prune -f

ここにあるsecrets.HOSTecrets.USERNAMEsecrets.PRIVATE_KEYは以下の手順で確認できます。

認証情報の準備

GitHubがサーバーと接続できるように鍵を設定します。

ssh-keygen -t ed25519 -f github_action_key -N "" -C "github-actions"

これを実行すると、以下のようなものが出たら成功です。

Generating public/private ed25519 key pair.
Your identification has been saved in github_action_key
Your public key has been saved in github_action_key.pub
The key fingerprint is:
SHA256:rU1g+... (ランダムな文字列) ... github-actions
The key's randomart image is:
+--[ED25519 256]--+
|                 |
|       .         |
|      o          |
|     . + +       |
|      o S o      |
|     . + O .     |
|      . B = .    |
|       o * +     |
|        E.o.     |
+----[SHA256]-----+

これで公開鍵と秘密鍵が生成でき、それぞれをサーバーとGitHubに登録します。

公開鍵をサーバーに登録する

# サーバーではなくパソコン上で実行し、`ssh-ed25519 AAAAC3NzaC... github-actions`を改行を含まず**全て**コピーしてください。
cat github_action_key.pub

その後、以下のコマンドを実行し、一番下の行に付け加えてください。

nano ~/.ssh/authorized_keys

最後に、以下のコマンドを実行してPCと接続できれば成功です。

# サーバーではなくパソコン上で実行
ssh -i github_action_key opc@(サーバーIPアドレス)
#例) ssh -i github_action_key opc@123.4.56.789

秘密鍵をGitHubに登録する

以下のコマンドを執行し、-----BEGIN OPENSSH PRIVATE KEY-----から-----END OPENSSH PRIVATE KEY-----まで、最初と最後の行を含めて全てコピーしてください

# サーバーではなくパソコン上で実行
cat github_action_key

その後、リポジトリのページのSettings > Secrets and variables > Actions > New repository secretを開き、ワークフローのNamePRIVATE_KEYを記入し、Secretにコピーした内容をペーストしてください。
これでキーの設定が完了しました。
その後、pushしたら反映され、それ以降、pushがあると自動で再起動されます。

デプロイ準備

リポジトリのクローンと環境定数

Bot自体のソースコードはGitHubで管理していることを前提に書きます。
リポジトリからcloneをしてソースコードはGitHubから引っ張れるので、キーなどの環境定数はOCI上に書く必要があります。

クローン

git clone (リポジトリのURL)
cd Your-Repo

.envの作成

nano .env

nanoを実行すると何もない画面に切り替わると思います。ここに、以下のように環境定数を書いていきます。
注意して欲しいのは、=の間に を入れないでください。

.env
BOT_KEY=123456789
CHANNEL_ID=123456789

ビルド

docker build -t my-discord-bot .

# テスト起動 (Ctrl+Cで停止)
docker run --rm \
  --env-file .env \
  -v $(pwd)/google_secret.json:/bot/google_secret.json:Z \
  my-discord-bot

Systemdによる常時起動

docker run -d だけではサーバーとパソコンの接続が切れた時にBotも一緒に落ちてしまいます。
そこで、systemdを用いて常時起動させます。

ファイルの作成

sudo nano /etc/systemd/system/discord-bot.service

nanoエディター内で以下のコードをコピペしてください。

[Unit]
Description=Discord Bot Container Service
Requires=network-online.target
After=network-online.target

[Service]
# コンテナをフォアグラウンドで起動し、systemdに監視させる
Type=simple
Restart=always
# 停止しても5秒後に自動で再起動
RestartSec=5s

User=opc
# ★重要: ディレクトリ名は実際の環境に合わせて変更してください
WorkingDirectory=/home/opc/Your-Repo

# Podman起動コマンド
# --rm: 停止時にコンテナを削除 (名前衝突防止)
# -p 8000:8000: Uvicornのポートをホスト側と繋ぐ (ヘルスチェック用)
# --memory: Microインスタンス(1GB)用に調整。Ampereなら4gでもOK
ExecStart=/usr/bin/podman run --name my-bot --rm \
    -p 8000:8000 \
    --env-file /home/opc/Your-Repo/.env \
    --memory 900m \
    my-discord-bot

# 停止コマンド (10秒待って終了しなければ強制終了)
ExecStop=/usr/bin/podman stop -t 10 my-bot

[Install]
WantedBy=multi-user.target

有効にして起動

# 設定の反映と自動起動の有効化
sudo systemctl daemon-reload
sudo systemctl enable discord-bot.service

# 起動
sudo systemctl start discord-bot.service

# 状態確認 (Active: active (running) ならOK)
sudo systemctl status discord-bot.service

コードの変更

私のようなKoyebを利用したデプロイをOCIに切り替える時、コードの変更は不要ですが、一点だけ変更が必要です。

Unicornを消す

main.pyの一番下とかに以下のようなコードがあれば消してください。
私はこれを消すのを忘れており、処理がすぐにタイムアウトしてするなどの問題が発生していました。

main.py
import uvicorn
if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

運用・保守コマンド

サーバーに接続する(SSH接続)

ssh -i (SSH接続のKeyを保存しているパス) opc@(サーバーのIPアドレス)
#例) ssh -i '/Users/tanami/Oracle_Cloud_Key.key' opc@123.4.56.789

Botのディレクトリに移動

cd Discord_Bot

Botの起動

docker stop my-bot
docker rm my-bot
sudo systemctl daemon-reload
sudo systemctl start discordbot
sudo systemctl enable discordbot

実行状況の確認

sudo systemctl status discordbot

ログを見る

sudo journalctl -u discordbot -f
# Ctrl+Cで閉じれます

サーバー再起動

sudo reboot

現在のコミットを確認

git log -1 --oneline
git rev-parse HEAD
git show --oneline -s HEAD
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?