aboutsummaryrefslogtreecommitdiff
path: root/content/blog/ansible/factorio.md
blob: c4fad35fbe3ae938ac6bba40b99f3a4dd5aaf78a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
---
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'
```

## 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!