見出し画像

¥0で常時稼働マイクラサーバーを建てる(全文無料)

マイクラ始めて3日目の人が、¥0でほぼ常時稼働のマイクラサーバーを頑張って建てたので、その備忘録としてここに記す。コード全文あり。


Java版マイクラサーバーとは?

今回はJava版のサーバーを建てることになる。

マイクラには統合版とJava版が存在し、MacはJava版のみがプレイできる。

またJavaのサーバーは、Discordのサーバーのようにボタン一つで作成できるものではなく、ガチのサーバーを用意し、Javaのファイルを実行し、ポートを開いてそのアドレスを公開する必要がある。

それが面倒な人向けに、Realmsと呼ばれる公式の鯖や、Xserver for Gameといったレンタルサーバーがあるが、どれも有料である。(Aternos等、無料のサービスも存在するが、常時稼働できない、重いなど、快適なプレイとは程遠い状態である)

試行錯誤

ここでは完成に至るまでの試行錯誤を記す。結果だけ知りたい人は読み飛ばして欲しい。

メモリ問題

マイクラサーバーに必要なRAMは、一人当たり500MBという記事を見かけた。10人くらいで遊べたらいいなぁと思っていたので、RAMは5GB程度が必要ということになる。

無料の仮想マシンといえば、GoogleのCompute Engine、AWSのEC2、MicrosoftのAzureなどが有名であるが、どれもRAMは1GB以下である。話にならない。

調べているとOCI Cloud Free Tierというものを見つけた。どうやら24GBのRAMが無料で使えるらしい。(本当か?)早速登録しようとしたが、クレジットカードの登録がクリアできなかった。デビットカードでも良いと書かれていたので、クレジットカードを所持しない小生はデビットカードを登録したが、それがいけなかったのかもしれない。とりあえず登録できないものはできないので諦める。

ここで目を付けたのが、みんな大好きGoogle Colaboratoryである。Colabでは無料にもかかわらず、12GBのRAMが提供される。(TPUにすると300GBになる???)これを使うしかない。

しかし、Google Colaboratoryは12時間で強制終了される。(巷では12時間ルールと呼ばれる)12時間おきに起動し直せば良いのだが、その間に数分間の空白ができてしまう。これが「ほぼ」常時稼働と言った所以である。

遅延問題

PCとサーバーの間に距離があれば、やはり遅延が発生してしまう。サーバーはなるべくアジアにあって欲しい。

Colabで

!curl ipinfo.io

と実行すると、Colabがどこのサーバーで実行されているかを確認することができる。100回ほど繰り返したところ、

Taipei
Las Vegas
Salt Lake City
North Charleston
Council Bluffs
Washington
Groningen

からランダムで選択されているようだった。
台北がある! かなり近い!
ということで、台北が出るまで再接続を繰り返し、台北を引いたらそれを使うことにする。

ポート解放問題

Colabでマイクラサーバーを動かすのは良いが、みんながそこにアクセスできるように、ポートを解放しなければいけない。しかし、Colabではそんなことができないので、「トンネリング」というものを使う。簡単にいうと、ColabとPCの間に仲介役のようなものを置いて、Colabにアクセスできるようにするのだ。

トンネリングで一番有名なものは「ngrok」である。
まずngrokを試してみた。遅延も少なく、かなり良かったが、無料版では繋ぎ直すたびにアドレスが変わってしまう。遊んでもらうときに毎回アドレスを入力してもらうのは面倒なので他のサービスを探す。

次に「bore」というものを試してみた。大変手軽であったが、遅延が400msとなかなか酷いものであった。考えるとわかるが、サーバーが台北にあっても、トンネルが北米を経由していては意味が無い。ということで海外製のトンネリングサービスは除外するしかなさそうだ。(ngrokは日本にもサーバーがあるので、アドレスの変更が気にならないのであれば十分使える)

次に「TCP Exposer」という国産のサービスを見つけた。良い感じ。使えそう。登録すればアドレスも固定できる。

次に「OwnServer」という国産のサービスも見つけた。こちらのサービスをなんとなく見ていると、「自前のサーバーでトンネリングを行う」という記述があった。
自前……? なるほど。国内にポート解放できる自前のサーバーを用意すれば、台北からそこを経由して接続できるのでは????

先ほど挙げた、Compute Engine、EC2、Azureといったサービスは自分の好きなポートを解放することができる。この中でもAzureは無料で東京のサーバーが使えるっぽい(?)

ということで今回は「frp」というものを使って、ColabからAzureを経由して接続するという方針でいく。

無料サーバーを建てる(失敗)

お待たせした。

Azureの用意

  1. AzureのVMを立ち上げる

  2. 適当なポートを2つ開く(Colab接続用と、PC接続用)

  3. SSH接続できるようにする

  4. VMにfrpを入れる

  5. tmuxを入れて、tmuxでfrpを実行

Colabの用意

  1. マイクラサーバーのjavaファイルをダウンロードし、実行

  2. frpを入れて実行

これで一応完成だが、Colabの実行ボタンは誰が押すのか……?
Compute Engineに押してもらう。

Compute Engineの用意

  1. https://qiita.com/Brutus/items/22dfd31a681b67837a74
    https://zenn.dev/taksas/articles/1f8e0f3676628d
    を参考にVMを立ち上げる

  2. VMにseleniumを入れる

  3. 気合いでGoogleログインし、ChromeのUserProfileを作成する

Colabを実行するには、当たり前だがGoogleのログインが必要である。「selenium google ログイン」と検索すると出てくる記事にある通り、一度ログインしてしまえば、UserProfileを使い回すことができる。ログインする方法だが、当初リモートデスクトップ接続でログインしようとしたがメモリが足りなさすぎて動かなかった。ので、ActionChainsとdriver.save_screenshotを駆使してCUI上で気合いでログインした。

これで12時間ごとに実行ボタンを押せば良いのだが、どうやら実行開始数分後にreCAPTCHAの画面が現れ、これをクリアしないと40分で実行が打ち切られるようだ。巷では90分ルールと呼ばれるが、これがどうやら40分に短縮されているようだ。さらに、ページをリロードしたり、マウスをクリックすれば回避できるというのが定説であったが、reCAPTCHAが導入されて以降は、どんなにユーザーのアクションがあったとしても、reCAPTCHAをクリアしないと強制的に切られるようである。困った。

探していると「Selenium-recaptcha-solver」なるものを見つけた。試してみると上手くいったので、これを導入して無料サーバーの完成である。

追記 試行錯誤その2

あ、

ということで、動かし始めて丸1日ほどが経ったとき、GoogleからBANされてしまった。もうColabは使えない……

Colabの代替を探す

まず初めに見つけたのは「Paperspace」というものだ。Colab同様、無料でGPUが使えると謳われており(今回欲しいのはGPUではなくRAMなのだが)、試しに登録した。

いざ動かそうとすると、無料枠は混雑のため、すぐには使えないとのこと。なんだこれ。使えないじゃないか。

次に見つけたのは「Kaggle」というものだ。Colabと同じように使える。触ってみると、最大実行時間は12時間、RAMは30GB、さらにAPIでコマンドからバックグラウンド実行できるというColabの上位互換!? と思うような機能を備えていることがわかった。しかもGoogleサーバーで動いているらしく、例のごとくガチャで台北が出ることが判明。

これを使うことにする。

真の完成へ

ここでは最終的に完成したサーバーの作り方を1から説明する。

稼働時間について

Kaggleは連続12時間使用できるが、サーバーの起動などにも時間が必要なため、12時間丸々マイクラサーバーを動かすことはできない。
今回は、

① 5:35 Kaggle起動、台北ガチャ、サーバーダウンロード開始
 6:00 サーバー稼働開始
 17:27 サーバー停止、サーバーアップロード開始
 17:30 Kaggle停止

② 17:05 Kaggle起動、台北ガチャ
 17:28 サーバーダウンロード開始
 17:30 サーバー稼働開始
 翌5:00 サーバー停止、サーバーアップロード開始
 5:03 Kaggle停止

という2つを回すことにする。利用者からは、
6:00 - 17:27 17:30 - 翌5:00
で稼働しているように見える。

Azureの用意(⚠️追記3に追記あり)

① Azureの無料アカウント登録
クレジットカード or デビットカードが必要

② AzureのVMを立ち上げる

③ ポートを解放する

適当なポートを2つ開ける。今回は8123と25565を開ける。
Azure ホーム > ネットワークセキュリティグループ > 設定 > 受信セキュリティ規則 > 追加
と進むと、追加の画面が出るので、宛先ポート範囲を8123, 25565として、その他は変更せずに追加ボタンを押す。

④ SSH接続できるようにする

$ ssh (VMのユーザー名)@(VMのip または DNS名)

でログインする。SSH接続した状態で、これ以降の作業を行う。

⑤ TimeZoneを東京に

$ sudo timedatectl set-timezone Asia/Tokyo

⑥ tmuxをインストール

$ sudo apt install tmux

⑦ frpを追加し、frps.tomlを書き換え

$ wget -q https://github.com/fatedier/frp/releases/download/v0.58.1/frp_0.58.1_linux_amd64.tar.gz
$ tar -xzf frp_0.58.1_linux_amd64.tar.gz
$ cd frp_0.58.1_linux_amd64
$ vi frps.toml

bindPort = 8123 に書き換える

$ cd

⑧ frpを起動

$ tmux
$ cd frp_0.58.1_linux_amd64
$ ./frps -c ./frps.toml

GoogleDriveの用意

① マイクラのserver.jarをダウンロード

https://www.minecraft.net/ja-jp/download/server

公式は↑こちら。自分はfabricのserver.jarを用意した。(公式は重いらしい)

② zipを作成

「Minecraft-server」という名前のファルダを作り、そこに「server.jar」、「eula.txt」、「frpc.toml」を入れて、「Minecraft-server.zip」を作成する。

eula.txt

eula=true

これは利用規約に同意するということを示すファイル。利用規約に目を通してから作成したい。

frpc.toml

serverAddr = "AzureのIPアドレス"
serverPort = 8123

[[proxies]]
name = "test-tcp"
type = "tcp"
localIP = "127.0.0.1"
localPort = 25565
remotePort = 25565

こちらは後ほどKaggleにコピーして使う。

③ zipをGoogleDriveにアップロード

場所はどこでも良いが、「Minecraft-server.zip」という名前のファイルはDrive内に一つだけにしておく。

④ GoogleDrive APIの有効化

Google Cloudに登録してから、有効化する。

token.jsonは後ほど使う。

Kaggleの用意

① Kaggleのアカウントを用意し、Notebookを作成

このときのNotebook名とアカウント名を繋げた、「アカウント名/Notebook名」という文字列を後ほど使う。

② 上の Add-ons > Secrets > Add と進み、Labelを「token」、Valueを先ほどのtoken.jsonの中身をそのまま貼り付けて、Saveを押す

③ 次のコードを貼り付け

import datetime
import time

jst = datetime.timezone(datetime.timedelta(hours=9))

def dt(t, min = 0, sec = 0, day = 0):
    return datetime.datetime.combine(datetime.datetime.now(jst).date(), t, jst) + datetime.timedelta(days=day, minutes=min, seconds=sec)
def dt2(t, min = 0, sec = 0, day = 0):
    return t + datetime.timedelta(days=day, minutes=min, seconds=sec)

def wait_until(t):
    while True:
        if datetime.datetime.now(jst) > t:
            break
        time.sleep(0.1)

times = [
    [
        dt(datetime.time(0, 0, 0)),
        dt(datetime.time(5, 0, 0))
    ],
    [
        dt(datetime.time(6, 0, 0)),
        dt(datetime.time(17, 27, 0))
    ],
    [
        dt(datetime.time(17, 30, 0)),
        dt(datetime.time(5, 0, 0), day = 1) 
    ]
]
now = datetime.datetime.now(jst)


def start(ti):
    import json
    import os

    res = os.popen('curl ipinfo.io').read()
    data = json.loads(res)
    print(data)

    if 'Asia' not in data['timezone'] and now < dt2(ti[0], -5):
        pass
    else:
        !sudo apt-get update > /dev/null 2>&1
        !sudo apt install -y openjdk-17-jdk > /dev/null 2>&1
        %cd /kaggle/working
        !wget -q https://github.com/fatedier/frp/releases/download/v0.58.1/frp_0.58.1_linux_amd64.tar.gz > /dev/null 2>&1
        !tar -xzf frp_0.58.1_linux_amd64.tar.gz > /dev/null 2>&1

        from kaggle_secrets import UserSecretsClient
        secret_value = UserSecretsClient().get_secret("token")
        with open('token.json', 'w') as token:
            token.write(secret_value)
            token.close()

        !pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib > /dev/null 2>&1

        import os.path
        import io
        from google.auth.transport.requests import Request
        from google.oauth2.credentials import Credentials
        from googleapiclient.discovery import build
        from googleapiclient.http import MediaIoBaseDownload
        from googleapiclient.http import MediaFileUpload
        from googleapiclient.errors import HttpError

        def download():
            SCOPES = ['https://www.googleapis.com/auth/drive']
            creds = None

            if os.path.exists('/kaggle/working/token.json'):
                creds = Credentials.from_authorized_user_file('/kaggle/working/token.json', SCOPES)
            if not creds or not creds.valid:
                if creds and creds.expired and creds.refresh_token:
                    creds.refresh(Request())
                with open('/kaggle/working/token.json', 'w') as token:
                    token.write(creds.to_json())
            try:
                service = build('drive', 'v3', credentials=creds)
                results = service.files().list(
                    q="name = 'Minecraft-server.zip'",
                    pageSize=1, 
                    fields="files(id, name)"
                ).execute()
                items = results.get('files', [])
                if not items:
                    print('No files found.')
                    return
                file_id = items[0]["id"]
                request = service.files().get_media(fileId=file_id)
                file = io.BytesIO()
                downloader = MediaIoBaseDownload(file, request)
                done = False
                while done is False:
                    status, done = downloader.next_chunk()
                    print(f"Download {int(status.progress() * 100)}%")
                file.seek(0)
                with open('/kaggle/working/Minecraft-server.zip', 'wb') as f:
                    f.write(file.read())
                    f.close()
            except HttpError as error:
                print(f'An error occurred: {error}')

        print("Wait...")
        wait_until(dt2(ti[0], -1, -30))
        download()
        time.sleep(5)
        !unzip /kaggle/working/Minecraft-server.zip > /dev/null 2>&1
        print("Downloaded Server", datetime.datetime.now(jst))

        !cp -f /kaggle/working/Minecraft-server/frpc.toml /kaggle/working/frp_0.58.1_linux_amd64/frpc.toml > /dev/null 2>&1

        import subprocess

        def upload():
            SCOPES = ['https://www.googleapis.com/auth/drive']
            creds = None

            if os.path.exists('/kaggle/working/token.json'):
                creds = Credentials.from_authorized_user_file('/kaggle/working/token.json', SCOPES)
            if not creds or not creds.valid:
                if creds and creds.expired and creds.refresh_token:
                    creds.refresh(Request())
                with open('/kaggle/working/token.json', 'w') as token:
                    token.write(creds.to_json())
            try:
                service = build('drive', 'v3', credentials=creds)
                results = service.files().list(
                    q="name = 'Minecraft-server.zip'",
                    pageSize=1, 
                    fields="files(id, name)"
                ).execute()
                items = results.get('files', [])
                if not items:
                    print('No files found.')
                    return
                file_id = items[0]["id"]
                file_metadata = {'name': 'Minecraft-server.zip'}
                media = MediaFileUpload(
                    '/kaggle/working/Minecraft-server.zip', 
                    mimetype='application/zip', 
                    resumable=True
                )
                file = service.files().update(
                    fileId=file_id, body=file_metadata, media_body=media, fields='id'
                ).execute()

            except HttpError as error:
                print(f'An error occurred: {error}')

        def main():
            %cd /kaggle/working/Minecraft-server

            server_process = subprocess.Popen(
                ['java', '-Xms27G', '-Xmx27G', '-jar', 'server.jar', 'nogui'],
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True
            )

            stop_flag = True
            def read_logs(process):
                while stop_flag:
                    log = process.stdout.readline()
                    if log != "":
                        print(log)
                    time.sleep(0.01)
            def read_stderr(process):
                while stop_flag:
                    log = process.stderr.readline()
                    if log != "":
                        print(log)
                    time.sleep(0.01)
            threading.Thread(target=read_logs, args=(server_process,)).start()
            threading.Thread(target=read_stderr, args=(server_process,)).start()

            def send_msg(msg):
                server_process.stdin.write(msg)
                server_process.stdin.flush()

            wait_until(dt2(ti[1], -5))
            send_msg("say このサーバーは5分後に一時停止します\n")
            send_msg("say 稼働時間は6:00-17:27 17:30-翌5:00です\n")

            wait_until(dt2(ti[1], -1))
            send_msg("say このサーバーは1分後に一時停止します\n")
            send_msg("say 稼働時間は6:00-17:27 17:30-翌5:00です\n")

            wait_until(dt2(ti[1], sec=-10))
            send_msg("say このサーバーは10秒後に一時停止します\n")
            send_msg("say 稼働時間は6:00-17:27 17:30-翌5:00です\n")

            wait_until(ti[1])
            send_msg("stop\n")

            time.sleep(3)
            stop_flag = False
            time.sleep(10)
            %cd /kaggle/working
            !rm Minecraft-server.zip
            !zip -r Minecraft-server.zip Minecraft-server > /dev/null 2>&1
            print("zip")
            time.sleep(1)
            upload()
            print("Upload Server", datetime.datetime.now(jst))
            
        def main2():
            %cd /kaggle/working/frp_0.58.1_linux_amd64
            !./frpc -c ./frpc.toml

        import threading

        threading.Thread(target=main).start()
        time.sleep(30)
        wait_until(dt2(ti[0], sec=-2))
        threading.Thread(target=main2).start()

        wait_until(dt2(ti[1], sec=5))
        !ps aux | grep ./frpc | grep -v grep | awk '{print $2}' | xargs kill -9

        time.sleep(120)
        print("exit")


i = len(times) - 1
while i >= 0:
    if dt2(times[i][0], -25) <= now and now < dt2(times[i][1], -8):
        start(times[i])
        break
    i -= 1

コードがきたないなぁ、と思う人は勝手に書き換えてくれ。

④ インターネットをONにする

SMS認証を済ませると、インターネットの設定ができるので、ONにする

⑤ Save version > Save を押す

これでなんともうマイクラサーバーが起動して使える。ここでは一旦、左下からStop Sessionしておく。

⑥ Kaggle API の kaggle.json をダウンロード

右上自分のアイコンをクリック > My Account > APIの項目の"Create New API Token"をクリック。jsonをダウンロードしておく。

台北ガチャを自動で引く

① 再びAzureにログイン

$ ssh (VMのユーザー名)@(VMのip または DNS名)

② Kaggleライブラリのダウンロード

$ pip install kaggle

③ .kaggle フォルダの作成

$ mkdir .kaggle

④ SSH接続を解除(ターミナル再起動)

⑤ kaggle.json を送る

$ scp (ダウンロードしたkaggle.jsonのパス) (VMのユーザー名)@(VMのip または DNS名):~/.kaggle/

⑥ kaggle_loop.py を送る

kaggle_loop.py

import subprocess
import time
import datetime

PULL_COMMAND = "kaggle kernels pull アカウント名/Notebook名 -p ~/kaggle -m"
PUSH_COMMAND = "kaggle kernels push -p ~/kaggle"
STATUS_COMMAND = "kaggle kernels status アカウント名/Notebook名"

def wait_until(t):
    while True:
        if datetime.datetime.now() > t:
            break
        time.sleep(1)

def run_command(command):
    process = subprocess.run(command, shell=True, capture_output=True, text=True)
    return process.stdout

def check_status():
    status_output = run_command(STATUS_COMMAND)
    return "running" in status_output

times = [
    datetime.datetime.combine(datetime.datetime.now().date(), datetime.time(5, 35, 0)),
    datetime.datetime.combine(datetime.datetime.now().date(), datetime.time(17, 5, 0))
]

def main(num):
    print("Wait until: ", times[num])
    wait_until(times[num])

    run_command(PULL_COMMAND)
    run_command(PUSH_COMMAND)
    print("try")

    count = 0
    while True:
        time.sleep(10)
        if check_status():
            count += 1
            if count >= 6:
                print("success")
                times[num] += datetime.timedelta(days=1)
                main((num + 1) % len(times))
                break
        else:
            print("retry")
            count = 0
            run_command(PUSH_COMMAND)

for i in range(len(times)):
    if datetime.datetime.now().time() > times[i].time():
        times[i] += datetime.timedelta(days=1)
    else:
        main(i)
        break
else:
    main(0)
$ scp (kaggle_loop.pyのパス) (VMのユーザー名)@(VMのip または DNS名):~/

⑦ kaggle_loop.py を起動

$ ssh (VMのユーザー名)@(VMのip または DNS名)
$ tmux
$ python3 kaggle_loop.py

これで、5:35か17:05になると動き出す!

マイクラから接続

マイクラを起動し、アドレス「VMのIPアドレス:25565」を追加

最後に

Kaggleの使用目的が本来のものとは違う気がするので、これを行う場合は自己責任で。

一週間後 追記

サーバーが動き始めてから一週間後、

google.auth.exceptions.RefreshError: ('invalid_grant: Token has been expired or revoked.', {'error': 'invalid_grant', 'error_description': 'Token has been expired or revoked.'})

というエラーが。どうやらDriveのAPIの認証画面で、テストモードのままにしておくと一週間で認証が切れるようだ。

どうやらDriveの「全てのファイルを操作」という権限を与えている状態では、テストモードから本番モードにするのにGoogleの承認が必要なようである。調べたところ、プライバシーポリシーやYouTubeの説明動画を用意しなければならないという。超めんどくさそう。

というわけで権限を減らした状態で作り直す。

機密性の高いスコープを削除

OAuth同意画面作成の2ページ目、「機密性の高いスコープ」と「制限付きのスコープ」と書かれているものを全て削除。(「非機密のスコープ」に「./auth/drive.file」というものがあればOK)

そのまま保存し、「アプリを公開」をクリック

credentials.jsonの作成

GCPの管理画面の「IAMと管理」>「サービスアカウント」> メールアドレスらしきものをクリック > 「キー」>「鍵を追加」>「JSON」と進むとJSONファイルがダウンロードされるので「credentials.json」という名前に変える。そして、その中身をまるまるKaggleのSecretsに「credentials」として登録

zipのアップロード

Driveの任意のフォルダを共有フォルダに設定し、他の人も編集できる設定にしておく。そして、そのフォルダのフォルダIDをコピー。(URLの「folders/」の後に続く文字列)

自分のPCで

$ pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib

を実行。

credentials.jsonがあるフォルダに以下のpythonファイルを作成。

from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload

creds = service_account.Credentials.from_service_account_file(
          'credentials.json',
          scopes=['https://www.googleapis.com/auth/drive.file']
        )

drive_service = build('drive', 'v3', credentials=creds)

file_name = 'Minecraft-server.zip'
file_metadata = {
  'name': file_name,
  'parents': ['<DriveのフォルダID>'],
}

media = MediaFileUpload(file_name, mimetype='application/zip')
file = drive_service.files().create(
          body=file_metadata,
          media_body=media,
          fields='id'
        ).execute()

print(f'File ID: {file.get("id")}')

Minecraft-server.zipも同じフォルダに置き、pythonを実行。
ファイルIDが出力されるので、KaggleのSecretsに「fileId」として登録。

Kaggleを書き換え

import datetime
import time

jst = datetime.timezone(datetime.timedelta(hours=9))

def dt(t, min = 0, sec = 0, day = 0):
    return datetime.datetime.combine(datetime.datetime.now(jst).date(), t, jst) + datetime.timedelta(days=day, minutes=min, seconds=sec)
def dt2(t, min = 0, sec = 0, day = 0):
    return t + datetime.timedelta(days=day, minutes=min, seconds=sec)

def wait_until(t):
    while True:
        if datetime.datetime.now(jst) > t:
            break
        time.sleep(0.1)

times = [
    [
        dt(datetime.time(0, 0, 0)),
        dt(datetime.time(5, 0, 0))
    ],
    [
        dt(datetime.time(6, 0, 0)),
        dt(datetime.time(17, 27, 0))
    ],
    [
        dt(datetime.time(17, 30, 0)),
        dt(datetime.time(5, 0, 0), day = 1) 
    ]
]
now = datetime.datetime.now(jst)


def start(ti):
    import json
    import os

    res = os.popen('curl ipinfo.io').read()
    data = json.loads(res)
    print(data)

    if 'Asia' not in data['timezone'] and now < dt2(ti[0], -5):
        pass
    else:
        !sudo apt-get update > /dev/null 2>&1
        !sudo apt install -y openjdk-17-jdk > /dev/null 2>&1
        %cd /kaggle/working
        !wget -q https://github.com/fatedier/frp/releases/download/v0.58.1/frp_0.58.1_linux_amd64.tar.gz > /dev/null 2>&1
        !tar -xzf frp_0.58.1_linux_amd64.tar.gz > /dev/null 2>&1

        from kaggle_secrets import UserSecretsClient
        credentials = UserSecretsClient().get_secret("credentials")
        with open('/kaggle/working/credentials.json', 'w') as token:
            token.write(credentials)
            token.close()
        fileId = UserSecretsClient().get_secret("fileId")

        !pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib > /dev/null 2>&1

        import os.path
        import io
        from google.oauth2 import service_account
        from googleapiclient.discovery import build
        from googleapiclient.http import MediaFileUpload
        from googleapiclient.http import MediaIoBaseDownload

        def download():
            creds = service_account.Credentials.from_service_account_file('/kaggle/working/credentials.json', scopes=['https://www.googleapis.com/auth/drive.file'])
            drive_service = build('drive', 'v3', credentials=creds)
            request = drive_service.files().get_media(fileId=fileId)
            file = io.BytesIO()
            downloader = MediaIoBaseDownload(file, request)
            done = False
            while done is False:
                status, done = downloader.next_chunk()
                print(f"Download {int(status.progress() * 100)}%")
            file.seek(0)
            with open('/kaggle/working/Minecraft-server.zip', 'wb') as f:
                f.write(file.read())
                f.close()

        print("Wait...")
        wait_until(dt2(ti[0], -1, -20))
        download()
        time.sleep(1)
        !unzip /kaggle/working/Minecraft-server.zip > /dev/null 2>&1
        print("Downloaded Server", datetime.datetime.now(jst))

        !cp -f /kaggle/working/Minecraft-server/frpc.toml /kaggle/working/frp_0.58.1_linux_amd64/frpc.toml > /dev/null 2>&1

        import subprocess

        def upload():
            creds = service_account.Credentials.from_service_account_file('/kaggle/working/credentials.json', scopes=['https://www.googleapis.com/auth/drive.file'])
            drive_service = build('drive', 'v3', credentials=creds)
            file_metadata = { 'name': 'Minecraft-server.zip' }
            media = MediaFileUpload('/kaggle/working/Minecraft-server.zip', mimetype='application/zip')
            file = drive_service.files().update(
                fileId=fileId, body=file_metadata, media_body=media, fields='id'
            ).execute()

        def main():
            %cd /kaggle/working/Minecraft-server

            server_process = subprocess.Popen(
                ['java', '-Xms27G', '-Xmx27G', '-jar', 'server.jar', 'nogui'],
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True
            )

            stop_flag = True
            def read_logs(process):
                while stop_flag:
                    log = process.stdout.readline()
                    if log != "":
                        print(log)
                    time.sleep(0.01)
            def read_stderr(process):
                while stop_flag:
                    log = process.stderr.readline()
                    if log != "":
                        print(log)
                    time.sleep(0.01)
            threading.Thread(target=read_logs, args=(server_process,)).start()
            threading.Thread(target=read_stderr, args=(server_process,)).start()

            def send_msg(msg):
                server_process.stdin.write(msg)
                server_process.stdin.flush()

            wait_until(dt2(ti[1], -5))
            send_msg("say このサーバーは5分後に一時停止します\n")
            send_msg("say 稼働時間は6:00-17:27 17:30-翌5:00です\n")

            wait_until(dt2(ti[1], -1))
            send_msg("say このサーバーは1分後に一時停止します\n")
            send_msg("say 稼働時間は6:00-17:27 17:30-翌5:00です\n")

            wait_until(dt2(ti[1], sec=-10))
            send_msg("say このサーバーは10秒後に一時停止します\n")
            send_msg("say 稼働時間は6:00-17:27 17:30-翌5:00です\n")

            wait_until(ti[1])
            send_msg("stop\n")

            time.sleep(3)
            stop_flag = False
            %cd /kaggle/working
            !rm Minecraft-server.zip
            !zip -r Minecraft-server.zip Minecraft-server > /dev/null 2>&1
            print("zip")
            upload()
            print("Upload Server", datetime.datetime.now(jst))
            
        def main2():
            %cd /kaggle/working/frp_0.58.1_linux_amd64
            !./frpc -c ./frpc.toml

        import threading

        threading.Thread(target=main).start()
        time.sleep(25)
        wait_until(dt2(ti[0], sec=-2))
        threading.Thread(target=main2).start()

        wait_until(dt2(ti[1], sec=5))
        !ps aux | grep ./frpc | grep -v grep | awk '{print $2}' | xargs kill -9

        time.sleep(120)
        print("exit")


i = len(times) - 1
while i >= 0:
    if dt2(times[i][0], -25) <= now and now < dt2(times[i][1], -8):
        start(times[i])
        break
    i -= 1

Kaggleを以上のように書き換え。

これで一週間が経過しても止まらない。

追記は以上。

二週間後 追記2

Azure VM の自動更新

サーバーの稼働開始から16日後、Azureが突然再起動し、実行していたtmuxが全て停止するという事案があった。調べてみると、Linuxの自動アップデートが3週に1回程度行われているようだ。

これを止める方法を調べたが、有益な情報は無く、また、アップデートを止めるのもセキュリティ上どうなのかと思った。ので、ここは自動アップデートと止めるのは諦めて、Linuxの再起動時にtmuxが自動で実行されるようにしようと思う。

再起動時にtmuxが走るようにする

① bashの作成

#!/bin/bash

tmux new-session -d "python3 ~/kaggle_loop.py"
tmux new-session -d "cd ~/frp_0.58.1_linux_amd64 && ./frps -c ./frps.toml"

自PCにおいて、上の内容を「start_tmux.sh」で保存し、kaggle_loop.pyと同様にVMのルートディレクトリに転送する。

② 実行権限の付与

SSHでVMにログインし、以下を実行。

$ chmod +x ~/start_tmux.sh

② crontabの作成

$ crontab -e

を実行。1~4の数字を聞かれたら、1と入力してEnter。

コメントアウトされたテキストがうわーと出てきたら、一番下に

@reboot ~/start_tmux.sh

を追加。Ctrl + xで抜ける。何か出てきたら、とりあえずEnterしておけば大丈夫。

これで再起動されても、tmuxが勝手に動く。

追記2は以上。

一ヶ月後 追記3

一ヶ月後、AzureのVMが「支払いを有効化してね」と止まってしまった。どうやらこれ以降は料金が発生するようだ。

「12ヶ月無料!」

と聞いていたんだが??????
固定IPとディスクに料金がかかっているようだ。(VM自体は無料のよう)

仕方がないので、みんな大好きAWSに乗り換えることにした

AWSでのVMの立ち上げ

アカウント作成からVM立ち上げまでの参考はこちら↓

SSH接続の参考はこちら↓

拒否される場合はこちらが参考になるかも↓

VMでの設定

Azureでやったことと同じことをすれば良い
frpの立ち上げと、kaggle_loop.pyの実行である(tmuxはデフォルトで入っている模様)

ポート解放の参考はこちら↓

pemを使ったscpはこちら↓

AWSも無料期間は一年間。それではよいマイクラライフを。

追記3は以上。

二ヶ月後 追記4

kaggleのアカウントが凍結された

移行先を探すもどれもいまいち

無料のVMも大体メモリが1GBで、少し発展したマイクラサーバーはまともに動かない

そういえばGCPの3ヶ月300ドルクレジットがまだ残っていたな、と思い、あと1ヶ月しか使えないがGCPに移行することにした

大阪リージョンのE2マシン

GCPでVMを立ち上げる

設定は
・asia-northeast2(大阪)
・e2-highmem-2(2 vCPU, 16GB RAM)
・Ubuntu 20.04 LTS
・10GB 標準永続ディスク
とした

料金についてはこちらを参照されたい

課金対象になるのは主に次の二つ
・E2 Instance Core running in Osaka 0.02802642 USD per 1 hour
・E2 Instance Ram running in Osaka 0.00373911 USD per 1 gibibyte hour

2コア 16GB RAMなので、1時間あたりの金額は0.1158786 USDとなる
これなら3ヶ月使用しても300ドルは超えないだろう

最後に

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

この下に本文はありません。少しでもお役に立てましたら。

ここから先は

0字

¥ 300

この記事が気に入ったらサポートをしてみませんか?