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 | } | ||