Using Ansible to deploy Factorio servers
Hello again! I’m writing this while in Australia for project work! Recently, I took it upon myself to automate a lot of virtual machine configuration via Ansible. This includes a common configuration for all servers, which sets the following:
- MOTD
- SSH configuration
- Backups
- LDAP login (if enabled, still working on getting this right)
- Firewall (if enabled, again, still working on getting this right)
LDAP configuration can probably take up a whole post on its own, which will come when it actually Works™️. The other part of this, however, is figuring out that often the best way for me to document the way servers are configured is to build an Ansible role step-by-step as I read through the documentation and figure things out. This helps to (1) test the Ansible role and make sure it’s repeatable, (2) get down all the steps to go from nothing to something, and (3) help me learn more about Ansible. The first service I tried this with was, as you may have guessed from the title, Factorio.
What is Factorio?
Factorio is an automation and factory-building game where player(s) must build a mostly or entirely-automated factory after crash-landing on an alien planet. It has the potential to get rather technical really quickly, which has a certain appeal to myself and my friends. It, importantly, supports dedicated servers that can be downloaded without authentication. I wanted to be able to configure dedicated servers by modifying my Ansible inventory, and have it be set up right away.
How do we get there?
First, let’s talk about how I structure my Ansible playbooks. At the root of the repository are two files and two folders. It looks a little something like this:
drwxr-xr-x 3 csalter csalter 4096 Feb 7 06:21 group_vars
-rw-r--r-- 1 csalter csalter 1485 Mar 11 10:09 inventory.yaml
-rw-r--r-- 1 csalter csalter 287 Mar 11 10:09 playbook.yaml
drwxr-xr-x 5 csalter csalter 4096 Mar 11 10:09 roles
group_vars/
holds legacy variables and my Ansible
Vault, and
isn’t really updated much outside of the vault. inventory.yaml
defines my
Ansible
inventory,
and associated variables. In this file is a segment defining a factorio
group,
with each host within that group receiving a YAML array of different servers.
That variable looks something like this:
|
|
Each server gets a defined name that’ll be used to determine filesystem location later on, as well as the save name and systemd service name. It also gets a version, so theoretically any Factorio version is up for use. Finally, there’s a server port definition to make sure that multiple servers on the same machine don’t conflict.
Next up is playbook.yaml
. The relevant part here isn’t really that
interesting:
|
|
It just tells ansible the name of the role, that it needs to become the
superuser to do the tasks in the role, and to run the factorio
role on the
factorio
group.
Now that that’s done, we can take a look at the way that the factorio
role is
structured. Ansible requires that roles have, at a minimum, a tasks/main.yaml
file to define the steps taken by the role. Having all the steps in one file
isn’t really ideal for readability, so in all of my roles, I separate out the
steps into different files, leaving tasks/main.yaml
looking something like
this:
|
|
This will include the tasks from tasks/users.yaml
, followed by
tasks/factorio.yaml
. tasks/users.yaml
is pretty basic, it just sets up a
service user:
|
|
The real bulk of this role is in tasks/factorio.yaml
. It contains all the
steps needed to set up multiple Factorio servers on one machine. Note that this
currently targets RHEL deriviatives, but only for the firewall portion. That can
be easily swapped out for iptables
, ufw
, or your chosen firewall setup.
In general, the steps to set up a headless Factorio server are:
- Download the correct version of the server
- Create required directories and extract the server files there
- Generate a map
- Configure systemd
- Allow ports through firewall
Let’s go through this task-by-task to set up all the Factorio servers. First up
is getting the right version of the game. Wube Software1 releases the
headless server files for free on the Factorio website. The releases follow a
consistent URL scheme, which makes it easy to download the right version using
Ansible’s get_url
task:
|
|
This task won’t download the file if it’s already been downloaded. with_items
is used to make sure we get all of the required versions. Next, we create all
the server directories:
|
|
This is required separately from extracting the tarball, as Ansible’s
unarchive
task doesn’t create target directories. Speaking of unarchive
:
|
|
The remote_src
option is required here to have it look on the remote server
for the tarball, and not locally. Potentially, there could be bandwidth savings
to be had by downloading to the Ansible controller, but the tarball is only 57MB
for version 1.1.104. Next up is the systemd service. Slightly out of order, but
this is roughly where my “document using Ansible as I figure it out” falls apart
somewhat. I created the service and tried to start it before I figured out that
the save needed to be created manually. In any case:
|
|
This references the factorio.service
template, which creates a dynamic2
systemd service for the game servers:
[Unit]
Description=Factorio Game Server - %i
After=network.target
[Service]
Type=simple
ExecStart=/srv/factorio/%i/factorio/bin/x64/factorio --start-server %i
WorkingDirectory=/srv/factorio/%i/factorio
User=factorio
Group=factorio
[Install]
WantedBy=multi-user.target
This uses a nifty feature of systemd where you can use %i
as a placeholder in
your service or socket or whatever, save it as name@.{unit type}
, and refer to
it in systemctl
invocations as name@foo
. That’ll cause %i
to be replaced
with foo
in your services. Anyways, this service uses this to avoid having to
create individual systemd services for each server. You can refer to the servers
as factorio@iqp_1
, instead. Now, we need to create the initial save files for
the servers:
|
|
This uses an interesting feature of command
, namely creates
. If the file
named by creates
exists, command
won’t run. Here, we use it to only generate
save files if one doesn’t already exist. Finally, we create the server
configuration (which allows us to set the server port we defined in the
inventory):
|
|
And enable all the servers:
|
|
Now we’re done, right? Unfortunately, I was also wrong and forgot that on RHEL
derivatives, firewalld
is both enabled by default and also defaults to no open
ports, so we need to allow the UDP ports through the server firewall:
|
|
With that, we’re finally done. There are funky things you can do with SRV
records to deal with non-standard ports, as well as port forwarding if you’re
behind a NAT, but that’s out of scope for this. If you want to see the full
tasks/factorio.yaml
file, you can find that
here.
Articles from my webring
gccrs: An alternative compiler for Rust
This is a guest post from the gccrs project, at the invitation of the Rust Project, to clarify the relationship with the Rust Project and the opportunities for collaboration. gccrs is a work-in-progress alternative compiler for Rust being developed as part…
via Rust Blog November 7, 2024Status update, October 2024
Hi! This month XDC 2024 took place in Montreal. I wasn’t there in-person, but thanks to the organizers I could still ask questions and attend workshops remotely (thanks!). As usual, XDC has been a great reminder of many things I wanted to do but which got bur…
via emersion October 21, 2024Post-OCSP certificate revocation in the Web PKI
Introduction Today, TLS certificates in the Web public key infrastructure (PKI) have long validity: almost all remain valid for at least three months! An attacker compromising a certificate early enough in its lifetime1 keeps it compromised for months. Cer…
via Posts on Seirdy’s Home September 25, 2024Generated by openring