Skip to content

Terraform and Cloud-init to run Minecraft servers in AWS EC2 Spot instances controlled by Discord slash commands interactions.

License

Notifications You must be signed in to change notification settings

g-otn/minecraft-spot-discord

Repository files navigation

Minecraft Spot Discord

Spin up a cheap EC2 Spot instance to host a Minecraft server managed by Discord chat.

Made for a personal and small server with few players.

Table of Contents

Strategy

The idea is reduce costs by basically:

  1. Start the server only when players want to play
  2. Automatically stop the server when there are no players
  3. Avoid paying for a domain/etc by using a DDNS service
  4. Using spot instances

This is achieved by:

  1. Starting the server via Discord slash commands interactions
    • Slash commands work via webhook which don't require a Discord bot running 24/7, so we can use AWS Lambda + Lambda Function URL (GCP Cloud Run could work too)
  2. Using the Auto-stop feature from itzg/docker-minecraft-server (a plugin like vincss/mcEmptyServerStopper could work too) alongside a systemd timer
  3. Setting up Duck DNS inside the instance (No-IP could work too)

Workflow

The process works as follow:

  1. The player types /start in a Discord server text channel
  2. Discord calls our Lambda function via its Function URL
  3. The Lambda function sends the interaction token alongside the start command to another Lambda via SNS and then ACKs the interaction to avoid Discord's 3s interaction response time limit.
  4. The other Lambda which can take its time, and starts the EC2 instance. Other commands such as stop, restart and status can stop, reboot and describe the instance.
  5. The instance starts
  6. The Duck DNS systemd service updates the domain with the new IP (this can take a while to use due to DNS caching)
  7. The Minecraft systemd service runs the Docker Compose file to start the server
  8. The Minecraft shutdown systemd timer starts checking if the container is running
  9. After a minute or so, the server is ready to connect and play
  10. After 10 minutes without a connection or after the last player disconnects, the server is shutdown automatically via the Auto-stop feature.
  11. After a minute or less, the Minecraft shutdown timer/service sees that the server is shutdown and shuts down the instance itself.

Diagram

diagram

Cost breakdown

TL;DR: Between 0.4 to 1.1 USD for 30h of gameplay for 2 vCPU with 2.5GHz and 8GB RAM

  • AWS Princing Calculator estimate
    • Does not include Public IP cost, see table below
  • Last updated: June 2024
  • Region assumed is us-east-2 (Ohio)
  • Prices are in USD
  • Assumes usage of Always Free monthly offers (different from 12 month Free Tier)
    • This is important mostly due to the monthly free 100GB outbound data transfer from EC2 to the internet. (See also blog post) Otherwise due to the current price rates and regular gameplay network usage, it would cost more than the instance itself
  • For the EC2 prices (in the table below), keep in mind about:
    • Surplus vCPU usage credits charges when using burstable instances in unlimited mode (default). See Earn CPU credits and When to use unlimited mode versus fixed CPU.
      • Basically, don't play at heavy CPU usage continuously for TOO long when using those instances types.
    • Spot prices change:
      • With time
      • Depending on the selected availability zone. See Spot Instance pricing history in "AWS Console > EC2 > Instances > Spot Requests > Pricing History" to see if this variation is significant and to choose the current best availability zone for you.
    • You can always change the instance type, don't forget to change the other related Terraform variables!

Notable expenses

Service Sub-service / description Price/hour Price 30h/month
EC2 t4g.large spot instance $0.02379 $0.531
EBS 10GB volume for Minecraft data $0.00109 $0.032
EBS Daily volume snapshots, for backup - ~$0.03
VPC Public IPv4 address $0.005 $0.015
Total $0.029 $0.578

12-month Free Tier

If you have access to the 12-month Free tier, you should automatically benefit from the following offers:

Always Free offers

Some of the services used are more than covered by the "always free" monthly offers, namely:

Lambda; SNS; KMS; CloudWatch / X-Ray; Network Data Transfer from EC2 to internet.

Prerequisites

  • An AWS account and associated credentials for Terraform to create resources
  • An Discord app on the Developer portal
  • A Duck DNS account and domain
  • A SSH keypair for SSH-ing into your instance (e.g ssh-keygen -t ed25519 -C "minecraft-spot-discord")

Setup

Requirements: Terraform, Python 3.6+ (due to terraform-aws-lambda), Node 18+ (to compile Lambda functions)

Initial setup

  1. Clone and navigate to the project:
git clone https://github.com/g-otn/minecraft-spot-discord.git && cd minecraft-spot-discord
  1. Initialize Terraform:
terraform init
  1. Manually build the lambda functions (terraform-aws-lambda was breaking a lot during plan/apply)
npm i
npm run build --workspaces

Terraform variables

  1. Create a file named terraform.tfvars and fill the required variables.

Ports

Any extra port you want to open needs to be set both in extra-ingress-rules and minecraft_compose_ports variables in a way in which they match.

Note that by default the Minecraft container exposes port 25565, so if you want to run the server in another port you should either change only the host port (like 12345:25565 where 12345 is the custom port) or change the SERVER_PORT variable.

Instance type

I'd recommend nothing less than 1 vCPU and 4GB RAM. This project was mainly tested and monitored with a t4g.large instance.

Please check out:

  • The Vantage website
    • Tip: Hide Name, Windows-related, Network Performance, On-demand and Reserved columns; Show all Linux Spot-related and Clock Speed columns; Sort by Linux Spot Average cost
  • Spot Instance advisor

Recommended RAM

In the variables you can set the JVM Heap size (Xms and Xmx options) via the minecraft_compose_environment variable - INIT/MAX_MEMORY option and the Docker deploy resource memory limit before the OS kills your container via minecraft_compose_limits - memory. See example.tfvars

Firstly, around 200MB is not really available in the instance for usage.

Then I recommended reserving at least 300MB for idle OS, Docker, etc to try prevent the instance from freezing. The remaining will be your Docker memory limit for the container. You could also not set a Docker limit at all.

Finally, save around 600MiB-1.5GiB for the JVM / Off-heap memory.

Instance memory Available memory Docker limit Heap size Recommended players (Vanilla)
2GiB 1.8GiB 1.6GB 1GB 1-2
4GiB 3.8GiB 3.6GB 2.8GB 1-4
8GiB 7.8GiB 7.6GB 6.1GB 2-8

Recommended Minecraft server plugins

  • DiscordSRV - We're already using Discord, so why not? However it seems this plugin overrides the interactions, so you'll have to create another Discord app on the developer portal just for this. See Installation
  • AFK-Kicker - Or any other plugin which can kick afk players, so the server doesn't stays on if nobody is playing
  • TabTPS - Or any other plugin for easy in-game information display of server load, etc

Applying

  1. Run terraform plan and revise the resources to be created

  2. Run terraform apply after a while the instance and the game server should be running and accessible

Discord interactions

  1. Go to the Lambda console on the region you chose, find the interaction-handler Lambda and copy it's Function URL.

  2. Go to your Application on the Discord Developer portal > General Information and paste the URL into Interactions Endpoint URL and click save.

  3. Invite your app to a specific Discord server (guild) using the OAuth2 link found at Installation. Make sure applications.commands is set in the Default Install Settings.

  4. In the project, navigate to scripts and create a .env file

cd scripts
touch .env
  1. Fill the environment variables required by add-slash-commands.js:
DISCORD_APP_ID=123456789
DISCORD_APP_GUILD_ID=3123456789
DISCORD_APP_BOT_TOKEN=MTABCDE

Guild ID is the Discord server ID, app ID and bot token can be found in the Discord Developer Portal.

  1. Run the script while loading the .env file. The script should call the Discord API and register the slash command interactions which the Lambda is ready to handle to that specific Discord server:
node --env-file=.env add-slash-commands.js
  1. You should now be able to use the /start, /stop, /restart, /ip or /status commands into one of the text channels to manage the instance.
    • You may need do additional permission/role setup depending on your Discord server configuration (i.e if the app can't use the text channel)

Automatic backups

Daily snapshots of the data volume are taken via Data Lifecycle Manager. However depending on your region, you must enable regional STS endpoint. us-east-2 (Ohio) for example, requires it. Otherwise the DLM policy will error when it tries to create the snapshot.

  1. If applicable, enable the STS regional endpoint for your region on the IAM Console. See Activating and deactivating AWS STS in an AWS Region

Testing and troubleshooting

CloudWatch and X-Ray

CloudWatch log groups are created for the Lambda and VPC flow logs.

X-Ray tracing is also enabled, however you need to manually set up SNS permissions so the traces show up correctly in the Trace Map / etc.

Useful commands

Game data EBS volume is mounted at /srv/minecraft; Compose container name is minecraft-mc-1.

Inside the instance:

  • htop
  • docker stats: Visualize current RAM usage vs the limit
  • docker attach minecraft-mc-1: attach terminal to Minecraft server console
  • docker logs minecraft-mc-1 -f: Latest logs from the container
  • sudo systemctl stop minecraft_shutdown.timer: Stops the systemd timer which prevents the instance from being shut down automatically until next reboot. Don't forget to shutdown manually or start the timer again!

To-do

I may or may not do these in the future:

  • Isolate the reusable resources into a Terraform module so it's easy to spin more than one instance - similar to mamoit/minecraft-ondemand-terraform
  • Make it generic so other games are supported - similar to Lemmons/minecraft-spot
    • Create some generic solution for auto-stop, watching active connections etc.
  • Create CloudWatch dashboard via Terraform

Notes and acknowledgements

This project was made for studying purposes mainly. The following repos and articles were very helpful in the learning and development process: