Minecraft Server: How to control using AWS SDK

Today, I am continuing my quest to set up a Minecraft Server using AWS. Previously, we created the server and installed Minecraft. As I noted, AWS is a great option for creating server resources on demand. However, I don’t want to pay for AWS when my children are not using the server. Therefore, I wanted an easy way to get the Minecraft server status and to start or shut down the server without logging into AWS. This post shows how to set up a status checking class in Python. In a future post, we plan to expand on this by creating an app that my children can use themselves.

Setting Up Python

Feel free to skip this section if you already have some background in Python.

Your first question might be “What is Python?” Python is a high level programming language that was originally released in 1991. As documented in the Zen of Python, Python focuses on readability and structure. Python is probably my favorite programming language. It is incredibly easy to get started in Python and very quick to prototype new ideas. Supporting the quick prototyping, Python has a robust set of libraries that can be easily installed and used. For example, we will use three libraries in today’s example that greatly simplify our code.

If you are new to Python, installing and setting up Python is fairly easy. If you want some quick tutorials on getting started with Python, I recommend W3 Schools. W3 Schools is useful not only for getting started, but also for reference on specific syntax.

One of the features of Python that makes it great for prototyping is the REPL. REPL stands for Read-Eval-Print-Loop. The REPL lets you test without creating a python module. Even when I am writing code in an editor, I frequently use the REPL to test my code as I write.

To start the REPL, at the command line, you just type:

python3

You will be presented with a prompt that looks like this.

Python 3.9.13 (main, Aug 25 2022, 18:29:29) 
[Clang 12.0.0 ] :: Anaconda, Inc. on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

 You can simply enter Python statements into this prompt. For example, for the obligatory “Hello World” you can type:

print("Hello World!")

Press enter, and the code executes.

Install the AWS Python SDK (Boto3)

Amazon provides a Software Development Kit (SDK) for Python that allows you to easily access AWS resources like EC2.

Installing the Python module, called boto3, is easy. From the command line, type:

pip3 install boto3

Assuming everything installed correctly, we now have the module ready to go.

For this code, we are also going to use a module called Columnar, which turns a list into a formatted table, and mcstatus, which gets the status of a Minecraft server.

pip3 install mcstatus
pip3 install columnar

Create a User

Now that we have the AWS SDK, we need to set up the credentials so we can access AWS using the SDK.

First, we need to create a user. Log into the AWS console. Type IAM into the search bar at the top. This will take us to the Identity and Access Management (IAM) dashboard. Click on Access management->Users. This lists the current users who have access to your AWS resources. Click on “Add users”.

Choose a user name and click the checkbox for “Access key – Programmatic access.” Click “Next: Permissions” at the bottom right.

The next screen allows us to give permissions to the user. In my case, I did not have an existing group that gave the permissions I needed, so I set up a new group and gave it the AmazonEC2FullAccess policy. In the near future, I will edit this policy, because I don’t want the user to have the ability to create new instances or to terminate existing instances. I just want it to be able to get information about the instances and start/stop the existing instances.

The next page allows you to add key value tags for the users (e.g., email address). I did not add any tags. Click “Next: Review”. The final page allows you to review your choices and create the user.

Once the user is created, you should see a success page. This page will give you two strings that you need. First, it provides an access key ID and second, a secret access key. Keep both of these available for the next step.

Create a Credential File

Now that we have a user, we need ot create a credential file with the access key ID and secret access key.

The AWS SDK will look for a file named credentials in a specific place. Specifically, it will look in the home directory in a hidden folder called .aws. Of course, you can change the location if you prefer to place somewhere else.

Start by creating the directory.

mkdir ~/.aws

Next create a file named “credentials”.

cd ~/.aws
touch credentials
nano credentials

Add the following text:

[default]
aws_access_key_id = REPLACE WITH ACCESS KEY ID
aws_secret_access_key = REPLACE WITH SECRET ACCESS KEY
region = REPLACE WITH REGION, e.g., us-east-1

The Full Code

Now we are going to move onto the Python code. Here is the full code. We create a class called EC2Status. We will step through and explain each method in the following sections.

import boto3
import columnar
from mcstatus import JavaServer

class EC2Status:
    def __init__(self):
        self.GetStatus()

    # Get a list of current instances and their status. 
    # Store the list in a class variable named data.
    def GetStatus(self):
        self.instances = boto3.resource('ec2').instances.all()

        # Initialize data to an empty list
        self.data = []

        count = 0
        # Iterate through instances and store the data in a list that is appended to data
        for instance in self.instances:
            self.data.append([count,instance.id,instance.tags[0]['Value'],instance.state["Name"],instance.public_ip_address])
            count += 1

    # Override the string function to print data in a nice format.
    def __str__(self):
        headers = ['InstanceNum','ID','Name','State','IPv4']

        # Columnar formats a list into a table
        table = columnar.columnar(self.data, headers)
        return table

    # Start a specified instance. 
    # instanceNum is 0...n-1 where n is the number of AWS instances.
    def Start(self,instanceNum):
        instanceID = self.data[instanceNum][1]

        response = self.ec2.Instance(instanceID).start()

        return response

    # Stop a specified instance.
    def Stop(self,instanceNum):
        instanceID = self.data[instanceNum][1]

        response = self.ec2.Instance(instanceID).stop()

        return response

    # Get the IP Address of a specified instance.
    def GetIP(self,instanceNum):
        return self.data[instanceNum][4]

    # Get the current status of a Minecraft server.
    def MCStatus(self,ip_addr):
        server = JavaServer(ip_addr)
        status = server.status()
        print(f"The server has {status.players.online} player(s) online and replied in {status.latency} ms.")

        return status

Status of EC2 Instances

First, we will get the status of all EC2 instances.

def GetStatus(self):
    self.instances = boto3.resource('ec2').instances.all()

    # Initialize data to an empty list
    self.data = []

    count = 0
    # Iterate through instances and store the data in a list that is appended to data
    for instance in self.instances:
        self.data.append([count,instance.id,instance.tags[0]['Value'],instance.state["Name"],instance.public_ip_address])
        count += 1

We create a variable, called instances, that will hold the data we receive from AWS:

self.instances = boto3.resource(‘ec2’).instances.all()

Next, we create an empty list to hold the data that we want to extract from instances. We then iterate through instances to retrieve the information.

count = 0
# Iterate through instances and store the data in a list that is appended to data
for instance in self.instances:
    self.data.append([count,instance.id,instance.tags[0]['Value'],instance.state["Name"],instance.public_ip_address])
    count += 1

This function creates a more manageable ID (using a temporary variable count) and collects the instance name, state, and IP address.

Note that I implemented self.data as a list of lists. However, to make the code more readable, I could have implemented as a list of dictionaries, which would have allowed me to access key/value pairs (e.g., self.data[0][‘ip_addr’]). As currently written, if I add another column in the middle of the instance list, it would break later code.

Display the Instances

The next function overrides the string function of the class. This allows us to simply call print() on our class and it will display a formatted table with the instances. The method uses the module Columnar. Columnar takes a data list and a header list and creates a formatted table. There are many formatting options for Columnar. However, we will use the default table. Columnar returns a string that we return from our method.

def __str__(self):
    headers = ['InstanceNum','ID','Name','State','IPv4']

    # Columnar formats a list into a table
    table = columnar.columnar(self.data, headers)
    return table

Starting and Stopping Instances

The next two methods start and stop a specified instance. Note that in these basic functions, we do not perform any error checking. Within the method, we first get the full instance ID from our short ID. Then we call Instance.start() or Instance.stop(). We return the response, in case the user wants to debug a failure or needs confirmation that the instance started.

def Start(self,instanceNum):
    instanceID = self.data[instanceNum][1]
    response = self.ec2.Instance(instanceID).start()
    return response

def Stop(self,instanceNum):
    instanceID = self.data[instanceNum][1]
    response = self.ec2.Instance(instanceID).stop()
    return response

Get the IP Address

Once we have the ability to start and stop instances, next we want to get the IP address of an instance. Obviously, we can see the IP address when we print the table. However, there may be times when we want to use the IP address (for example when testing a Minecraft server).

This method takes a short ID, looks up the IP address in self.data and returns the IP address.

def GetIP(self,instanceNum):
    return self.data[instanceNum][4]

Minecraft Status

The last step for today is to implement a method that gets the status of a Minecraft server at a given IP address. This method uses the module mcstatus.

def MCStatus(self,ip_addr):
    server = JavaServer(ip_addr)
    status = server.status()
    print(f"The server has {status.players.online} player(s) online and replied in {status.latency} ms.")

We create a variable called server and initialize with an mcstatus JavaServer. Next we create a variable status and retrieve the status from the server. We print the current status and return the status to the user in case they want to perform further operations. Again, note that we do not perform any error checking.

Conclusion

In this post, we wrote a small class that allows us to use Python to get the status of AWS instances and to control those instances. In addition, we are able to verify whether our Minecraft server is working properly.

For additional information on working with EC2 instances using boto3, I found the following link to be useful.

There is so much more that we would like to do with this. For example, stopping the server should send a message to players that the server is about to stop. It would also be great if we could get an alert that the server has been running for a specified period of time. I will leave these ideas for a future day.

One Comment

Comments are closed.