ローコストで24時間MineCraftサーバーを実現する

# きっかけ
友人とマイクラで遊ぶぞってことで、私がサーバーを用意することになった。
いつも白羽の矢が立つが正直めんどくさい。しかも各々ログイン時間が異なるため24時間サーバーを運用する要件付き。
~~たまには欲しい物リストの1つや２つ、プレゼントしてくれ~~

メインPCでサーバーを構築するとリソースを食いすぎて支障なので、使っていないベアボーンPCをサーバーにすることにしたが、故障していて自宅サーバーは断念。
結局お金を出し合ってVPS借りる話になり、ちょうど私がAWS-SAAの勉強中だったのでAWSで構築することにした。


# 今回の要件
1. 24時間ゲームに参加できる
1. 参加人数は増える可能性あるためスケーラブルに
1. できる限りローコスト


## アーキテクチャ
コスト最適化のために必要なときにインスタンスを起動し、ゲームのアクティブが0のときEC2インスタンスを停止する。

起動、停止の度に私がコンソールで操作するのは面倒なので、LambdaのURL関数で参加者自身にインスタンスの起動させる。

Elastic IPアドレスを割り当てるとインスタンス停止中に課金されるので、起動の度に新しいIPアドレスをDiscordに通知する。

停止はインスタンスの中でcronを回し参加人数を監視する。0人でシャットダウンシェルを作動させる。

必要に応じて手動でインスタンスタイプをスケールアップする。

<details><summary>追加機能案メモ（折りたたみ）</summary>

シャットダウンシェルの中でS3にワールドデータをバックアップする。
停止時にもDiscordに通知する。EventBridge→Lambda経由
スポットインスタンスを活用する
</details>



![AWS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3572096/d96df431-9d00-fdb0-db6e-3b7b88ea2dba.png)



### コストについて
今回、構築段階では無料枠だけで済むように構成したので規模が小さければ0円サーバーが完成する。
サーバースペックが足りないようなら、スケールアップでその都度課金するイメージ。


## 環境
|  |  |
|:-:|:-:|
| EC2 OS | Amazon Linux 2023 |
|EC2 タイプ| t4g.small (Arm系) |
| Lambda | Python 3.11 |
| Tera Term | 5.0 beta1 |
| Discord | Stable 235476 (a7cb1d5) |
| MineCraft | JAVA版 1.20.2 |


# 作業手順
LinuxとAWSについてある程度の知識があることを想定しています。
以下のことは完全には記述しません。
- Linuxの操作コマンド
- AWSサービスの機能説明、料金等

各種名付けは自分で分かるものを適当につけてください。
## EC2インスタンスの作成
<dl>
  <dt>AMI</dt>
  <dd>Amazon Linux 2023 : Arm</dd>
  <dt>インスタンスタイプ</dt>
  <dd>t4g.small</dd>
  <dt>キーペア</dt>
  <dd>RSAのpem形式　SSH接続で使用</dd>
  <dt>VPC</dt>
  <dd>デフォルト<br></dd>
  <dt>セキュリティグループ</dt>
  <dd> SSH  と TCP: 25565 すべて許可</dd>
  <dt>ストレージ</dt>
  <dd>8GB gp3　足りなければ後から足せる</dd>
  <dt></dt>
  <dd></dd>

</dl>
<br>
このあとLambdaで起動テストをするため、起動後停止させる。

## LambdaにアタッチするIAMロールを作成
IAMロールがないとLambdaからEC2を操作できないため作成する。

### ポリシー作成
```JSON:JSON
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:StartInstances",
                "ec2:DescribeInstances",
                "ec2:StopInstances"
            ],
            "Resource": "*"
        }
    ]
}
```

### ロール作成
ユースケースはLambda

カスタマー管理で絞り込んで前に作ったポリシーをアタッチ

## Lambda関数の作成

<dl>
  <dt>ランタイム</dt>
  <dd>Python3.11</dd>
  <dt>アーキテクチャ</dt>
  <dd>x86_64</dd>
  <dt>実行ロール</dt>
  <dd>前に作ったもの</dd>
  <dt>詳細設定</dt>
  <dd>関数URLを有効化<br></dd>
  <dt>認証タイプ</dt>
  <dd>NONE</dd>
</dl>


:::note warn
認証タイプをNONEにすると誰でもLambdaを実行させることができるため、セキュリティが低下します。
AWS_IAMを設定する場合、AWS-SDK使って署名を付与してリクエストする必要があります。
今回は手軽にブラウザからリクエストすることを想定しているのでNONEに設定しています。
:::

### コード
```Python:Python
import boto3
import time
import requests

def lambda_handler(event, context):
    # EC2インスタンスID
    instance_id = 'i-#########'

    # EC2インスタンスを起動するためのパラメータ
    ec2 = boto3.client('ec2')
    response = ec2.start_instances(InstanceIds=[instance_id])

    # EC2インスタンスが起動するのを待つ
    instance_running = False
    while not instance_running:
        instance = ec2.describe_instances(InstanceIds=[instance_id])
        state = instance['Reservations'][0]['Instances'][0]['State']['Name']
        if state == 'running':
            instance_running = True
        else:
            time.sleep(5)

    # EC2インスタンスのパブリックIPアドレスを取得する
    instance = ec2.describe_instances(InstanceIds=[instance_id])
    public_ip = instance['Reservations'][0]['Instances'][0]['PublicIpAddress']
    
    #DiscordにWebhookを送信
    webhook_url = 'https://discord.com/api/webhooks/################'
    data = {
        'content': f'インスタンス【{instance_id}】は起動しました\r{public_ip}'
    }
    requests.post(webhook_url, json=data)
    
    return public_ip
```
`instance_id`と`webhook_url`を書き換えて、Deployを忘れずにしてください。

### レイヤー追加
このままコードを実行しても`requests`モジュールが使えないのでレイヤーを追加する。

参考

https://aduce.jp/aws/volunteers-lambda-python-layers

```:拝借したレイヤー
arn:aws:lambda:ap-northeast-1:770693421928:layer:Klayers-p311-requests:2
```
### タイムアウト
インスタンスの起動待機中にタイムアウトになると困るのでタイムアウトを長めにする
一般設定
タイムアウト　3分

### 動作確認
ブラウザから関数URLにアクセス
1分くらい待っていると起動したインスタンスのIPアドレスがレスポンスされる。
DiscordにもWebhookが送信される。

## インスタンスの中を設定
インスタンスを立ち上げてSSHでMinecraftサーバー構築と停止スクリプトを設定する。

:::note warn
Amazon Linux 2023にTera Termで接続する際は`バージョン5.0 beta1`でないと接続できない。
:::
:::note warn
追記
10/15にTeraTermのメジャーバージョン5.0が18年ぶりにリリースされたのでそっちを使いましょう。
他にもWin10から標準でcmdにSSHができたことを知りました。
:::

ユーザー名：ec2-user
パスフレーズ：なし
RSAキーを使う：キーペア作成でダウンロードした`.pem`

### Minecraftサーバー設定
バージョンは`1.20.2`
別のバージョンがよければ、下サイトから`wget`のダウンロードリンクを置換

https://mcversions.net/

```:console
#Java17をインストール
sudo yum install java-17-amazon-corretto-headless

#サーバー設置のディレクトリを作成
mkdir minecraft
cd minecraft

#サーバーjar(1.20.2)をダウンロード
wget https://piston-data.mojang.com/v1/objects/5b868151bd02b41319f54c8d4061b8cae84e665c/server.jar

#jarを実行
java -Xmx1G -Xms1G -jar server.jar nogui

#eulaを編集
vim eula.txt

#もう一度実行
java -Xmx1G -Xms1G -jar server.jar nogui

#下のログ表示で起動完了
[Server thread/INFO]: Done (70.809s)! For help, type "help"

```

実際にクライアントで接続できるか試す。
良ければ`stop`で止める。

:::note info
jarを実行する際のオプションの数値を変更することによって、
メモリ割当量（ヒープサイズ）を調整できる。
:::

### インスタンス起動時にマイクラサーバを自動起動
以下を参考にほぼ同じ

https://ayatec.hatenablog.com/entry/2021/01/21/031122

```:
$ sudo vim /etc/systemd/system/minecraft.service

[Unit]
Description=Minecraft Server
After=network-online.target

[Service]
WorkingDirectory=/home/ec2-user/minecraft
User=ec2-user

ExecStart=/bin/bash -c '/bin/screen -DmS minecraft /bin/java -server -Xms1G -Xmx1G -jar ./server.jar nogui'

ExecReload=/bin/screen -p 0 -S minecraft -X eval 'stuff "reload"\\015'

ExecStop=/bin/screen -p 0 -S minecraft -X eval 'stuff "say Server Shutdown. Saving map..."\\015'
ExecStop=/bin/screen -p 0 -S minecraft -X eval 'stuff "save-all"\\015'
ExecStop=/bin/screen -p 0 -S minecraft -X eval 'stuff "stop"\\015'
ExecStop=/bin/sleep 10

Restart=on-failure
RestartSec=60s

[Install]
WantedBy=network-online.target


$ sudo systemctl daemon-reload
$ sudo systemctl enable minecraft
```
これでインスタンスの起動でマイクラサーバーが自動起動して、
インスタンスが停止するとワールドを保存して終了までできる。

### ログインを監視して自動停止

`/home/ec2-user/minecraft/logs/latest.log`を監視してアクティブが0になると自動停止するcronを作成。

どんなコード書こうか考えてたら同じことしてた人が居たため参考にした。
ただコピペしても面白くないので解らないRubyをなんとなく解釈してPythonに書き換えた。

参考

https://zenn.dev/shatos/articles/50c43d875a64eb#%E8%87%AA%E5%8B%95%E5%81%9C%E6%AD%A2


```python:ActiveMonitoring.py
import datetime
import os

file_path='/home/ec2-user/minecraft/logs/latest.log'
f=open(file_path)
in_cnt=0
out_cnt=0
starting=True

#起動後15分はシャットダウンしない
for i, l in enumerate(f.readlines()):
    line = l.strip()
    if i==0:
        hms=line.split()[0][1:-1].split(':')
        before=int(hms[0])*3600+int(hms[1])*60+int(hms[2])
        now=datetime.datetime.now()
        after=now.hour*3600+now.minute*60+now.second
        diff = after - before
        diff+=24*3600 if diff<0
        starting=False if diff >= 15*60
    if 'joined the game' in line:
        in_cnt+=1
    if 'left the game' in line:
        out_cnt+=1

if starting:
    print('server is starting')
elif in_cnt > out_cnt:
    print('someone is login')
else:
    print('shutdown now')
    os.system('sudo shutdown -h now')

```

### cron登録
10分間隔で実行
```
sudo yum install cronie -y
sudo systemctl enable crond.service
sudo systemctl start crond.service
sudo crontab -e
*/10 * * * * /usr/bin/python3 /home/ec2-user/ActiveMonitoring.py
sudo systemctl restart cron
```


# 感想
AWSを使って構築はいろいろな組み合わせを考えたりできて、面白かった。
もっといろいろなサービスを使ってみたかったが、コスト優先で使うサービスはEC2とLambdaだけを使用した。

ググるとEC2でマイクラサーバーを立てる人は意外とたくさんいたし、自動化をする人もいて考えることは同じなんだなーと思った。
自分自身、特にコーディング技術力がなくて自動停止の実装が遅れたがRubyをPythonに書き換えるいい経験ができた。

最後に
使い方によるが、AWSは＄で請求が来るので超円安の現在(149円)は国産VPSで完全24時間サーバーのほうが簡単で安かったかもしれない。
来年からIPv4も有料になるし。IPv6勉強しよ。