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.

Drop the Terraform configurations; cloud-init is the deployment path

51624dc663738e70cc37b8ac5e232841fd49aaf8 · Keenan Nemetz · 3d ago · parent e21f6c5e

Drop the Terraform configurations; cloud-init is the deployment path

A Vole instance is one package, one configuration file, and one directory.
The first-boot file that installs the package and writes the configuration
is that same work described once, and it needs no provider, no state file,
and no cloud token with write access to an account. What Terraform did
outside the machine — firewall, address, DNS record — is three fields in a
console the operator already has open.

So the three configurations go, and with them what existed only to check
them: the pinned Terraform download in the verify image, the `fmt -check`
step, the .terraform entries in the ignore file, and the directory skip in
the link checker. The deployment check now needs python3 and PyYAML and
nothing else — no network, no credentials.

deploy/README.md keeps the reasoning, so the next person tempted to add a
provider back sees what it would buy and what it would cost.

The templates' header comments no longer name a renderer that is gone:
render.sh fills them in, and check-deploy.py renders them the same way.

deploy/terraform/hetzner/main.tf −224

@@ -1,224 +0,0 @@
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 # A runner is a second machine with its own job: it leases work from the
46 # instance, runs each job in a container, and reports the result. It does not
47 # need a name of its own, an address that stays put, or backups.
48 runner_user_data = templatefile(
49 var.runner_cloud_init_file != "" ? var.runner_cloud_init_file : "${path.module}/../../cloud-init/runner.yaml",
50 {
51 runner_hostname = "${var.name}-runner"
52 forge_host = var.domain
53 runner_runtime = "podman"
54 runner_network = var.runner_network
55 runner_dir = "/var/lib/vole-runner"
56 runner_labels = join(" ", [for label in var.runner_labels : "--label ${label}"])
57 vole_package_url = var.vole_package_url
58 }
59 )
60 }
61
62 resource "hcloud_ssh_key" "admin" {
63 name = "${var.name}-admin"
64 public_key = trimspace(var.admin_ssh_public_key)
65 labels = local.labels
66 }
67
68 resource "hcloud_firewall" "vole" {
69 name = var.name
70 labels = local.labels
71
72 rule {
73 direction = "in"
74 protocol = "icmp"
75 source_ips = ["0.0.0.0/0", "::/0"]
76 }
77
78 # The provider's own SSH, used to log in and repair an instance. Git pushes do
79 # not use it; they arrive on 2222 and are authenticated by Vole.
80 rule {
81 direction = "in"
82 protocol = "tcp"
83 port = "22"
84 source_ips = var.ssh_admin_cidrs
85 }
86
87 # Git over SSH. Allowing only your team's addresses here is the cheapest way to
88 # take the write surface off the internet.
89 rule {
90 direction = "in"
91 protocol = "tcp"
92 port = "2222"
93 source_ips = var.ssh_admin_cidrs
94 }
95
96 rule {
97 direction = "in"
98 protocol = "tcp"
99 port = "80"
100 source_ips = ["0.0.0.0/0", "::/0"]
101 }
102
103 rule {
104 direction = "in"
105 protocol = "tcp"
106 port = "443"
107 source_ips = ["0.0.0.0/0", "::/0"]
108 }
109 }
110
111 resource "hcloud_server" "vole" {
112 name = var.name
113 image = var.image
114 server_type = var.server_type
115 location = var.location
116 ssh_keys = [hcloud_ssh_key.admin.id]
117 firewall_ids = [hcloud_firewall.vole.id]
118 user_data = local.user_data
119 labels = local.labels
120
121 # The instance is the only copy of its accounts, CI history, and the host key
122 # people's clients pin. Both protections have to be lifted on purpose.
123 delete_protection = var.protect_instance
124 rebuild_protection = var.protect_instance
125
126 # Daily snapshots, kept for a week by Hetzner. A backup you never restore is a
127 # hope; see docs/deploy/README.md for why that is not the whole story.
128 backups = var.enable_backups
129
130 public_net {
131 ipv4_enabled = true
132 ipv6_enabled = true
133 }
134
135 lifecycle {
136 # user_data only matters at first boot; changing it later would replace the
137 # machine, and the machine is the instance's data.
138 ignore_changes = [user_data]
139 }
140 }
141
142 resource "hcloud_zone" "this" {
143 count = var.create_dns ? 1 : 0
144
145 name = var.dns_zone
146 mode = "primary"
147 }
148
149 resource "hcloud_zone_rrset" "forge" {
150 count = var.create_dns ? 1 : 0
151
152 zone = hcloud_zone.this[0].name
153 name = "@"
154 type = "A"
155 records = [{ value = hcloud_server.vole.ipv4_address }]
156 }
157
158 resource "hcloud_firewall" "runner" {
159 count = var.runner ? 1 : 0
160
161 name = "${var.name}-runner"
162 labels = local.labels
163
164 # Nothing arrives except the operator's own shell. The runner dials out to the
165 # instance and to the registries its images come from.
166 rule {
167 direction = "in"
168 protocol = "tcp"
169 port = "22"
170 source_ips = var.ssh_admin_cidrs
171 }
172
173 rule {
174 direction = "in"
175 protocol = "icmp"
176 source_ips = ["0.0.0.0/0", "::/0"]
177 }
178
179 rule {
180 direction = "out"
181 protocol = "tcp"
182 port = "1-65535"
183 destination_ips = ["0.0.0.0/0", "::/0"]
184 }
185
186 rule {
187 direction = "out"
188 protocol = "udp"
189 port = "1-65535"
190 destination_ips = ["0.0.0.0/0", "::/0"]
191 }
192
193 rule {
194 direction = "out"
195 protocol = "icmp"
196 destination_ips = ["0.0.0.0/0", "::/0"]
197 }
198 }
199
200 resource "hcloud_server" "runner" {
201 count = var.runner ? 1 : 0
202
203 name = "${var.name}-runner"
204 image = var.image
205 server_type = var.runner_type
206 location = var.location
207 ssh_keys = [hcloud_ssh_key.admin.id]
208 firewall_ids = [hcloud_firewall.runner[0].id]
209 user_data = local.runner_user_data
210 labels = local.labels
211
212 # A runner holds no data worth a snapshot: its checkouts and caches are copies
213 # of what the repositories have.
214 backups = false
215
216 public_net {
217 ipv4_enabled = true
218 ipv6_enabled = true
219 }
220
221 lifecycle {
222 ignore_changes = [user_data]
223 }
224 }