How to use Pulumi and Scala to deploy a Minecraft Server on a DigitalOcean Droplet

How to use Pulumi and Scala to deploy a Minecraft Server on a DigitalOcean Droplet

TL;DR Le code

Introduction

Since I am a big fan of Minecraft and Pulumi I wanted to combine both and see If I am able to use Scala as programming language. As we all know, Pulumi is a multi-language framework and supports many languages. And since now a while, also Java. Scala is a JVM language and compiles to Java bytecode. So I thought, why not give it a try.

Prerequisites

To follow this tutorial you need to have the following installed:

  • The Pulumi CLI and a backend to host your state. I am using the Pulumi Service. It is free for open source projects.
  • Scala
  • DigitalOcean Account and API Token. You can create here an account and get $200 credit for 60 days.
  • IDE of your choice. I am using IntelliJ IDEA. Add the Scala plugin to your IDE, for a more pleasant experience.

What is Scala?

I shamelessly copied this from the Scala website:

Scala combines object-oriented and functional programming in one concise, high-level language. Scala's static types help avoid bugs in complex applications, and its JVM and JavaScript runtimes let you build high-performance systems with easy access to huge ecosystems of libraries.

There you go, I could not better explain this myself.

Install Scala

I am using homebrew to install Scala on my Mac. So the installation is as easy as:

brew install coursier/formulas/coursier && cs setup

Create a new Pulumi project

After the installation of Scala I create a new Scala project with the following command:

sbt new scala/scala3.g8

And use as project name pulumi-scala-minecraft. This will create a new directory with the name pulumi-scala-minecraft and some files in it. We will change this files, no worries.

To add Pulumi, I just added a Pulumi.yaml file with the following content:

name: pulumi-scala-minecraft
description: A minimal Java Pulumi program with Maven builds
runtime:
  name: java

Now we can add the Pulumi dependencies to our project. We need the Pulumi core library and the Pulumi DigitalOcean provider.

Open the build.sbt file and add the following dependencies after the exiting libraryDependencies line:

val scala3Version = "3.2.1"

lazy val root = project.in(file(".")).settings(
  name := "pulumi-scala-minecraft",
  version := "0.1.0-SNAPSHOT",

  scalaVersion := scala3Version,

  libraryDependencies += "org.scalameta" %% "munit" % "0.7.29" % Test,
  libraryDependencies += "com.pulumi" % "pulumi" % "0.6.0",
  libraryDependencies += "com.pulumi" % "digitalocean" % "4.16.0",
  libraryDependencies += "com.pulumi" % "command" % "4.5.0",
)

Some remarks: I am using Scala 3.2.1 for this tutorial.

Now I create a package io.dirien.minecraft in the src/main/scala directory and add a file App$.scala to it. This will be our main class. Add the following content to the file:

package io.dirien.minecraft


import com.pulumi.{Context, Pulumi}
import io.dirien.minecraft.MinecraftServer

object App {

  final val region = "fra1"
  final val size = "c2-4vcpu-8gb"
  final val image = "ubuntu-22-04-x64"


  def main(args: Array[String]): Unit = {
    Pulumi.run { (ctx: Context) =>

      val minecraftServer = new MinecraftServer(image, region, size)
      val ip = minecraftServer.infrastructure()

      ctx.`export`("public ip", ip)
    }
  }
}

As you can see, we created a new Object App with a main method. In this method we create a new instance of our MinecraftServer class and call the infrastructure method. This method will create the infrastructure for our Minecraft Server. The infrastructure method returns the public IP address of the Droplet. We export this IP address with the export method of the Context class. This will make the IP address available as output of our Pulumi program.

Create the MinecraftServer class

The MinecraftServer class is the heart of our Pulumi program. It will create the infrastructure for our Minecraft Server. We will use the DigitalOcean provider to create a Droplet and a Floating IP. The Droplet will use cloud-init to download, install and configure the Minecraft Server.

Create a new file MinecraftServer.scala in the src/main/scala/io/dirien/minecraft directory and add the following content:

package io.dirien.minecraft

import com.pulumi.core.Output
import com.pulumi.digitalocean.{Droplet, DropletArgs, SshKey, SshKeyArgs}
import com.pulumi.{Context, Pulumi}
import com.pulumi.command.remote.{Command, CommandArgs}
import com.pulumi.command.remote.inputs.ConnectionArgs

import scala.io.Source

class MinecraftServer(image: String, region: String, size: String) {

  def readFile(path: String): String = {
    val file = Source.fromFile(path)
    val fileContent = try file.mkString finally file.close()
    fileContent
  }

  def infrastructure(): Output[String] = {
    val cloudInit = readFile("src/main/resources/cloud-init.yaml")
    val sshPublicKeyString = readFile("src/main/resources/minecraft.pub")

    val sshKey = new SshKey("minecraft-scala", SshKeyArgs.builder
      .publicKey(sshPublicKeyString)
      .build())

    val fingerprints: Output[java.util.List[String]] = sshKey.fingerprint().apply(fingerprint => {
      Output.listBuilder().add(fingerprint).build()
    })

    val minecraftServer = new Droplet("minecraft", DropletArgs.builder
      .image(this.image)
      .region(this.region)
      .size(this.size)
      .userData(cloudInit)
      .sshKeys(
        fingerprints
      )
      .build())

    val privateKey = readFile("src/main/resources/minecraft")

    new Command("install-minecraft", CommandArgs.builder()
      .connection(ConnectionArgs.builder()
        .host(minecraftServer.ipv4Address())
        .privateKey(privateKey)
        .user("root")
        .build())
      .create("cloud-init status --wait")
      .build())

    minecraftServer.ipv4Address()
  }
}

You can see that we have a constructor with three parameters: image, region and size. These parameters are used to create the Droplet.

As you may have spotted, we have a method readFile which reads a file from any given path and takes care of closing the Source.

There is one convenience Pulumi resource called Command. This resource allows us to execute a command on a remote host. In this case, we use it to wait for the cloud-init script to finish.

Before I forget, you need to create an ssh key pair. You can do this with the following command:

ssh-keygen -f $PWD/minecraft

and copy both files minecraft and minecraft.pub to the src/main/resources directory. Of course, you can use any other name and path, but you need to adjust the calls to readFile in the MinecraftServer class.

That's it from the code side. Now we can run our Pulumi program.

Run the Pulumi program

Set the DIGITALOCEAN_TOKEN environment variable to your DigitalOcean API token.

export DIGITALOCEAN_TOKEN=your-token

Now you can run the Pulumi program with the following command:

sbt update
pulumi up -f -y

The first command will download all dependencies. The second command will run the Pulumi program.

After a few minutes, you should see the following output:

❯ pulumi up -f -y     
Please choose a stack, or create a new one: dev
Updating (dev)

View Live: https://app.pulumi.com/dirien/pulumi-scala-minecraft/dev/updates/22

     Type                           Name                        Status             
 +   pulumi:pulumi:Stack            pulumi-scala-minecraft-dev  created (44s)      
 +   ├─ digitalocean:index:SshKey   minecraft-scala             created (1s)       
 +   ├─ digitalocean:index:Droplet  minecraft                   created (41s)      
 +   └─ command:remote:Command      install-minecraft           created (122s)     


Outputs:
    public ip: "xx.yy.zz.aa"

Resources:
    + 4 created

Duration: 2m53s

You can see that we created a new stack called dev. The stack contains the resources we created. You can see the IP address of the Droplet in the output section.

Connect to the Minecraft Server

Copy the IP address from the output section and connect to the server in your Minecraft client.

image.png

image.png

Enjoy your Minecraft Server!

Clean up

To clean up the resources, run the following command:

pulumi destroy -f -y

Conclusion

It was very interesting to create a Pulumi program with Scala. I can see why so many developers love Scala. And it works really well with Pulumi. I hope you enjoyed this article. If you have any questions or comments, please let me know in the comments section.