Claus

7 minute read

Do you remember the good old days when SSH, manual tuning, and shell scripts were considered a solid base for running a Linux server? Often I couldn’t remember how I configured something just a day later…

These days we have it better: declaration files have become more descriptive code and ideally they produce a consistent state in hardware and software. One such example is the magnificient Terraform. Together with a containers as a deployment format, deployment and maintenance should be a breeze. Right? Right!

Factorio - Gamifying Architecture and Process design

Factorio is a game to build … well, factories. Together with their entire supply chain and logistics, this game requires foresight, planning, process architecture, and thinking things through, somewhat like gamified software design architecture. These things can be incredible, like an RGB-switched display playing a video in-game:

Anyway - the game is best played together, which is why people host their own servers for a circle of friends. However, setting up and maintaining a Linux server somewhere is not exactly how you’d imagine spending game night. It’s supposed to “just work” - how can we go about that?

Running A Container On Azure

Microsoft’s Azure provides a PaaS-type service for running containers (several actually) that can be turned on and off with an API. Most importantly it’s built for dynamic workloads - i.e. start a container, work on stuff, shut it down - a perfect fit for a weekend warrior’s gaming needs.

What you need

The primary requirements are:

We are going to use Azure Container Instances to create and run the server based on the Docker image factoriotools/factorio. Additionally, let’s attach a small management API included to start/stop and check on the server.

Terraforming in the clouds

Microsoft and Hashicorp provide a well-maintained and easy-to-use provider for applying Terraform templates directly to the cloud. This allows you to use my template without any changes to create the following resources:

Included in this template are: - ACI instance - Azure Storage Account and file share for game state/settings/… - A resource group - An Azure function to start/stop and check on the container - A contributor role assignment for the function to the container instances. i.e. the function will have contributor permissions on your ACI

On resource requirements: experience over the last couple of months shows that 2 cores and 4 GB of memory is too much for small groups of players. You can get away with fewer cores and safe some money; memory depends on how much you actually build 😅.

Now it’s time to look at the actual template - it’s short and sweet so let’s go through it. The first part is typically variables that are passed in, so we don’t have to hardcode sensitive information like subscription IDs. For more information on variables, check out the Terraform docs. Note that most variables here have a sensible default value, only the bottom three don’t to keep sensitive information off GitHub. Use environment variables or local the docs to set them or provide the information as soon as the CLI asks. Let’s walk through the template:

variable REGION {
  default = "westeurope"
}

variable n_cores {
  default = 2
}

variable mem_gb {
  default = 4
}

# Blogpost incoming to explain what's up
variable package_deploy_url {
  default = "https://github.com/celaus/fn-manage-aci/releases/download/tag-7287778cb03583625477550ab8eb6fcabe5a7120/aci-manage.zip"
}

variable RESOURCE_GROUP_NAME {
  default = "factorio"
}

variable factorio_server_version {
  default = "0.18.21"
}

variable dns_label {} # DNS name for the service: XXXX.westeurope.azurecontainer.io
variable tenantid {}  # see the authorization section
variable subid {}     # Subscription ID where the resources should be deployed

Next we are going to initialize the provider and use the auth variables to authenticate Terraform for your Azure account:

provider "azurerm" {
  version = "~>2.2.0"
  features {}

  subscription_id = var.subid
  tenant_id       = var.tenantid
}

The next bits create a bunch of resources, starting with the resource group - a logical group to keep track of everything in it. Afterwards we are going to add a storage account to it, as well as an SMB share inside the storage account. This is the network location will be attached to the container instance for storing mods, save games, etc:

resource "azurerm_resource_group" "main" {
  name     = var.RESOURCE_GROUP_NAME
  location = var.REGION
}

resource "azurerm_storage_account" "data" {
  name                     = "factoriodata"
  resource_group_name      = azurerm_resource_group.main.name
  location                 = azurerm_resource_group.main.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "azurerm_storage_share" "gamedata" {
  name                 = "gamedata"
  storage_account_name = azurerm_storage_account.data.name
  quota                = 50
}

With these resources available we can create a container instance (also called container group) and attach the network share we defined above:

resource "azurerm_container_group" "gameserv" {
  name                = "factorio-gameserver"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name
  ip_address_type     = "public"
  dns_name_label      = var.dns_label
  os_type             = "Linux"

  container {
    name   = "factoriogame"
    image  = "factoriotools/factorio:${var.factorio_server_version}"
    cpu    = var.n_cores
    memory = var.mem_gb

    ports {
      port     = 34197
      protocol = "UDP"
    }
    ports {
      port     = 27015
      protocol = "TCP"
    }

    volume {
      name                 = "gamedatamount"
      mount_path           = "/factorio"
      storage_account_name = azurerm_storage_account.data.name
      storage_account_key  = azurerm_storage_account.data.primary_access_key
      share_name           = azurerm_storage_share.gamedata.name
    }
  }
}

… and that’s it! Nothing more is required for a basic containerized server that forwards the appropriate ports and has a defined version of the game server. For customizing configs, maintaining mods, or selecting save games, check out Storage Explorer.

A word on authentication

Terraform needs to authenticate with Azure - and one easy way uses just the Azure subscription associated with the Azure CLI. By exporting an environment variable TF_VAR_tenantid and TF_VAR_subid you can pass these values into the Terraform provider. Check out this guide to find out more.

Cost control

The default configuration is intended to make the game run without hickups and if you tailor the specs to your needs you can massively reduce that bill:

  • The hardware specs are too generous, you should be able to use 1 core and 2 GB of memory until you run into issues, a quick check on the pricing calculator says € 27 a month (247)
  • The full amount is only due if you run 247, in which case you should consider running on a different service. If you start the container only on weekends (8 days a month), you’d end up with about a third of the price (~€ 9)

TL; DR: Run Your Own Game Server

First, clone the Github repository into a directory. Use your favorite terminal, cd into the brand-new clone (the directory containing main.tf) and run these commands:

$ terraform init
Initializing the backend...

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "azurerm" (hashicorp/azurerm) 2.2.0...

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
$ terraform apply
 terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # azurerm_container_group.gameserv will be created
  + resource "azurerm_container_group" "gameserv" {
      + dns_name_label      = "safespacefactorio"
      + fqdn                = (known after apply)
      + id                  = (known after apply)
      + ip_address          = (known after apply)
      + ip_address_type     = "public"
      + location            = "westeurope"
      + name                = "factorio-gameserver"
      + os_type             = "Linux"
      + resource_group_name = "factorio"
      + restart_policy      = "Always"

      + container {
          + commands = (known after apply)
          + cpu      = 2
          + image    = "factoriotools/factorio"
          + memory   = 4
          + name     = "factoriogame"
[...]

Plan: 4 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

azurerm_resource_group.main: Creating...
[...]
azurerm_container_group.gameserv: Creation complete after 1m8s

… and you are good to go. Check on the deployment through the portal. There you can also find the access keys for the Function app that lets you (or someone else) manage the container instance without logging in. While I will do another write up of the API, here is a brief intro: this is an API that you can call from a browser to https://<dns_label>-ctrl.azurewebsites.net/api/status?code=<host key from the Azure function App>. Instead of /api/status you can also call /api/start or /api/stop for the respective actions.

Check out the GitHub repo to see the entire code, submit PRs and change and adapt.

Obviously if you don’t want to run this yourself, send this article to your computer person

Thank you.