AWS CloudFormationでMinecraftマルチサーバーを構築する

## はじめに
最近Minecraftなどのマルチサーバーをレンタルできるというサービスをしばしば目にします。
ただ少し高く、無料や安価なプランだとラグがひどいということがあるのでAWS EC2で必要最小限の要素だけつけて、安価でMinecraftサーバーを構築してみようと思います。

ローカルでサーバー構築すれば良いじゃんという意見もあるかと思いますが、めんどくさい、ポート解放できない、PCのスペックが足りない、PCつけっぱなしにしたくない、などローカルでは出来ない・やりたくない理由もあるのでパブリッククラウド上に構築するのは選択肢の1つになり得ると思います。

AWSでMinecraftサーバーを構築するという記事はいくつかありましたが、CloudFormation で行う記事がなかなかなかったため、CloudFormationで作成する方法を記事としてまとめようと思います。

:::note info
ソースコードは[こちら](https://github.com/sayyyyyy/minecraft-server-cfn)に配置しています。
基本的にはAWSが上げている[こちら](https://aws.amazon.com/jp/blogs/news/setting-up-a-minecraft-java-server-on-amazon-ec2/)のブログの内容をCloudFormationで実現しています。
:::

:::note warn
AWSアカウントの登録、作業用のIAMロール作成の手順は本記事の中では取り扱いません。
リソースを作成すると料金が発生するためご注意ください。
:::

## 1分でわかるCloudFormation
CloudFormation はIaC(Infrastructure as Code)と呼ばれる技術で、コードからAWSリソースを作成することが出来ます。

他に有名なIaCとしてTerraformがありますが、CloudFormationはAWSが提供していることもあり、AWSに特化しています。

https://aws.amazon.com/jp/cloudformation/

## CloudFormationテンプレート

### Minecraft-Server-Base.yml
```yml
AWSTemplateFormatVersion: "2010-09-09"
Description: Building a Minecraft Server Base

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      Tags:
        - Key: Name
          Value: minecraft-server-vpc

  IGW:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: minecraft-server-igw

  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref IGW

  PubSub:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      VpcId: !Ref VPC
      CidrBlock: 10.0.0.0/16
      Tags:
        - Key: Name
          Value: minecraft-server-subnet

  PubSubRT:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: minecraft-server-rt

  PubSubToInternet:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PubSubRT
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref IGW

  AssociatePubSubToRT:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PubSub
      RouteTableId: !Ref PubSubRT

  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: minecraft-server-sg
      GroupDescription: Allow Minecraft Port
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 25565
          ToPort: 25565
          CidrIp: "0.0.0.0/0"
      Tags:
        - Key: Name
          Value: minecraft-server-sg

  MinecraftServerAccessRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
      Tags:
      - Key: Name
        Value: minecraft-server-iam-role

Outputs:
  PubSub:
    Description: Public Subnet
    Value: !Ref PubSub
    Export:
      Name: "minecraft-server-public-subnet"
  SecurityGroup:
    Description: EC2 Security Group
    Value: !Ref SecurityGroup
    Export:
      Name: "minecraft-server-ec2-security-group"
  MinecraftServerAccessRole:
    Description: Session Mangager Access Role
    Value: !Ref MinecraftServerAccessRole
    Export:
      Name: "minecraft-server-iam-role"
```

### Minecraft-Server-EC2.yml

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: Building a Minecraft Server

Parameters:
  MinecraftServerUrl:
    Type: String
    Default: https://piston-data.mojang.com/v1/objects/84194a2f286ef7c14ed7ce0090dba59902951553/server.jar
    Description: Minecraft Server Download URL
  JavaVersion:
    Type: String
    Default: 17
    AllowedValues:
      - 11
      - 16
      - 17
      - 21
    Description: |
      Java version
      ~1.16: Java11
      1.17~: Java16
      1.18~: Java17
      1.20.5~: Java21
  MemorySize:
    Type: String
    Default: 1300M
    Description: Server Memory Size

Resources:
  InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: "/"
      Roles:
      - !ImportValue "minecraft-server-iam-role"
  EC2: 
    Type: AWS::EC2::Instance
    Properties: 
      ImageId: ami-0ad215c298e692194
      InstanceType: t4g.small
      IamInstanceProfile: !Ref InstanceProfile
      NetworkInterfaces: 
        - AssociatePublicIpAddress: "true"
          DeviceIndex: "0"
          SubnetId: !ImportValue "minecraft-server-public-subnet"
          GroupSet: 
            - !ImportValue "minecraft-server-ec2-security-group"
      UserData:
        "Fn::Base64": !Sub |
          #!/bin/bash

          sudo dnf install -y java-${JavaVersion}-amazon-corretto-headless
          sudo adduser minecraft
          sudo mkdir /opt/minecraft/
          sudo mkdir /opt/minecraft/server/
          cd /opt/minecraft/server

          sudo wget ${MinecraftServerUrl}

          sudo chown -R minecraft:minecraft /opt/minecraft/
          sudo java -Xmx1300M -Xms1300M -jar server.jar nogui
          sleep 40
          sed -i 's/false/true/p' eula.txt
          touch start
          printf '#!/bin/bash\njava -Xmx${MemorySize} -Xms${MemorySize} -jar server.jar nogui\n' | sudo tee -a start
          sudo chmod +x start
          sleep 1
          sudo touch stop
          printf '#!/bin/bash\nkill -9 $(ps -ef | pgrep -f "java")' | sudo tee -a stop
          sudo chmod +x stop
          sleep 1

          cd /etc/systemd/system/
          sudo touch minecraft.service
          printf '[Unit]\nDescription=Minecraft Server on start up\nWants=network-online.target\n[Service]\nUser=minecraft\nWorkingDirectory=/opt/minecraft/server\nExecStart=/opt/minecraft/server/start\nStandardInput=null\n[Install]\nWantedBy=multi-user.target' | sudo tee -a minecraft.service
          sudo systemctl daemon-reload
          sudo systemctl enable minecraft.service
          sudo systemctl start minecraft.service
      Tags:
        - Key: Name
          Value: minecraft-server-instance
```

## 作成するAWSリソース

### VPC
デフォルトのVPCを利用しても良いですが、削除している場合もあると思うので新たに作成しています。
RFC1918に従ってCidrを設定します。

https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/vpc-cidr-blocks.html

```yaml
VPC:
  Type: AWS::EC2::VPC
  Properties:
    CidrBlock: 10.0.0.0/16
    Tags:
      - Key: Name
        Value: minecraft-server-vpc
```

### サブネット
Minecraftサーバーを構築する場合、外部に公開されていないと一緒に遊ぶユーザー、それどころか自分ですらサーバーに参加できないのでパブリックサブネットを利用します。

```yaml
PubSub:
  Type: AWS::EC2::Subnet
  Properties:
    AvailabilityZone: ap-northeast-1a
    VpcId: !Ref VPC
    CidrBlock: 10.0.0.0/16
    Tags:
      - Key: Name
        Value: minecraft-server-subnet
```

### インターネットゲートウェイ
サブネットを作成しただけではインターネットに接続できないので、インターネットに接続するためにインターネットゲートウェイを作成し、サブネットにアタッチする必要があります。

```yaml
IGW:
  Type: AWS::EC2::InternetGateway
  Properties:
    Tags:
      - Key: Name
        Value: minecraft-server-igw
```

### ルートテーブル
リクエストを制御するためにルートテーブルを作成し、ルートを定義します。

```yaml
PubSubRT:
  Type: AWS::EC2::RouteTable
  Properties:
    VpcId: !Ref VPC
    Tags:
      - Key: Name
        Value: minecraft-server-rt
```

### セキュリティグループ
必要な通信以外を行わないようにセキュリティグループを作成します。
Minecraftのポート番号は25565のため、そのポートはインバウンド通信を受け入れてあげる必要があります。

```yaml
SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
  GroupName: minecraft-server-sg
  GroupDescription: Allow Minecraft Port
  VpcId: !Ref VPC
  SecurityGroupIngress:
    - IpProtocol: tcp
      FromPort: 25565
      ToPort: 25565
      CidrIp: "0.0.0.0/0"
  Tags:
    - Key: Name
      Value: minecraft-server-sg
```

### EC2インスタンス
メインとなるサーバーを動作させるEC2インスタンスです。
OSはAmazon Linux 2023を利用し、インスタンスタイプはt4g.smallを選択します。
今回は大人数で遊ぶわけではなかったため、最小限のスペックにしています。実際に動作しせてみて、重いようであればインスタンスタイプを変更すると正常に動作するようになるかもしれません。
MinecraftサーバーはJavaで動作するためJavaをダウンロードする必要があります。
UserDataにサーバー構築用コマンドを設定することで、インスタンス内部で作業せずにすぐに遊ぶことができます。

```yaml
EC2: 
  Type: AWS::EC2::Instance
  Properties: 
    ImageId: ami-0ad215c298e692194
    InstanceType: t4g.small
    IamInstanceProfile: !Ref InstanceProfile
    NetworkInterfaces: 
      - AssociatePublicIpAddress: "true"
        DeviceIndex: "0"
        SubnetId: !ImportValue "minecraft-server-public-subnet"
        GroupSet: 
          - !ImportValue "minecraft-server-ec2-security-group"
    UserData:
      "Fn::Base64": !Sub |
        #!/bin/bash

        sudo dnf install -y java-${JavaVersion}-amazon-corretto-headless
        sudo adduser minecraft
        sudo mkdir /opt/minecraft/
        sudo mkdir /opt/minecraft/server/
        cd /opt/minecraft/server

        sudo wget ${MinecraftServerUrl}

        sudo chown -R minecraft:minecraft /opt/minecraft/
        sudo java -Xmx1300M -Xms1300M -jar server.jar nogui
        sleep 40
        sed -i 's/false/true/p' eula.txt
        touch start
        printf '#!/bin/bash\njava -Xmx${MemorySize} -Xms${MemorySize} -jar server.jar nogui\n' | sudo tee -a start
        sudo chmod +x start
        sleep 1
        sudo touch stop
        printf '#!/bin/bash\nkill -9 $(ps -ef | pgrep -f "java")' | sudo tee -a stop
        sudo chmod +x stop
        sleep 1

        cd /etc/systemd/system/
        sudo touch minecraft.service
        printf '[Unit]\nDescription=Minecraft Server on start up\nWants=network-online.target\n[Service]\nUser=minecraft\nWorkingDirectory=/opt/minecraft/server\nExecStart=/opt/minecraft/server/start\nStandardInput=null\n[Install]\nWantedBy=multi-user.target' | sudo tee -a minecraft.service
        sudo systemctl daemon-reload
        sudo systemctl enable minecraft.service
        sudo systemctl start minecraft.service
    Tags:
      - Key: Name
        Value: minecraft-server-instance
```

### IAM ロール
EC2インスタンスで作業をするために今回はセッションマネージャーを利用しますが、そのためには`AmazonSSMManagedInstanceCore`ポリシーが必要なためIAM ロールにポリシーを付与し、そのIAMロールをEC2インスタンスにアタッチする必要があります。

```yaml
MinecraftServerAccessRole:
  Type: AWS::IAM::Role
  Properties:
    AssumeRolePolicyDocument:
      Version: "2012-10-17"
      Statement:
        - Effect: Allow
          Principal:
            Service:
              - ec2.amazonaws.com
          Action:
            - 'sts:AssumeRole'
    ManagedPolicyArns:
      - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
    Tags:
    - Key: Name
      Value: minecraft-server-iam-role
```

## 料金について
実際に私が利用してみたところ2人で月に8回ほど4時間遊んで2ドル弱でした。
現在は円安のため相対的に高くなってしまいますが、かなり安価に利用できます！

Volumeを利用している限り、EC2を起動していなくても料金が発生してしまいますが、Minecraftのデータは全て一つのフォルダに格納されるため、そのフォルダさえS3など、どこか別の場所に保存しておけば、EC2を終了してVolumeを削除してしまっても問題ありません。その場合遊ぶたびにCloudFormationを流す必要がありますが...

## ついでに
ずっとサーバーをつけっぱなしにしておくのは料金がかかってしまうので、停止し忘れた時も自動でサーバーを停止するようにスケジューラーを利用することをお勧めします。
AWSの場合EventBridge Schedularを利用するとスケジュール機能を実装することができます。
以下のCloudFormationテンプレートを利用すると毎日午前3時にインスタンスを停止するスケジューラを作成することができます。

### Minecraft-Server-Schedular.yml

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: Scheduler to stop the Minecraft Server

Parameters:
  InstanceId:
    Type: AWS::EC2::Instance::Id
    Description: EC2 Instance ID to be stopped
  ScheduleStopTime:
    Type: String
    Default: "cron(0 3 * * ? *)"
    Description: Time to stop the EC2 Instance
  ScheduleTimezone:
    Type: String
    Default: Asia/Tokyo
    Description: Schedule timezone

Resources:
  SchedulerToStopEC2:
    Type: AWS::Scheduler::Schedule
    Properties:
      Name: "minecraft-server-stop-scheduler"
      FlexibleTimeWindow:
        Mode: "OFF"
      ScheduleExpression: !Ref ScheduleStopTime 
      ScheduleExpressionTimezone: !Ref ScheduleTimezone
      State: ENABLED
      Target:
        Arn: arn:aws:scheduler:::aws-sdk:ec2:stopInstances
        Input: !Sub |-
          {
            "InstanceIds": ["${InstanceId}"]
          }
        RoleArn:
          Fn::GetAtt:
          - SchedulerToStopEC2Role
          - Arn
  SchedulerToStopEC2Role:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - scheduler.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      Policies:
        - PolicyName: EC2Stop
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:StopInstances
                Resource:
                  - "*"
```

## 使い方

1. [こちら](https://mcversions.net/)のサイトで遊びたいバージョンのサーバーのダウンロードリンクを取得する
    - バージョンを選択し、Download Server jarを右クリックしてリンクをコピーする
![スクリーンショット 2024-07-26 19.49.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1087368/6f2ea392-a1e9-f622-30aa-71ea112791c5.png)

2. AWSにサインインする
3. リージョンを自分の住んでいる地域の近くにする
4. CloudFormationの画面を開く
5. CloudFormationテンプレートを流してスタックを作成する
    1. スタックの作成＞新しいリソースを使用（標準）を押下
    2. テンプレートのアップロード＞ファイルの選択を押下し、以下のCloudFormationテンプレートをアップロード
        1. `Minecraft-Server-Base.yml`
            - Parameters
                - なし
        2. `Minecraft-Server-EC2.yml`
            - Parameters
                - MinecraftServerUrl: 手順1で取得したダウンロードリンク
                - JavaVersion: Minecraftのバージョンに合わせたJavaバージョン
                    - ~1.16: Java11
                    - 1.17~: Java16
                    - 1.18~: Java17
                    - 1.20.5~: Java21
                - MemorySize: 大人数でなければデフォルト
    3. スタックオプションはデフォルトで次に進む
    4. `AWS CloudFormation によって IAM リソースが作成される場合があることを承認します。`にチェックを入れて送信ボタンを押下
    - 注意点
        - IAMを変更するため確認画面でチェックをつける必要がある
        - スタックを作成する順番を逆にするとうまくいかないため注意
        - インスタンス起動のタイミングで[EULA](https://www.minecraft.net/ja-jp/terms/r3)に自動的に同意するため注意
6. EC2の画面を開き、パブリックIPv4アドレスをコピーする
![スクリーンショット 2024-07-26 20.01.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1087368/40df99f9-fa84-4492-32af-0746596f6d5c.png)

7. MinecraftでサーバーIPの欄にパブリックIPv4アドレスをつけてサーバーに接続する
    - EC2インスタンスが作成された後もインスタンス内部で設定を行っているため、5分程待ってからアクセスする必要がある
8. 遊び終わったらEC2インスタンスを停止する
    - 稼働させている限り料金が発生してしまう
    - 停止ではなく終了してしまうとデータが消えてしまうので、理由がない限り停止を推奨

## 備考
- 個人利用のため安価で遊べるように1リージョン1AZに配置している
- 既存のワールドファイルを利用する場合はS3経由でEC2インスタンス上に配置するのが楽
    - Amazon Linux2023には最初からAWS CLIが導入されているので、cli経由でS3のファイルを簡単にEC2インスタンス内に配置可能
- サーバーの操作はセッションマネージャーでEC2インスタンスに接続して行う
    - Roleを作成する必要がある
        - AmazonSSMManagedInstanceCoreポリシー
    - Amazon Linux 2023には最初からエージェントがインストールされている
        - https://docs.aws.amazon.com/systems-manager/latest/userguide/ami-preinstalled-agent.html
    - 特に料金はかからない
        - https://aws.amazon.com/jp/systems-manager/pricing/
- バックアップを取得する設定などはしていないので注意
- コストを抑える目的で特にIPを固定していないため、インスタンスを再起動するたびにIPが変更される

## おわりに
私が中学生の時にMinecraftにめちゃめちゃハマっていたのですが、そのころはエンジニアになるなんて思ってもいませんでした。
その頃を思うと感慨深いものがあります。

## 参考

https://qiita.com/tyoshitake/items/c5176c0ef4de8d7cf5d8

https://dev.classmethod.jp/articles/new-minecraft-for-aws_ec2-instance/

https://aws.amazon.com/jp/blogs/news/setting-up-a-minecraft-java-server-on-amazon-ec2/

https://dev.classmethod.jp/articles/cloudformation-template-eventbridge-scheduler-ec2-start-stop/

https://docs.mcserversoft.com/advanced/java-version