vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Turn a fresh machine into an instance on first boot
04e063ca5d73b8f57f45d7c8bb07b0fdaa91b5b8 · Keenan Nemetz · 3d ago · parent ea21f923
Turn a fresh machine into an instance on first boot A new instance is one package, one configuration, one directory, and one account. Assembling that by hand is where a first deployment goes wrong: the capability the hardened unit needs for ports 80 and 443, the order that makes the package's own init harmless, and the fact that the first instance admin has to be granted on the host. deploy/cloud-init/vole.yaml is that assembly. It stages two configurations, one that lets Vole hold its own certificate and one that leaves TLS to a proxy, then installs the package, writes the configuration, creates the instance admin, and starts the service. Running it again changes nothing. render.sh renders it for providers that take user data without Terraform, using the placeholder list as the contract with the template. The three Terraform configurations create the machine, its firewall, its address, and optionally the DNS record, and hand it that same file. They are root modules: init and apply, three variables, no other file to edit. First boot is not configuration management. apply does not upgrade an instance or touch its data, because users.db migrates in place and cannot be rolled back.
deploy/terraform/aws/variables.tf +102
| @@ -0,0 +1,102 @@ | |||
| 1 | variable "name" { | ||
| 2 | description = "Name for the instance, key pair, and security group." | ||
| 3 | type = string | ||
| 4 | default = "vole" | ||
| 5 | } | ||
| 6 | |||
| 7 | variable "domain" { | ||
| 8 | description = "DNS name the instance answers on, as in forge.example.com. It becomes public_url, and passkeys are bound to it, so it cannot change later without re-enrolling everyone." | ||
| 9 | type = string | ||
| 10 | |||
| 11 | validation { | ||
| 12 | condition = !startswith(var.domain, "http://") && !startswith(var.domain, "https://") | ||
| 13 | error_message = "domain is a bare host name: forge.example.com, not https://forge.example.com." | ||
| 14 | } | ||
| 15 | } | ||
| 16 | |||
| 17 | variable "admin_account" { | ||
| 18 | description = "Name of the first account, made an instance admin. Account names are permanent." | ||
| 19 | type = string | ||
| 20 | default = "admin" | ||
| 21 | } | ||
| 22 | |||
| 23 | variable "admin_ssh_public_key" { | ||
| 24 | description = "Public key of the first account, as in file contents: ssh-ed25519 AAAAC3... you@host." | ||
| 25 | type = string | ||
| 26 | } | ||
| 27 | |||
| 28 | variable "admin_email" { | ||
| 29 | description = "Contact address given to the certificate authority. Optional but recommended when ACME is on." | ||
| 30 | type = string | ||
| 31 | default = "" | ||
| 32 | } | ||
| 33 | |||
| 34 | variable "instance_name" { | ||
| 35 | description = "Name the landing page shows." | ||
| 36 | type = string | ||
| 37 | default = "Vole" | ||
| 38 | } | ||
| 39 | |||
| 40 | variable "tls_mode" { | ||
| 41 | description = "acme lets Vole obtain its own certificate on ports 80 and 443 and needs DNS pointing here. proxy leaves TLS to a proxy in front and serves plain HTTP on loopback." | ||
| 42 | type = string | ||
| 43 | default = "acme" | ||
| 44 | |||
| 45 | validation { | ||
| 46 | condition = contains(["acme", "proxy"], var.tls_mode) | ||
| 47 | error_message = "tls_mode must be acme or proxy." | ||
| 48 | } | ||
| 49 | } | ||
| 50 | |||
| 51 | variable "vole_package_url" { | ||
| 52 | description = "URL of the .deb for this machine's architecture, from a Vole release page: https://.../r/vole/releases/v0.2.0/artifacts/vole_0.2.0-1_amd64.deb" | ||
| 53 | type = string | ||
| 54 | } | ||
| 55 | |||
| 56 | variable "protect_instance" { | ||
| 57 | description = "Refuse to terminate the instance from the API while true. A machine holding a team's repositories should not disappear because of a mistyped command." | ||
| 58 | type = bool | ||
| 59 | default = true | ||
| 60 | } | ||
| 61 | |||
| 62 | variable "region" { | ||
| 63 | description = "AWS region, as in eu-central-1 or us-east-1." | ||
| 64 | type = string | ||
| 65 | default = "eu-central-1" | ||
| 66 | } | ||
| 67 | |||
| 68 | variable "instance_type" { | ||
| 69 | description = "EC2 instance type. t3.medium (2 vCPU, 4 GB) is enough for a small team. Graviton types need an arm64 image and an arm64 package." | ||
| 70 | type = string | ||
| 71 | default = "t3.medium" | ||
| 72 | } | ||
| 73 | |||
| 74 | variable "disk_gb" { | ||
| 75 | description = "Root volume size in GiB. Repositories, LFS objects, and CI logs live on it; 40 is a reasonable start." | ||
| 76 | type = number | ||
| 77 | default = 40 | ||
| 78 | } | ||
| 79 | |||
| 80 | variable "create_dns" { | ||
| 81 | description = "Create the A record in a Route 53 hosted zone this account already holds. Leave false to point your own DNS at the address output instead." | ||
| 82 | type = bool | ||
| 83 | default = false | ||
| 84 | } | ||
| 85 | |||
| 86 | variable "dns_zone" { | ||
| 87 | description = "Hosted zone name for create_dns, as in example.com. Must match the zone exactly." | ||
| 88 | type = string | ||
| 89 | default = "" | ||
| 90 | } | ||
| 91 | |||
| 92 | variable "ssh_admin_cidrs" { | ||
| 93 | description = "Addresses allowed to reach port 22 (server login) and port 2222 (Git over SSH). Restrict these to your team if you can." | ||
| 94 | type = list(string) | ||
| 95 | default = ["0.0.0.0/0"] | ||
| 96 | } | ||
| 97 | |||
| 98 | variable "cloud_init_file" { | ||
| 99 | description = "Override the user data file. Empty uses deploy/cloud-init/vole.yaml from this repository." | ||
| 100 | type = string | ||
| 101 | default = "" | ||
| 102 | } | ||