EC2に建てたマイクラのサーバーをDiscordから起動する

# EC2に建てたマイクラのサーバーをDiscordから起動する

だいぶ前にマイクラにハマっていた時がありました。
その時に一緒に遊んでいた仲間内で自由にサーバーの起動ができるように、DiccordからEC2上に建てたマイクラのサーバーを起動するBotを作成したので、作り方を簡単に紹介します。

## 構成

- EC2(マイクラサーバー)
- Lambda(サーバー側)
- DiscordBot(クライアント側)

DiscordBotからLambdaにリクエストを飛ばし、LambdaからEC2を起動します。

## EC2

起動スクリプトです。
これをEC2起動時に自動で実行するようにします。
マイクラ内でstopコマンドを叩くことでマイクラサーバーが終了し、そのままEC2を終了させるリクエストをLambdaに投げます。
ついでに、DiscordのWebHookでサーバーが終了したときに通知するようにします。

``` shell
java -Xms$XMS_SIZE -Xmx$XMX_SIZE -jar {マイクラサーバーのJarファイル} -nogui
curl -X POST {DiscordのWebHookURL} -H 'Content-Type: application/json' --data '{"content": "サーバーを終了します"}'
curl -X POST {LambdaのリクエストURL} -H 'Content-Type: application/json' -H 'x-api-key: {LambdaのAPIキー}' --data '{"action": "stop"}'
```

## Lambda

LambdaからEC2を操作するコードです。
リクエストボディーからリクエストの種類を取得し、起動・停止します。

``` go
package main

import (
	"encoding/json"
	"log"
	"os"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/ec2"
)

type Action string

const (
	ACTION_STOP  Action = "stop"
	ACTION_START Action = "start"
)

type Request struct {
	Action Action `json:"action"`
}

type Response struct {
	Result  int    `json:"result"`
	Message string `json:"message"`
}

var instanceId string

const (
	layout = "2006-01-02"
	region = "ap-northeast-1"
)

func init() {
	instanceId = os.Getenv("TARGET_INSTANCE_ID")
}

func startInstance(ec2Client *ec2.EC2, instance string) error {
	params := &ec2.StartInstancesInput{
		InstanceIds: []*string{
			&instance,
		},
	}
	_, err := ec2Client.StartInstances(params)
	return err
}

func stopInstance(ec2Client *ec2.EC2, instance string) error {
	params := &ec2.StopInstancesInput{
		InstanceIds: []*string{
			&instance,
		},
	}
	_, err := ec2Client.StopInstances(params)
	return err
}

func proc(request Request) Response {
	session := session.Must(session.NewSession())
	svc := ec2.New(session, aws.NewConfig().WithRegion(region))

	var (
		err     error
		message string
		result  int
	)
	switch request.Action {
	case ACTION_STOP:
		err = stopInstance(svc, instanceId)
	case ACTION_START:
		err = startInstance(svc, instanceId)
	default:
		result = 1
		message = "undefined action: " + string(request.Action)
	}

	if err != nil {
		result = -1
		message = err.Error()
	}

	return Response{
		Result:  result,
		Message: message,
	}
}

func handler(requestSource events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	log.Println(requestSource.Body)

	var request Request
	json.Unmarshal([]byte(requestSource.Body), &request)

	response := proc(request)

	bytes, _ := json.Marshal(response)

	return events.APIGatewayProxyResponse{
		Body:       string(bytes),
		StatusCode: 200,
	}, nil
}

func main() {
	lambda.Start(handler)
}
```

## DiscordBot

全体を乗せると長いので一部を抜粋しています。
```*start```という特定のメッセージに反応して、LambdaにEC2を起動するようにリクエストします。

``` go
func (a *app) startServer() error {
	request := StartRequest{
		Action: "start",
	}

	reqData, err := json.Marshal(request)
	if err != nil {
		return err
	}

	req, err := http.NewRequest("POST", "{YOUR_LAMBDA_URL}", bytes.NewBuffer(reqData))
	if err != nil {
		return err
	}
	req.Header.Set("x-api-key", "{YOUR_LAMBDA_API_KEY}")
	req.Header.Set("content-type", "application/json")

	client := new(http.Client)
	res, err := client.Do(req)
	if err != nil {
		return err
	}

	resData, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return err
	}

	var response StartResponse
	err = json.Unmarshal(resData, &response)
	if err != nil {
		return err
	}

	if response.Result != 0 {
		return errors.New(response.Message)
	}

	return nil
}

func (a *app) messageHnadler(s *discordgo.Session, m *discordgo.MessageCreate) {
	switch m.Message.Content {
	case "*start":
		var message string
		err := a.startServer()
		if err != nil {
			message = "サーバーの起動に失敗しました。\n" + err.Error()
		} else {
			message = "サーバーの起動に成功しました"
		}
		a.discord.ChannelMessageSend(m.ChannelID, message)
	default:
		s.ChannelMessageDelete(m.ChannelID, m.ID)
	}
}

```

## おわり

（あまり大きな声では言えないですが）
マイクラにハマっていたときにバックアップデータをGitlab上に上げていたので、草がいっぱい生えて綺麗でした。