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/aws/main.tf −271

@@ -1,271 +0,0 @@
1 # A Vole instance on EC2, 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/aws
7 # terraform init
8 # terraform apply \
9 # -var region=eu-central-1 \
10 # -var domain=forge.example.com \
11 # -var "admin_ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
12 # -var vole_package_url=https://example.com/r/vole/releases/v0.2.0/artifacts/vole_0.2.0-1_amd64.deb
13 #
14 # Credentials come from the usual AWS environment or profile. See
15 # docs/deploy/aws.md.
16
17 terraform {
18 required_version = ">= 1.9"
19 required_providers {
20 aws = {
21 source = "hashicorp/aws"
22 version = ">= 5.60"
23 }
24 }
25 }
26
27 provider "aws" {
28 region = var.region
29 }
30
31 # Ubuntu's own images, published by Canonical. The filter names the release and
32 # architecture rather than an AMI id, so the plan picks the current image in
33 # whatever region is chosen.
34 data "aws_ami" "ubuntu" {
35 most_recent = true
36 owners = ["099720109477"]
37
38 filter {
39 name = "name"
40 values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]
41 }
42
43 filter {
44 name = "virtualization-type"
45 values = ["hvm"]
46 }
47 }
48
49 locals {
50 tags = {
51 Name = var.name
52 app = "vole"
53 }
54
55 user_data = templatefile(
56 var.cloud_init_file != "" ? var.cloud_init_file : "${path.module}/../../cloud-init/vole.yaml",
57 {
58 hostname = var.domain
59 admin_account = var.admin_account
60 admin_ssh_key = trimspace(var.admin_ssh_public_key)
61 admin_email = var.admin_email
62 instance_name = var.instance_name
63 tls_mode = var.tls_mode
64 vole_package_url = var.vole_package_url
65 }
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 )
83 }
84
85 resource "aws_key_pair" "admin" {
86 key_name = "${var.name}-admin"
87 public_key = trimspace(var.admin_ssh_public_key)
88 tags = local.tags
89 }
90
91 resource "aws_security_group" "vole" {
92 name = var.name
93 description = "Vole software forge"
94 tags = local.tags
95
96 ingress {
97 description = "Server login"
98 from_port = 22
99 to_port = 22
100 protocol = "tcp"
101 cidr_blocks = var.ssh_admin_cidrs
102 }
103
104 # Git over SSH. Restricting this to your team's addresses is the cheapest way
105 # to take the write surface off the internet.
106 ingress {
107 description = "Git over SSH"
108 from_port = 2222
109 to_port = 2222
110 protocol = "tcp"
111 cidr_blocks = var.ssh_admin_cidrs
112 }
113
114 ingress {
115 description = "HTTP, used for the certificate challenge"
116 from_port = 80
117 to_port = 80
118 protocol = "tcp"
119 cidr_blocks = ["0.0.0.0/0"]
120 }
121
122 ingress {
123 description = "HTTPS"
124 from_port = 443
125 to_port = 443
126 protocol = "tcp"
127 cidr_blocks = ["0.0.0.0/0"]
128 }
129
130 ingress {
131 description = "ICMP"
132 from_port = -1
133 to_port = -1
134 protocol = "icmp"
135 cidr_blocks = ["0.0.0.0/0"]
136 }
137
138 egress {
139 from_port = 0
140 to_port = 0
141 protocol = "-1"
142 cidr_blocks = ["0.0.0.0/0"]
143 }
144 }
145
146 resource "aws_instance" "vole" {
147 ami = data.aws_ami.ubuntu.id
148 instance_type = var.instance_type
149 key_name = aws_key_pair.admin.key_name
150 vpc_security_group_ids = [aws_security_group.vole.id]
151 associate_public_ip_address = true
152 user_data = local.user_data
153 tags = local.tags
154
155 # The instance is the only copy of its accounts, CI history, and the host key
156 # people's clients pin. Deleting it has to be done on purpose, from outside
157 # Terraform, or by lifting the protection.
158 disable_api_termination = var.protect_instance
159
160 root_block_device {
161 volume_size = var.disk_gb
162 volume_type = "gp3"
163 encrypted = true
164 tags = local.tags
165 }
166
167 metadata_options {
168 http_endpoint = "enabled"
169 http_tokens = "required"
170 }
171
172 lifecycle {
173 # user_data only matters at first boot; changing it later would replace the
174 # machine, and the machine is the instance's data.
175 ignore_changes = [user_data]
176 }
177 }
178
179 # A fixed address. An EC2 public address changes when the instance is stopped and
180 # started, and clients pin both the host name and Vole's SSH host key.
181 resource "aws_eip" "vole" {
182 domain = "vpc"
183 tags = local.tags
184 }
185
186 resource "aws_eip_association" "vole" {
187 instance_id = aws_instance.vole.id
188 allocation_id = aws_eip.vole.id
189 }
190
191 data "aws_route53_zone" "this" {
192 count = var.create_dns ? 1 : 0
193
194 name = var.dns_zone
195 private_zone = false
196 }
197
198 resource "aws_route53_record" "forge" {
199 count = var.create_dns ? 1 : 0
200
201 zone_id = data.aws_route53_zone.this[0].zone_id
202 name = var.domain
203 type = "A"
204 ttl = 300
205 records = [aws_eip.vole.public_ip]
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 }