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.

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/hetzner/main.tf +140

@@ -0,0 +1,140 @@
1 # A Vole instance on Hetzner Cloud, configured by cloud-init.
2 #
3 # Run it from a clone of the Vole repository, because the machine's user data is
4 # deploy/cloud-init/vole.yaml and is read by its path:
5 #
6 # cd deploy/terraform/hetzner
7 # terraform init
8 # terraform apply \
9 # -var domain=forge.example.com \
10 # -var "admin_ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
11 # -var vole_package_url=https://example.com/r/vole/releases/v0.2.0/artifacts/vole_0.2.0-1_amd64.deb
12 #
13 # HCLOUD_TOKEN must be set. See docs/deploy/hetzner.md.
14
15 terraform {
16 required_version = ">= 1.9"
17 required_providers {
18 hcloud = {
19 source = "hetznercloud/hcloud"
20 version = ">= 1.49"
21 }
22 }
23 }
24
25 provider "hcloud" {}
26
27 locals {
28 labels = {
29 app = "vole"
30 }
31
32 user_data = templatefile(
33 var.cloud_init_file != "" ? var.cloud_init_file : "${path.module}/../../cloud-init/vole.yaml",
34 {
35 hostname = var.domain
36 admin_account = var.admin_account
37 admin_ssh_key = trimspace(var.admin_ssh_public_key)
38 admin_email = var.admin_email
39 instance_name = var.instance_name
40 tls_mode = var.tls_mode
41 vole_package_url = var.vole_package_url
42 }
43 )
44 }
45
46 resource "hcloud_ssh_key" "admin" {
47 name = "${var.name}-admin"
48 public_key = trimspace(var.admin_ssh_public_key)
49 labels = local.labels
50 }
51
52 resource "hcloud_firewall" "vole" {
53 name = var.name
54 labels = local.labels
55
56 rule {
57 direction = "in"
58 protocol = "icmp"
59 source_ips = ["0.0.0.0/0", "::/0"]
60 }
61
62 # The provider's own SSH, used to log in and repair an instance. Git pushes do
63 # not use it; they arrive on 2222 and are authenticated by Vole.
64 rule {
65 direction = "in"
66 protocol = "tcp"
67 port = "22"
68 source_ips = var.ssh_admin_cidrs
69 }
70
71 # Git over SSH. Allowing only your team's addresses here is the cheapest way to
72 # take the write surface off the internet.
73 rule {
74 direction = "in"
75 protocol = "tcp"
76 port = "2222"
77 source_ips = var.ssh_admin_cidrs
78 }
79
80 rule {
81 direction = "in"
82 protocol = "tcp"
83 port = "80"
84 source_ips = ["0.0.0.0/0", "::/0"]
85 }
86
87 rule {
88 direction = "in"
89 protocol = "tcp"
90 port = "443"
91 source_ips = ["0.0.0.0/0", "::/0"]
92 }
93 }
94
95 resource "hcloud_server" "vole" {
96 name = var.name
97 image = var.image
98 server_type = var.server_type
99 location = var.location
100 ssh_keys = [hcloud_ssh_key.admin.id]
101 firewall_ids = [hcloud_firewall.vole.id]
102 user_data = local.user_data
103 labels = local.labels
104
105 # The instance is the only copy of its accounts, CI history, and the host key
106 # people's clients pin. Both protections have to be lifted on purpose.
107 delete_protection = var.protect_instance
108 rebuild_protection = var.protect_instance
109
110 # Daily snapshots, kept for a week by Hetzner. A backup you never restore is a
111 # hope; see docs/deploy/README.md for why that is not the whole story.
112 backups = var.enable_backups
113
114 public_net {
115 ipv4_enabled = true
116 ipv6_enabled = true
117 }
118
119 lifecycle {
120 # user_data only matters at first boot; changing it later would replace the
121 # machine, and the machine is the instance's data.
122 ignore_changes = [user_data]
123 }
124 }
125
126 resource "hcloud_zone" "this" {
127 count = var.create_dns ? 1 : 0
128
129 name = var.dns_zone
130 mode = "primary"
131 }
132
133 resource "hcloud_zone_rrset" "forge" {
134 count = var.create_dns ? 1 : 0
135
136 zone = hcloud_zone.this[0].name
137 name = "@"
138 type = "A"
139 records = [{ value = hcloud_server.vole.ipv4_address }]
140 }