Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Ship the runner's unit and let a deployment create a runner machine

d0d4239f3f7236197c87a752d7965de238430f98 · Keenan Nemetz · 3d ago · parent dbae2e01

Ship the runner's unit and let a deployment create a runner machine

The package installs one binary that is the forge, the runner, and the CLI,
so it now carries a second unit. It is not enabled, and it refuses to start
with a message until an operator fills in /etc/vole/runner.env: who runs
builds on a machine is that machine owner's decision.

The unit runs as a dedicated account, because rootless podman needs a home
directory and a range of subordinate ids, and because a job should not find
the forge's own account on the same disk. The arguments live in that one
file rather than in the unit, split at spaces by systemd, so a single unit
serves every machine.

Each Terraform configuration takes runner=true and creates that machine
beside the forge: its own firewall, no backups (its checkouts and caches are
copies), the same key for a shell, and Cloud-init that installs the package,
creates the account and its subuids, generates the runner's key, pins the
instance's host key once, and enables the unit. The runner_setup output
prints the three commands that let it take work, filled in with the
addresses this apply created.

The first-boot file cannot register the runner: the allowlist is per
repository, and at first boot there are no repositories. That step stays
with an instance admin, and the docs say so.

deploy/terraform/aws/main.tf +81

@@ -64,6 +64,22 @@ locals {
64 64 vole_package_url = var.vole_package_url
65 65 }
66 66 )
67
68 # A runner is a second machine with its own job: it leases work from the
69 # instance, runs each job in a container, and reports the result. It does not
70 # need a name of its own, an address that stays put, or backups.
71 runner_user_data = templatefile(
72 var.runner_cloud_init_file != "" ? var.runner_cloud_init_file : "${path.module}/../../cloud-init/runner.yaml",
73 {
74 runner_hostname = "${var.name}-runner"
75 forge_host = var.domain
76 runner_runtime = "podman"
77 runner_network = var.runner_network
78 runner_dir = "/var/lib/vole-runner"
79 runner_labels = join(" ", [for label in var.runner_labels : "--label ${label}"])
80 vole_package_url = var.vole_package_url
81 }
82 )
67 83 }
68 84
69 85 resource "aws_key_pair" "admin" {
@@ -188,3 +204,68 @@ resource "aws_route53_record" "forge" {
188 204 ttl = 300
189 205 records = [aws_eip.vole.public_ip]
190 206 }
207
208 resource "aws_security_group" "runner" {
209 count = var.runner ? 1 : 0
210
211 name = "${var.name}-runner"
212 description = "Vole CI runner"
213 tags = local.tags
214
215 # Nothing arrives except the operator's own shell. The runner dials out to the
216 # instance and to the registries its images come from.
217 ingress {
218 description = "Server login"
219 from_port = 22
220 to_port = 22
221 protocol = "tcp"
222 cidr_blocks = var.ssh_admin_cidrs
223 }
224
225 ingress {
226 description = "ICMP"
227 from_port = -1
228 to_port = -1
229 protocol = "icmp"
230 cidr_blocks = ["0.0.0.0/0"]
231 }
232
233 egress {
234 from_port = 0
235 to_port = 0
236 protocol = "-1"
237 cidr_blocks = ["0.0.0.0/0"]
238 }
239 }
240
241 resource "aws_instance" "runner" {
242 count = var.runner ? 1 : 0
243
244 ami = data.aws_ami.ubuntu.id
245 instance_type = var.runner_type
246 key_name = aws_key_pair.admin.key_name
247 vpc_security_group_ids = [aws_security_group.runner[0].id]
248 associate_public_ip_address = true
249 user_data = local.runner_user_data
250 tags = local.tags
251
252 # A runner holds no data worth protecting from a rebuild: its checkouts and
253 # caches are copies of what the repositories have.
254 disable_api_termination = false
255
256 root_block_device {
257 volume_size = var.runner_disk_gb
258 volume_type = "gp3"
259 encrypted = true
260 tags = local.tags
261 }
262
263 metadata_options {
264 http_endpoint = "enabled"
265 http_tokens = "required"
266 }
267
268 lifecycle {
269 ignore_changes = [user_data]
270 }
271 }