⛏️

Discord botからEC2のMinecraftサーバーを起動停止するやつ

2023/06/12に公開

Minecraftって数年に1回のペースでめちゃくちゃやりたくなる時期ありませんか?

僕は今、その数年に1回の時期になってしまいました。

ただ大学生の頃のように、Minecraftの世界で生活していると勘違いするほど熱狂している暇もなく、常にサーバーを建てておくことはコスト面で非効率なので、Discord botからプレイ時のみ起動できるようにしました。

似たような記事もいくつかありましたが令和最新版として最低限動作するまでの道のりをメモしておきます。

MinecraftサーバーをEC2インスタンスで建てる

これは既に有益な記事がたくさんあるのでお好みの方法でセットアップしてください。

自分は以下の記事を参考に、AWSコンソール上からポチポチしました。

https://staff-blog.faith-sol-tech.com/第1回-aws-で-minecraft-サーバー構築【1-19-2】~バニラサーバ/

インスタンスの起動/停止時にMinecraftが自動で起動/停止するように設定しておくとよいでしょう。

また、EC2インスタンスの起動停止でパブリックIPが変わってしまうので、Elastic IPを割り当てておくと毎回IPを入力し直さなくてよくなります。(コストはかかりますが)

Discord botを作成してスラッシュコマンドを作成する

こちらもドキュメントを見ながらbotを作成してスラッシュコマンドを登録するだけなので省略します。

https://discord.com/developers/docs/interactions/application-commands

自分は以下のように設定しました。

import fetch from 'node-fetch'
require('dotenv').config()

const appID = process.env.APP_ID
const guildID = process.env.GUILD_ID
const apiEndpoint = `https://discord.com/api/v8/applications/${appID}/guilds/${guildID}/commands`
const botToken = process.env.BOT_TOKEN

const commandData = {
  name: 'minecreate',
  description: 'start/stop minecraft server.',
  options: [
    {
      name: 'action',
      description: 'start/stop or test',
      type: 3,
      required: true,
      choices: [
        {
          name: 'start',
          value: 'start',
        },
        {
          name: 'stop',
          value: 'stop',
        },
        {
          name: 'test',
          value: 'test',
        },
      ],
    },
  ],
}

async function main() {
  const response = await fetch(apiEndpoint, {
    method: 'post',
    body: JSON.stringify(commandData),
    headers: {
      Authorization: 'Bot ' + botToken,
      'Content-Type': 'application/json',
    },
  })
  const json = await response.json()

  console.log(json)
}
main()

これをlocalで実行してstart/stop/testの登録ができたことを確認。

Discord botからのPOSTを受け取るLambda関数の作成

今回はスラッシュコマンドを受け付けるLambda関数から、EC2インスタンスを起動停止する別のLambda関数を呼び出す方法にしました。

まずはLambda関数からLambda関数を呼び出すポリシーを作成。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "cloudwatch:*",
                "logs:*",
                "lambda:*"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": "iam:PassRole",
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "iam:PassedToService": "lambda.amazonaws.com"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:DescribeLogStreams",
                "logs:GetLogEvents",
                "logs:FilterLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:log-group:/aws/lambda/*"
        }
    ]
}

次にスラッシュコマンドを受け付けてEC2インスタンスを起動停止するLambda関数を呼び出す関数を作成する。

const { InteractionType, InteractionResponseType, verifyKey } = require('discord-interactions');
const { Lambda } = require('aws-sdk');

const verifyRequest = (event) => {
  const { headers, body } = event
  const signature = headers['x-signature-ed25519']
  const timestamp = headers['x-signature-timestamp']
  const publicKey = process.env.DISCORD_PUBLIC_KEY
  if (!body || !signature || !timestamp || !publicKey) {
    return false
  }
  return verifyKey(body, signature, timestamp, publicKey)
}

const region = '呼び出すLambda関数のregion';
const lambda = new Lambda({ region });

const StartEC2InstancesFanction = async() => {
  const StartEC2Instances = process.env.START_EC2_INSTANCES_LAMBDA
  
  try {
    const params = {
      FunctionName: StartEC2Instances,
      InvocationType: 'Event',
    };

    const response = await lambda.invoke(params).promise()
    return 'Starting the server now.'
  } catch (error) {
    return 'Something went wrong.'
  }
}

const StopEC2InstancesFanction = async() => {
  const StopEC2Instances = process.env.STOP_EC2_INSTANCES_LAMBDA
  
  try {
    const params = {
      FunctionName: StopEC2Instances,
      InvocationType: 'Event',
    };

    const response = await lambda.invoke(params).promise()
    
    return 'Stopping the server now.'
  } catch (error) {
    return 'Something went wrong.'
  }
}

const handleInteraction = async(interaction) => {
  
  if (interaction.type === InteractionType.APPLICATION_COMMAND) {
    const { data } = interaction
    let resContent
    
    switch (data.options[0].value) {
      case 'start':
        resContent = await StartEC2InstancesFanction()
        break;
      case 'stop':
        resContent = await StopEC2InstancesFanction()
        break;
      case 'test':
        resContent = 'Hello.'
        break;
      default:
       resContent = 'Something went wrong.'
    }
    
    return {
      type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
      data: {
        content: resContent
      },
    }

  }
}

exports.handler = async (event) => {
  if (!verifyRequest(event)) {
    return {
      statusCode: 400,
    }
  }

  const { body } = event
  const interaction = JSON.parse(body)

  return {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(await handleInteraction(interaction)),
  }
}

一旦今回は非同期でLambda関数の呼び出しができていれば応答を返すようにしています。

ランタイムはNode16、discord-interactionsだけレイヤーに設定しておいてください。

EC2インスタンスを起動停止するLambda関数の作成

こちらはEC2インスタンスを起動停止するためのポリシーを作成。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:Start*",
                "ec2:Stop*",
            ],
            "Resource": "*"
        }
    ]
}

そしてLambda関数を作成。

import boto3
region = 'EC2インスタンスのregion'
instances = process.env.INSTANCE_ID
ec2 = boto3.client('ec2', region_name=region)

def lambda_handler(event, context):
    ec2.start_instances(InstanceIds=instances)
    
    return {
        'statusCode': 200,
        'body': {
            'status': 'success'
        }
    }
import boto3
region = 'EC2インスタンスのregion'
instances = process.env.INSTANCE_ID
ec2 = boto3.client('ec2', region_name=region)

def lambda_handler(event, context):
    ec2.stop_instances(InstanceIds=instances)
    
    return {
      'statusCode': 200,
      'body': {
        'status': 'success'
      }
    }

エラーハンドリングもしていない最低限のコードです。

ランタイムはPython3.9、なんでこっちはPythonかというとChat GPTに雑に投げて書いてもらったコードがこれだったからです。べんり。

動作確認

これでDiscord botからスラッシュコマンドを実行することで、Minecraftサーバーの起動停止ができました、お疲れ様でした。

動作確認スクリーンショット

はやくMinecraftがやりたすぎて現状最低限のコードなのでいい感じに改善していきたいと思います。

とりあえずElastic IPの割り当ては400円/月くらいかかっちゃうらしいので他の方法を考えたいです、あとはサーバーの起動停止失敗時にちゃんと通知してくれたり停止し忘れ時に自動で停止してくれたりする機能ぐらいは付けておきたいですね。

駆け足になりましたがうまくいかなかったりしたらコメントしてください。

Discussion