aboutsummaryrefslogtreecommitdiff
path: root/content/blog/ansible/factorio.md
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--content/blog/ansible/factorio.md265
1 files changed, 265 insertions, 0 deletions
diff --git a/content/blog/ansible/factorio.md b/content/blog/ansible/factorio.md
new file mode 100644
index 0000000..08e2827
--- /dev/null
+++ b/content/blog/ansible/factorio.md
@@ -0,0 +1,265 @@
+---
+title: 'How to self host a Factorio headless server'
+description: 'Automated with ansible'
+date: '2024-09-25'
+tags:
+- ansible
+- Debian
+- Factorio
+---
+
+## Introduction
+
+With the upcoming v2.0 release next month, we decided to try a [seablock](https://mods.factorio.com/mod/SeaBlock) run with a friend and see how far we go in this time frame. Here is a the small ansible role I wrote to deploy this. It is for a Debian server but any Linux distribution with systemd will do. And if you ignore the service unit file, any Linux or even [FreeBSD](factorio-server-in-a-linux-jail.md) will do.
+
+## Tasks
+
+This role has a single `tasks/main.yaml` file containing the following.
+
+### User
+
+This is fairly standard:
+``` yaml
+- name: 'Create factorio group'
+ group:
+ name: 'factorio'
+ system: 'yes'
+
+- name: 'Create factorio user'
+ user:
+ name: 'factorio'
+ group: 'factorio'
+ shell: '/usr/bin/bash'
+ home: '/srv/factorio'
+ createhome: 'yes'
+ system: 'yes'
+ password: '*'
+```
+
+### Factorio
+
+Factorio has an API endpoint that provides information about its latest releases, I query and then parse it with:
+``` yaml
+- name: 'Retrieve factorio latest release number'
+ shell:
+ cmd: "curl -s https://factorio.com/api/latest-releases | jq -r '.stable.headless'"
+ register: 'factorio_version_info'
+ changed_when: False
+
+- set_fact:
+ factorio_version: '{{ factorio_version_info.stdout_lines[0] }}'
+```
+
+Afterwards, it is just a question of downloading and extracting factorio:
+``` yaml
+- name: 'Download factorio'
+ get_url:
+ url: "https://www.factorio.com/get-download/{{ factorio_version }}/headless/linux64"
+ dest: '/srv/factorio/headless-{{ factorio_version }}.zip'
+ mode: '0444'
+ register: 'factorio_downloaded'
+
+- name: 'Extract new factorio version'
+ ansible.builtin.unarchive:
+ src: '/srv/factorio/headless-{{ factorio_version }}.zip'
+ dest: '/srv/factorio'
+ owner: 'factorio'
+ group: 'factorio'
+ remote_src: 'yes'
+ notify: 'restart factorio'
+ when: 'factorio_downloaded.changed'
+```
+
+I also create the saves directory with:
+``` yaml
+- name: 'Make factorio saves directory'
+ file:
+ path: '/srv/factorio/factorio/saves'
+ owner: 'factorio'
+ group: 'factorio'
+ mode: '0750'
+ state: 'directory'
+```
+
+### Configuration files
+
+There are two configuration files to copy from the `files` folder:
+``` yaml
+- name: 'Deploy configuration files'
+ copy:
+ src: '{{ item.src }}'
+ dest: '{{ item.dest }}'
+ owner: 'factorio'
+ group: 'factorio'
+ mode: '0440'
+ notify:
+ - 'systemctl daemon-reload'
+ - 'restart factorio'
+ loop:
+ - { src: 'factorio.service', dest: '/etc/systemd/system/' }
+ - { src: 'server-adminlist.json', dest: '/srv/factorio/factorio/' }
+```
+
+The systemd service unit file contains:
+``` ini
+[Unit]
+Descripion=Factorio Headless Server
+After=network.target
+After=systemd-user-sessions.service
+After=network-online.target
+
+[Service]
+Type=simple
+User=factorio
+ExecStart=/srv/factorio/factorio/bin/x64/factorio --start-server game.zip
+WorkingDirectory=/srv/factorio/factorio
+
+[Install]
+WantedBy=multi-user.target
+```
+
+The admin list is simply:
+
+``` json
+["adyxax"]
+```
+
+I generate the factorio game password with terraform/OpenTofu using a resource like:
+
+``` hcl
+resource "random_password" "factorio" {
+ length = 16
+
+ lifecycle {
+ ignore_changes = [
+ length,
+ lower,
+ ]
+ }
+}
+```
+
+This allows me to have it persist in the terraform state which is a good thing. For simplification, let's say that this state (which is a json file) is in a local file that I can load with:
+``` yaml
+- name: 'Load the tofu state to read the factorio game password'
+ include_vars:
+ file: '../../../../adyxax.org/01-legacy/terraform.tfstate'
+ name: 'tofu_state_legacy'
+```
+
+Given this template file:
+``` json
+{
+ "name": "Normalians",
+ "description": "C'est sur ce serveur que jouent les beaux gosses",
+ "tags": ["game", "tags"],
+ "max_players": 0,
+ "visibility": {
+ "public": false,
+ "lan": false
+ },
+ "username": "",
+ "password": "",
+ "token": "",
+ "game_password": "{{ factorio_game_password[0] }}",
+ "require_user_verification": false,
+ "max_upload_in_kilobytes_per_second": 0,
+ "max_upload_slots": 5,
+ "minimum_latency_in_ticks": 0,
+ "max_heartbeats_per_second": 60,
+ "ignore_player_limit_for_returning_players": false,
+ "allow_commands": "admins-only",
+ "autosave_interval": 10,
+ "autosave_slots": 5,
+ "afk_autokick_interval": 0,
+ "auto_pause": true,
+ "only_admins_can_pause_the_game": true,
+ "autosave_only_on_server": true,
+ "non_blocking_saving": true,
+ "minimum_segment_size": 25,
+ "minimum_segment_size_peer_count": 20,
+ "maximum_segment_size": 100,
+ "maximum_segment_size_peer_count": 10
+}
+```
+
+Note the usage of `[0]` for the variable expansion: it is a disappointing trick that you have to remember when dealing with json query parsing using ansible's filters: these always return an array. The template invocation is:
+``` yaml
+- name: 'Deploy configuration templates'
+ template:
+ src: 'server-settings.json'
+ dest: '/srv/factorio/factorio/'
+ owner: 'factorio'
+ group: 'factorio'
+ mode: '0440'
+ notify: 'restart factorio'
+ vars:
+ factorio_game_password: "{{ tofu_state_legacy | json_query(\"resources[?type=='random_password'&&name=='factorio'].instances[0].attributes.result\") }}"
+```
+
+### Service
+
+Finally I start and activate the factorio service on boot:
+``` yaml
+- name: 'Start factorio and activate it on boot'
+ service:
+ name: 'factorio'
+ enabled: 'yes'
+ state: 'started'
+```
+
+### Backups
+
+I invoke a personal borg role to configure my backups. I will detail the workings of this role in a next article:
+``` yaml
+- include_role:
+ name: 'borg'
+ tasks_from: 'client'
+ vars:
+ client:
+ jobs:
+ - name: 'save'
+ paths:
+ - '/srv/factorio/factorio/saves/game.zip'
+ name: 'factorio'
+ server: '{{ factorio.borg }}'
+```
+
+## Handlers
+
+I have these two handlers:
+
+``` yaml
+---
+- name: 'systemctl daemon-reload'
+ shell:
+ cmd: 'systemctl daemon-reload'
+
+- name: 'restart factorio'
+ service:
+ name: 'factorio'
+ state: 'restarted'
+```
+
+## Generating a map and starting the game
+
+If you just followed this guide factorio failed to start on the server because it does not have a map in its save folder. If that is not the case for you because you are coming back to this article after some time, remember to stop factorio with `systemctl stop factorio` before continuing. If you do not, when you later restart factorio will overwrite your newly uploaded save.
+
+Launch factorio locally, install any mod you want then go to single player and generate a new map with your chosen settings. Save the game then quit and go back to your terminal.
+
+Find the save file (if playing on steam it will be in `~/.factorio/saves/`) and upload it to `/srv/factorio/factorio/saves/game.zip`. If you are using mods, `rsync` the mods folder that leaves next to your saves directory to the server with:
+
+``` shell
+rsync -r ~/.factorio/mods/ root@factorio.adyxax.org:/srv/factorio/factorio/mods/`
+```
+
+Then give these files to the factorio user on your server before restarting the game:
+
+``` shell
+chown -R factorio:factorio /srv/factorio
+systemctl start factorio
+```
+
+## Conclusion
+
+Good luck and have fun!