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.
Rewrite the deployment guides as console walkthroughs
942c93cd06345416c580176c1a149f98e7e96cc0 · Keenan Nemetz · 3d ago · parent 51624dc6
Rewrite the deployment guides as console walkthroughs With the provider configurations gone, the guides are what is left, and their shape changed: render a file, paste it into a console field, point a name at the machine. The index gains the section the Terraform version did not need — first boot happens once. Editing the pasted file afterwards changes nothing, which is how cloud-init works rather than a limitation of these pages, so a later settings change is editing /etc/vole/vole.toml and restarting, and an upgrade is a package install. Each cloud page is a table of console fields: image, size, where the user data goes, and a firewall. The firewall table is load-bearing rather than advice, because Hetzner and DigitalOcean leave a new machine open and AWS hands it a security group that allows nothing inbound. The runner page leads with render.sh runner and keeps the by-hand path under it; the registration commands are written out now that no output prints them.
docs/deploy/aws.md +105−78
| @@ -1,111 +1,138 @@ | |||
| 1 | 1 | # Vole on AWS | |
| 2 | 2 | ||
| 3 | This creates one EC2 instance, a security group, a key pair, and an elastic | ||
| 4 | address, then turns the machine into a Vole instance on first boot. It takes | ||
| 5 | about three minutes. | ||
| 3 | One EC2 instance, configured on its first boot by a file you paste into the launch | ||
| 4 | form. AWS differs from the other two guides in one way that matters: a new | ||
| 5 | instance gets a security group that allows nothing inbound, so the security group | ||
| 6 | is not optional here. | ||
| 6 | 7 | ||
| 7 | 8 | ## What you need | |
| 8 | 9 | ||
| 9 | - Credentials for an account that may create instances, security groups, and | ||
| 10 | elastic addresses. The usual AWS environment variables or profile are enough; | ||
| 11 | no permission is needed for anything outside the instance and its address | ||
| 12 | unless DNS is also managed here. | ||
| 13 | - A DNS name, such as `forge.example.com`. Either point it at the elastic address, | ||
| 14 | or let Terraform create the record if the name is in a Route 53 zone this | ||
| 15 | account holds. | ||
| 16 | - Your SSH public key, and the URL of a Vole `.deb` for `amd64`. | ||
| 10 | - An AWS account, and a region you are happy to keep a team's forge in. | ||
| 11 | - A DNS name, such as `forge.example.com`. | ||
| 12 | - An SSH key pair. The public half goes in the launch form and in the file; keep | ||
| 13 | the private half where you keep keys. | ||
| 14 | - The URL of a Vole `.deb` for `amd64`. | ||
| 17 | 15 | ||
| 18 | ## Deploy | ||
| 16 | ## Render the first-boot file | ||
| 19 | 17 | ||
| 20 | From a clone of the Vole repository: | ||
| 18 | From a clone of the Vole repository, on your own computer: | ||
| 21 | 19 | ||
| 22 | 20 | ```sh | |
| 23 | export AWS_PROFILE=... # or AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY | ||
| 24 | cd deploy/terraform/aws | ||
| 25 | terraform init | ||
| 26 | |||
| 27 | terraform apply \ | ||
| 28 | -var region=eu-central-1 \ | ||
| 29 | -var domain=forge.example.com \ | ||
| 30 | -var "admin_ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \ | ||
| 31 | -var vole_package_url=https://example.com/r/vole/releases/v0.2.0/artifacts/vole_0.2.0-1_amd64.deb | ||
| 21 | cd deploy/cloud-init | ||
| 22 | |||
| 23 | HOSTNAME=forge.example.com \ | ||
| 24 | ADMIN_ACCOUNT=admin \ | ||
| 25 | ADMIN_EMAIL=you@example.com \ | ||
| 26 | ADMIN_SSH_KEY="$(cat ~/.ssh/id_ed25519.pub)" \ | ||
| 27 | VOLE_PACKAGE_URL=https://example.com/r/vole/releases/v0.2.0/artifacts/vole_0.2.0-1_amd64.deb \ | ||
| 28 | ./render.sh > user-data.yaml | ||
| 32 | 29 | ``` | |
| 33 | 30 | ||
| 34 | Type `yes` when it asks. Then point `forge.example.com` at the `public_ipv4` | ||
| 35 | output, or pass `-var create_dns=true -var dns_zone=example.com` to have Terraform | ||
| 36 | create the record in Route 53. | ||
| 31 | Open `user-data.yaml` before you paste it. It is the whole setup, and it is the | ||
| 32 | last chance to notice a wrong account name: names are permanent. | ||
| 37 | 33 | ||
| 38 | ## What you get | ||
| 34 | ## Create the security group and the instance | ||
| 39 | 35 | ||
| 40 | ```text | ||
| 41 | web_url = "https://forge.example.com" | ||
| 42 | git_remote = "ssh://git@forge.example.com:2222/" | ||
| 43 | public_ipv4 = "203.0.113.10" | ||
| 44 | admin_account = "admin" | ||
| 45 | server_login = "ssh -i <private-key> ubuntu@203.0.113.10" | ||
| 46 | ``` | ||
| 36 | In the EC2 console, **Launch instance**: | ||
| 37 | |||
| 38 | | Field | What to choose | | ||
| 39 | |---|---| | ||
| 40 | | Name | `vole` | | ||
| 41 | | AMI | Ubuntu Server 24.04 LTS (HVM), SSD Volume Type, published by Canonical | | ||
| 42 | | Instance type | `t3.medium` — 2 vCPU, 4 GB. Raise it if you expect big pushes or LFS | | ||
| 43 | | Key pair | yours — the same one you put in the file | | ||
| 44 | | Network | the default VPC's public subnet, with **auto-assign public IPv4** enabled. Without a public address the instance cannot fetch its package | | ||
| 45 | | Storage | 40 GiB gp3, encrypted | | ||
| 46 | | **Advanced details → User data** | paste the contents of `user-data.yaml` | | ||
| 47 | |||
| 48 | When it asks for a security group, create one with these inbound rules. Anything | ||
| 49 | else can stay closed; the instance dials out for its package, its certificate, and | ||
| 50 | updates: | ||
| 51 | |||
| 52 | | Type | Protocol | Port | Source | | ||
| 53 | |---|---|---|---| | ||
| 54 | | SSH | tcp | 22 | your team's addresses, for shell access | | ||
| 55 | | Custom TCP | tcp | 2222 | your team's addresses. This is Git over SSH, the write transport | | ||
| 56 | | HTTP | tcp | 80 | anywhere, for the certificate challenge | | ||
| 57 | | HTTPS | tcp | 443 | anywhere | | ||
| 58 | |||
| 59 | Restricting 2222 to your team is the cheapest way to keep the write side private. | ||
| 60 | If you leave it open, Vole still authenticates every connection by key. | ||
| 61 | |||
| 62 | An Elastic IP is optional. It keeps the address after a stop and start, which | ||
| 63 | saves one DNS edit. Both the host name in `public_url` and the SSH host key | ||
| 64 | clients pin survive an address change, so it is convenience rather than | ||
| 65 | correctness. | ||
| 66 | |||
| 67 | ## Point the name at it | ||
| 68 | |||
| 69 | Create an `A` record for `forge.example.com` pointing at the instance's public IPv4 | ||
| 70 | address — in Route 53, or wherever the name lives. Vole cannot obtain a | ||
| 71 | certificate for a name that does not resolve to it yet, and it retries, so a record | ||
| 72 | created a few minutes after the instance is fine. | ||
| 73 | |||
| 74 | ## Check it works | ||
| 75 | |||
| 76 | Open `https://forge.example.com`. The first request may take a few seconds while | ||
| 77 | the certificate is issued; if it fails, wait a minute and reload. | ||
| 47 | 78 | ||
| 48 | Open `web_url` after DNS resolves. The account named by `admin_account` is an | ||
| 49 | instance admin and has no passkey yet; issue one from your laptop: | ||
| 79 | Issue a passkey for the admin account from your laptop: | ||
| 50 | 80 | ||
| 51 | 81 | ```sh | |
| 52 | 82 | vole admin --remote ssh://git@forge.example.com:2222 account invite admin | |
| 53 | 83 | ``` | |
| 54 | 84 | ||
| 85 | Then create a repository, and clone it: | ||
| 86 | |||
| 87 | ```sh | ||
| 88 | vole admin --remote ssh://git@forge.example.com:2222 repo create demo --owner admin | ||
| 89 | vole admin --remote ssh://git@forge.example.com:2222 acl set-default-role demo read | ||
| 90 | git clone ssh://git@forge.example.com:2222/demo.git | ||
| 91 | ``` | ||
| 92 | |||
| 93 | ## Adding a CI runner | ||
| 94 | |||
| 95 | Launch a second instance the same way, with a security group that allows only 22 | ||
| 96 | from your addresses, and give it the runner's file: | ||
| 97 | |||
| 98 | ```sh | ||
| 99 | ./render.sh runner > runner-user-data.yaml # FORGE_HOST=forge.example.com and VOLE_PACKAGE_URL | ||
| 100 | ``` | ||
| 101 | |||
| 102 | It needs one more step per repository before it takes work; see | ||
| 103 | [CI runners](runner.md). | ||
| 104 | |||
| 55 | 105 | ## The settings worth changing | |
| 56 | 106 | ||
| 57 | Everything else has a working default. Pass these with `-var` or in a | ||
| 58 | `terraform.tfvars` file beside `main.tf`. | ||
| 107 | The values you pass to `render.sh`. Everything else has a working default. | ||
| 59 | 108 | ||
| 60 | | Setting | Default | What it does | | ||
| 109 | | Variable | Default | What it does | | ||
| 61 | 110 | |---|---|---| | |
| 62 | | `ssh_admin_cidrs` | everything | Addresses that may reach port 22 (server login) and 2222 (Git over SSH). Setting this to your team's range is the cheapest way to keep the write side private. This is the one setting to change before you leave a public instance running | | ||
| 63 | | `instance_type` | `t3.medium` | 2 vCPU, 4 GB. `t3.large` doubles the memory if builds run on this machine | | ||
| 64 | | `disk_gb` | `40` | Root volume. Repositories, LFS objects, CI logs, and build artifacts live on it; grow it before it fills | | ||
| 65 | | `region` | `eu-central-1` | Any region with an Ubuntu 24.04 `amd64` image, which is all of them | | ||
| 66 | | `name` | `vole` | Name of the instance, key pair, and security group | | ||
| 67 | | `tls_mode` | `acme` | `proxy` leaves TLS to a proxy you run instead, and serves plain HTTP on loopback | | ||
| 68 | | `instance_name` | `Vole` | What the landing page calls this forge | | ||
| 69 | | `runner` | `false` | Create a second machine that runs CI jobs. See [CI runners](runner.md): it needs one more step per repository before it takes work | | ||
| 70 | | `runner_type` | `t3.medium` | Instance type for that machine. Builds want memory, so raise this before the forge's own type | | ||
| 71 | | `runner_disk_gb` | `40` | Its root volume. Container images and caches fill this, not the repositories | | ||
| 72 | | `runner_labels` | `["linux"]` | What the runner advertises; a job with `runs-on` only comes to a machine carrying every label it asks for | | ||
| 73 | | `runner_network` | `bridge` | Whether a job that asks for a network gets one. Jobs that fetch dependencies need it | | ||
| 74 | | `protect_instance` | `true` | Refuses termination through the API, so `terraform destroy` needs `-var protect_instance=false` | | ||
| 75 | |||
| 76 | `terraform apply` is safe to run again: it reports no changes. It does not upgrade | ||
| 77 | Vole, and it does not touch the machine's data. | ||
| 78 | |||
| 79 | With `-var runner=true`, `terraform output runner_setup` prints the commands that | ||
| 80 | let the second machine take jobs. It needs one step per repository before it | ||
| 81 | receives any work; see [CI runners](runner.md). | ||
| 82 | |||
| 83 | The elastic address is not decoration. An EC2 public address changes when an | ||
| 84 | instance is stopped and started, and both the host name in `public_url` and the | ||
| 85 | SSH host key people's Git clients pin depend on the instance keeping its | ||
| 86 | address. | ||
| 111 | | `HOSTNAME` | required | The name the instance answers on. It becomes `public_url`, and passkeys bind to it | | ||
| 112 | | `ADMIN_ACCOUNT` | required | The first account, made an instance admin. Permanent | | ||
| 113 | | `ADMIN_SSH_KEY` | required | That account's public key | | ||
| 114 | | `VOLE_PACKAGE_URL` | required | The `.deb` for the machine's architecture | | ||
| 115 | | `ADMIN_EMAIL` | empty | Contact address for the certificate authority | | ||
| 116 | | `INSTANCE_NAME` | `Vole` | What the landing page calls this forge | | ||
| 117 | | `TLS_MODE` | `acme` | `proxy` leaves TLS to a proxy you run, and serves plain HTTP on loopback | | ||
| 87 | 118 | ||
| 88 | 119 | ## If something is not right | |
| 89 | 120 | ||
| 121 | - **Everything times out.** The security group has no inbound rule for the port, | ||
| 122 | or the instance has no public IPv4 address. | ||
| 90 | 123 | - **The browser shows a certificate warning.** DNS is not answering with the | |
| 91 | elastic address yet, or ports 80 and 443 are not reachable. Check with | ||
| 92 | `dig +short forge.example.com`, then look at `journalctl -u vole | grep -i acme` | ||
| 93 | on the machine. | ||
| 94 | - **Ports 80 and 443 time out.** The default VPC's subnet needs a route to an | ||
| 95 | internet gateway. Terraform does not create a VPC; it uses the account's | ||
| 96 | default one. | ||
| 124 | instance's address yet, or ports 80 and 443 are blocked. Check | ||
| 125 | `dig +short forge.example.com`, then `journalctl -u vole | grep -i acme`. | ||
| 97 | 126 | - **The instance is up but the forge is not.** First-boot output is in | |
| 98 | `/var/log/vole-first-boot.log`, and the service log is `journalctl -u vole` | ||
| 99 | through Systems Manager or the EC2 console. The usual cause is a package URL | ||
| 100 | that 404s or is for the other architecture. | ||
| 101 | - **`terraform apply` fails on credentials.** No profile or keys were found, or | ||
| 102 | the identity may not create instances. | ||
| 103 | - **Pushing is refused.** Port 2222 has to be reachable from where you push, and | ||
| 104 | the key has to belong to an account. `vole whoami --remote | ||
| 105 | ssh://git@forge.example.com:2222` says which account your key is. | ||
| 127 | `/var/log/vole-first-boot.log`; the service log is `journalctl -u vole` through | ||
| 128 | the EC2 console or Systems Manager. The usual cause is a package URL that 404s | ||
| 129 | or is for the other architecture. | ||
| 130 | - **Pushing is refused.** `vole whoami --remote ssh://git@forge.example.com:2222` | ||
| 131 | says which account your key is; if it is not the one you expected, the key was | ||
| 132 | not the one in `user-data.yaml`. | ||
| 106 | 133 | ||
| 107 | 134 | ## Next | |
| 108 | 135 | ||
| 109 | - [Deploying Vole](README.md) — backups, upgrades, and what first boot does | ||
| 136 | - [Deploying Vole](README.md) — what first boot does, backups, and upgrades | ||
| 110 | 137 | - [Packages](../packaging.md) — the `.deb`, the unit file, and upgrades on the machine | |
| 111 | 138 | - [Accounts and access](../accounts-and-access.md) — the first accounts, roles, and passkeys | |