Search
  • Ryan Renn

Create a FREE Minecraft Server on Oracle Cloud (with Terraform)


Overview

A couple weeks ago, my older son came to me and was asking if he could use some of his allowance to pay a game server hosting company to host a Minecraft server for him and his friends to play on. Despite playing video games for many years, I've never actually paid anyone to host a game server for me. I was doing some research on these companies, the pricing, etc but I was curious if there was a way to accomplish this cheaper (or at no cost) without having to run a physical server at our home.


I stumbled on this fantastic blog post from Oracle that covers this exact need. I won't reiterate everything in the blog post, but to summarize, it provides you step by step instructions on how to use an Oracle Cloud "Always Free" account to host one (or more) powerful Minecraft servers for free and with no expiration period. By powerful, I mean that we are talking about a sufficient resource level to host many people on each server. The Oracle Cloud free account provides a pool of resources for some specific services and machine types. This means you aren't stuck to micro VMs like on AWS's free tier.


Additionally, if you are paranoid about a big and unexpected Cloud bill, Oracle Cloud's "Always Free" account will not let you use anything that is not free without explicitly going and upgrading your account to a "Pay-as-You-Go" account.



What This Post Will Cover

I don't plan to rehash the Oracle blog post since it already does a great job on covering how to create an Oracle Cloud "Always Free" account, the creation of the necessary resources in the UI, and the configuration of a basic Minecraft server. However, I wanted to make this a bit more interesting, so I will cover how I did this with Terraform and expand on the configuration items a bit.



Utilizing Terraform

* Note - At this point, I will assume that you followed the blog linked above and created an Oracle Cloud "Always Free" account.


For this Terraform configuration, I am using the following:

  • Terraform

  • Integrated Development Environment (VSCode)

  • Code Repository (Github.com)

  • Oracle Cloud ("Always Free" Account)

  • Development Workstation (MacBook Pro)


We will be matching the resource configuration in the Oracle blog post by using Oracle Linux 7.9 for the OS template, a E4 Flex Amphere ARM VM shape, and 4 OCPU/6GB RAM. For the boot drive, I bumped the size slightly from the default to 60 GB.


1. To get started, I created an empty private repo in Github.com, under my personal account. Then I cloned that repo down to my Mac.


2. Since I will be storing all of the configuration (not just Terraform) in this repo, I created a folder called "terraform" within it. At the root, I copied over a standard .gitignore file that I use and updated the readme.md file.


.gitignore

**/.terraform
*.tfstate
*.tfstate.*
crash.log
override.tf
override.tf.json
*_override.tf
*_override.tf.json
vault
.DS_Store
tf.out
tf.out.json
*.code-workspace
*.lock.hcl

3. Next, I created a versions.tf file to specify the minimum Terraform version and the required Terraform providers (OCI, in this case).


versions.tf

terraform {
  required_version = ">= 1.0.0"
  required_providers {
    oci = {
      source  = "hashicorp/oci"
      version = ">= 4.0.0"
    }
  }
}

4. Within an Oracle Cloud account, the primary logical grouping of resources is called a "compartment". This is similar to Azure's "resource groups". There is a root compartment already, but I want to create a new one just to group all of the Minecraft resources together. For this project, we will be creating a virtual cloud network, a subnet, route table, security list, internet gateway, and virtual machine(s). To create the compartment, I created a file called base.tf. The resource configuration for a compartment is very simple. You just need to provide a name for the compartment, a description, and the compartment id of the parent compartment. Compartments can be nested, so I will be nesting this one under the root compartment. You can find the id of the root compartment by logging into Oracle Cloud's UI, going to compartments, selecting the root compartment, and then clicking "show" next to OCID:


base.tf

resource "oci_identity_compartment" "minecraft" {
  compartment_id = "ocid1.tenancy.oc1..aaa.................nqxq"
  description    = "Compartment for minecraft server."
  name           = "minecraft"
}

5. Next, I created a provider.tf file to specify some configuration for the Oracle Cloud provider. To keep this simple and since I was going to run all of the Terraform actions locally from my workstation (no pipelines), I set the provider to use a security token authentication method with an auth config file. Auth methods are covered in the Oracle documentation here.


provider.tf

provider "oci" {
  auth                = "SecurityToken"
  config_file_profile = "DEFAULT"
}

6. For the networking, I created a file called network.tf. I will break down each resource in that file below.


network.tf

resource "oci_core_vcn" "minecraft" {
  compartment_id = oci_identity_compartment.minecraft.id
  cidr_blocks    = ["10.0.0.0/16"]
  display_name   = "Minecraft VCN"
  is_ipv6enabled = false
}

This is the virtual cloud network resource which is the same as a vnet in Azure or a vpc in AWS. For this resource, we want to reference the compartment id of the compartment we created using an attribute reference to that compartment resource, we also want to provide a display name, we can leave ipv6 disabled, and then we need to specify an IP range. Since this is just a simple setup and is segregated to just internet communication, I used a standard /16 range, but you can use whatever you want.


7. Continuing with network.tf, lets look at more of the resources in it.


resource "oci_core_subnet" "minecraft" {
  cidr_block                 = "10.0.1.0/24"
  compartment_id             = oci_identity_compartment.minecraft.id
  vcn_id                     = oci_core_vcn.minecraft.id
  display_name               = "Minecraft Subnet"
  prohibit_internet_ingress  = false
  prohibit_public_ip_on_vnic = false
  route_table_id             = oci_core_route_table.minecraft.id
  security_list_ids          = [oci_core_security_list.minecraft.id]
}

This resource is the subnet within the VCN we specified above. Once again I added an attribute reference to the compartment we created. Additionally, I added a reference to the VCN resource and to two resources farther down in the file (the route table and the security list). Besides the attribute references, you just need to specify the subnet's CIDR block (something within the range of the VCN), a display name, and you can specify options around internet ingress and public IPs. I left them as false since the servers will be internet facing.


resource "oci_core_internet_gateway" "minecraft" {
  compartment_id = oci_identity_compartment.minecraft.id
  vcn_id         = oci_core_vcn.minecraft.id
  enabled        = true
  display_name   = "Minecraft Internet Gateway"
}

This resource block specifies the internet gateway. This is needed to allow outbound internet access from the servers and to allow inbound connections for management and Minecraft connectivity. Besides the attribute references to the compartment and VCN, you just need to specify the display name and that it is enabled.


resource "oci_core_route_table" "minecraft" {
  compartment_id = oci_identity_compartment.minecraft.id
  vcn_id         = oci_core_vcn.minecraft.id
  display_name   = "Minecraft Route Table"
  route_rules {
    network_entity_id = oci_core_internet_gateway.minecraft.id
    description       = "Default Route"
    destination       = "0.0.0.0/0"
    destination_type  = "CIDR_BLOCK"
  }
}

The last resource in network.tf is the route table for the VCN. As before, you need your attribute references to the compartment and VCN. Additionally, you need to specify the display name and create a single, simple route rule. This will be the default route (0.0.0.0/0) and will send all traffic out to the internet gateway.


8. Next, we need to tackle security. Specifically, we need to allow certain traffic to the VCN and subnets so that we can manage the virtual machine(s), allow internet access from the virtual machine(s), and to play Minecraft. Since this is a larger chunk of code, I created another file called security.tf.


security.tf

resource "oci_core_security_list" "minecraft" {
  compartment_id = oci_identity_compartment.minecraft.id
  vcn_id         = oci_core_vcn.minecraft.id
  display_name   = "Minecraft Subnet Security List"
  egress_security_rules {
    destination      = "0.0.0.0/0"
    protocol         = "all"
    description      = "Allow all outbound."
    destination_type = "CIDR_BLOCK"
    stateless        = false
  }
  ingress_security_rules {
    protocol    = "6"
    source      = "your_home_public_ip/32"
    description = "Allow SSH inbound to VM for admin."
    source_type = "CIDR_BLOCK"
    stateless   = false
    tcp_options {
      max = 22
      min = 22
    }
  }
  ingress_security_rules {
    protocol    = "6"
    source      = "0.0.0.0/0"
    description = "Allow TCP Minecraft traffic inbound."
    source_type = "CIDR_BLOCK"
    stateless   = false
    tcp_options {
      max = 25565
      min = 25565
    }
  }
  ingress_security_rules {
    protocol    = "17"
    source      = "0.0.0.0/0"
    description = "Allow UDP Minecraft traffic inbound."
    source_type = "CIDR_BLOCK"
    stateless   = false
    udp_options {
      max = 25565
      min = 25565
    }
  }
}

In Oracle Cloud, a security list is the same as a network access control list (acl). In this file, we are specifying a security list resource, naming it with a display name, and creating attribute references back to the compartment and VCN.


The bulk of the code is the actual security rules. For those rules, I created four (1 x egress, 3 x ingress). The egress rule allows anything on the VCN to reach any IP address on any protocol. This rule is what allows for "internet access" so that servers can do updates, pull down packages, and download Minecraft.


Two of the ingress rules allow Minecraft game traffic from any source IP over port 25565. There is one rule for TCP and one for UDP with that port. The other ingress rule allows SSH (TCP 22) traffic for virtual machine management. For that particular rule, I set the source to my home's public IP address as it generally never changes (but your ISP may vary).


For the actual rule configuration options, there is your normal stuff like whether traffic is stateful or stateless (you want stateful), source/destination ip range, and description. Something is found confusing is the protocol configuration and _options block. The _options block is actually the port range (if applicable) and the heading of the _option block changes depending on the protocol. For the protocol setting, it actually uses a number rather than a string like "TCP", "UDP", "ICMP", etc. You can get a reference of the IP protocol numbers here.


9. The last piece that I had to tackle for configuration was the virtual machine(s). You can create one or more machines, but you will need to stay under the resource threshold for Oracle Cloud "Always Free" accounts, otherwise the subsequent machines will fail to provision. My son wanted a survival server and a creative server, so I ended up creating two machines, each with 2 OCPUs and 6 GB of RAM. For the vm configuration, I created a file called vm.tf.


vm.tf

data "oci_core_image" "oracle_linux_79" {
  image_id = "ocid1.image.oc1.iad.aaaaaaaac6jy4yovh7u6k7qguocu2wroyllwybfro6cir5mz5lsfdy7gg2cq"
}

This block is a data source that pulls information (for Terraform to reference) for the Oracle Linux 7.9 image.


resource "oci_core_instance" "minecraft_server" {
  for_each = {
    for key, value in var.minecraft_servers :
    key => value
  }
  availability_domain = each.value.availability_domain
  compartment_id      = oci_identity_compartment.minecraft.id
  shape               = "VM.Standard.A1.Flex"

  availability_config {
    is_live_migration_preferred = true
    recovery_action             = "RESTORE_INSTANCE"
  }
  create_vnic_details {
    assign_public_ip       = true
    display_name           = "${each.value.display_name}-vnic"
    skip_source_dest_check = false
    subnet_id              = oci_core_subnet.minecraft.id
  }
  display_name = each.value.display_name
  metadata = {
    ssh_authorized_keys = file("/Users/yourusername/.ssh/id_rsa.pub")
  }
  shape_config {
    memory_in_gbs = each.value.memory_in_gbs
    ocpus         = each.value.ocpus
  }
  source_details {
    source_id               = data.oci_core_image.oracle_linux_79.id
    source_type             = "image"
    boot_volume_size_in_gbs = each.value.boot_volume_size_in_gbs
  }
  preserve_boot_volume = false
}

This is the main block of code for the vm(s). Since I was creating multiple VMs, I chose to do a for_each on this resource and loop through an object map. Some values within the resource reference that object map and others are hardcoded. Overall, you need to create attribute references for the compartment, subnet, and the OS image. You will need to specify the ARM shape and some information for the VNIC, such as creating a public IP address and the display name. You will want to create a public IP.


For metadata, I decided to use SSH keys for login to the Linux VMs. I used Terraform's file function to reference my SSH public key on my local system. Terraform will pull your public key file and upload the contents to the VMs when they are created. This will allow you to SSH into the vms easily, as long as the private key is on your system.


10. As I stated in the previous step, I utilized an object map with for_each to create multiple VMs. I had to declare that object map as a variable in a variables.tf file.


variables.tf

variable "minecraft_servers" {
  description = "Object map of Minecraft servers."
  type = map(object({
    display_name            = string
    ocpus                   = number
    memory_in_gbs           = number
    boot_volume_size_in_gbs = number
    availability_domain     = string
  }))
}

In the variable, I am specifying the vm display name, the amount of OCPUs, the amount of RAM in GBs, the boot volume size in GBs, and the availability domain. Besides the name of the variable and a simple description, I added some type validation to ensure the correct key/value pairs are provided and that they are the correct type (string vs number, etc).


11. Next, I created a terraform.tfvars file to hold the variable inputs.


terraform.tfvars

minecraft_servers = {
  survival = {
    display_name            = "minecraft-server"
    ocpus                   = 2
    memory_in_gbs           = 6
    boot_volume_size_in_gbs = 100
    availability_domain     = "pxit:US-ASHBURN-AD-1"
  }
  creative = {
    display_name            = "minecraft-creative-server"
    ocpus                   = 2
    memory_in_gbs           = 6
    boot_volume_size_in_gbs = 60
    availability_domain     = "pxit:US-ASHBURN-AD-2"
  }
}

This object map of the Minecraft server VMs contains a key (survival or creative) for each server and each key contains the value inputs that map back to the vm.tf file. We've discussed the OCPUs, RAM, and boot volume size previously, but an additional configuration item is the availability domain.


You don't have to split up the availability domains, but I was running into an error message where the "host had run out of resources" during provisioning. Based on Oracle's FAQ, this can happen when a particular availability domain runs out of Free Tier resources. In my case, I placed the 2nd VM within another availability domain and the issue stopped happening.


For background, an availability domain is equivalent to availability zones in AWS. They are different physical data centers within a region and are encapsulated from a fault perspective. Additionally, Oracle Cloud has fault domains within availability domains which is a further breakdown of fault isolated resources.


12. After compiling these resource configuration files, you will need to authenticate to Oracle Cloud via the oracle-cli tool. This document from Oracle covers how to install the tool with Python.


13. After you have the Oracle OCI installed, you can source your virtual environment and then type the following command:

oci session authenticate

Then select your region and your web browser will open to the Oracle Cloud login page.



After authenticating in your web browser, you can go back to your terminal and it will ask you for a name of the profile. I used "DEFAULT". Be aware that this profile name is referenced in the Terraform code.



14. If desired, you can test your token authentication with the command displayed in the terminal. Now, you should be able to run your Terraform code. Within the terraform folder, run the following commands:

terraform init
terraform plan

If everything worked as expected, you should have a successful Terraform plan. Make sure to review the output and confirm it is going to create the resources as you expected.

Note: My output says no changes, as I am running this after the resources were created.



15. Then run the following command:

terraform apply

When prompted, enter "yes". Then it should run through the creation process. If everything works as expected, then you should see a success message and be able to see the resources in the Oracle Cloud UI.





Server Configuration

I won't be covering all of the steps required to connect to and configure the servers to run the Minecraft servers. That is covered already for a manual configuration approach in the Oracle blog and an automated approach using Ansible is covered in this article as well.


For me, I actually did both approaches. For the first server I created (survival), I followed the manual steps. To refresh myself on Ansible, I wrote some simple Ansible code a week later when I added the second server (creative).


For the Ansible approach, I created another folder in the same repo and created an inventory file along with an Ansible playbook. I also templated some of the Minecraft server configuration files so that I could version control them and add them back later if I ever need to re-run this.



I'll place the playbook code below. It's very simple Ansible code and could be greatly improved, but I only spent a small amount of time on it and was refreshing myself on Ansible after being away from it for a little over a year.


---
- hosts: "creative"
  vars: 
    packages: ['vim', 'wget', 'jdk-17.aarch64', 'screen']
    mcs_dir: /minecraft
    mcs_jar_url: https://launcher.mojang.com/v1/objects/125e5adf40c659fd3bce3e66e67a16bb49ecc1b9/server.jar
    session_name: creative

  tasks:
    - name: Update yum
      become: yes
      yum: update_cache=yes name='*' state=latest

    - name: Get packages 
      become: yes
      yum: name={{ item }} state=latest
      with_items: "{{ packages }}"

    - name: Create minecraftserver dir
      become: yes
      file: state=directory path={{ mcs_dir }}

    - name: Set minecraftserver dir permissions
      become: yes
      file:
        path: "{{ mcs_dir }}"
        state: directory
        owner: opc
        group: opc
        mode: "0770"

    - name: Wget mcserver jar
      get_url:
        url: '{{ mcs_jar_url }}'
        dest: '{{ mcs_dir }}/server.jar'

    - name: Agree to EULA
      template: src=eula.tpl dest='{{ mcs_dir }}/eula.txt'

    - name: Set server properties file
      run_once: true
      template: src=creative_server.properties.tpl dest='{{ mcs_dir }}/server.properties'

    - name: Set server whitelist file
      run_once: true
      template: src=whitelist.json.tpl dest='{{ mcs_dir }}/whitelist.json'

    - name: Set server ops file
      run_once: true
      template: src=ops.json.tpl dest='{{ mcs_dir }}/ops.json'

    - name: Check screen for running sessions
      shell: screen -ls
      register: sessions
      failed_when: sessions.rc == 0 or sessions.rc >= 2

    - name: Run mcserver
      command: screen -s {{ session_name }} -d -m java -Xmx4096M -Xms1024M -jar {{ mcs_dir }}/server.jar nogui
      args:
        chdir: '{{ mcs_dir }}'
      when: sessions.stdout.find(session_name) == -1

    - name: Update Linux FW
      become: yes
      shell:
        cmd: |
          firewall-cmd --permanent --zone=public --add-port=25565/tcp
          firewall-cmd --permanent --zone=public --add-port=25565/udp
          firewall-cmd --reload

To summarize the code, it connects via SSH to the VM, runs yum update, installs required packages, and then downloads the Minecraft server jar into a new directory. After that, it uses the file feature to overwrite the existing configuration files based on the template files from the repo. Then it starts the Minecraft server process in a detached screen session. Lastly, it then opens up the required ports on the Linux OS firewall.



Additional Notes

Don't forget to setup backups. You can do this manually, as noted in this article or I'm sure you could configure this easily in Ansible and even run it on a scheduled basis from a pipeline.



References

81 views2 comments

Recent Posts

See All