# dev: Gitea + Runner + Traefik (git.luke-else.co.uk, cicd.luke-else.co.uk) # Port sources: services/dev/*docker-compose.yml (published ports) and services/todo.md # (the documented UFW allow-list). dev has no firewall in docs/architecture.md, but we # add one anyway for baseline safety - see conversation history. resource "hcloud_firewall" "this" { name = "dev-firewall" rule { # server ssh - wired to the vpn server's public IP only, see infra/main.tf direction = "in" protocol = "tcp" port = "22" source_ips = var.allowed_ssh_source_ips } rule { # gitea ssh (git.luke-else.co.uk, published as 222:22) direction = "in" protocol = "tcp" port = "222" source_ips = ["0.0.0.0/0", "::/0"] } rule { # Traefik http/https (git.luke-else.co.uk, cicd.luke-else.co.uk) direction = "in" protocol = "tcp" port = "80" source_ips = ["0.0.0.0/0", "::/0"] } rule { direction = "in" protocol = "tcp" port = "443" source_ips = ["0.0.0.0/0", "::/0"] } rule { # traffic from prod over the private network direction = "in" protocol = "tcp" port = "1-65535" source_ips = [var.network_ip_range] } rule { direction = "in" protocol = "udp" port = "1-65535" source_ips = [var.network_ip_range] } } resource "hcloud_server" "this" { name = "dev" server_type = var.server_type image = var.image location = var.location ssh_keys = var.ssh_key_ids firewall_ids = [hcloud_firewall.this.id] network { network_id = var.network_id ip = var.private_ip } connection { type = "ssh" host = self.ipv4_address user = "root" private_key = file(var.ssh_private_key_path) } provisioner "file" { content = var.bootstrap_script destination = "/root/bootstrap.sh" } provisioner "remote-exec" { inline = [ "chmod +x /root/bootstrap.sh", "/root/bootstrap.sh", ] } } resource "hcloud_volume" "storage" { name = "dev-storage" size = var.volume_size server_id = hcloud_server.this.id automount = true format = "ext4" lifecycle { prevent_destroy = true } } locals { # Hetzner's automount convention (hc-utils' 99-hc-volume-automount.rules) is # always /mnt/HC_Volume_ - deterministic once the volume exists, since the # id is stable for the volume's lifetime regardless of which server it's # attached to. data_dir = "/mnt/HC_Volume_${hcloud_volume.storage.id}" } # Renders services/dev/Runners/docker-compose.yml directly into the repo, with # one runner service per var.runner_count. This only touches the local working # tree - deploying the change still goes through the normal scp + spinup.sh flow. resource "local_file" "runners_compose" { filename = "${path.root}/../services/dev/Runners/docker-compose.yml" content = templatefile("${path.module}/templates/runners-docker-compose.yml.tftpl", { runner_count = var.runner_count data_dir = local.data_dir }) file_permission = "0644" } # DATA_DIR is picked up automatically by `docker compose` (which auto-loads # .env from its working directory) for every dev/*-docker-compose.yml bind # mount - see services/dev/*.yml. Not committed (see .gitignore): it's tied to # the live volume's ID, so it's regenerated by tofu apply, not handed off via git. resource "local_file" "env" { filename = "${path.root}/../services/dev/.env" content = "DATA_DIR=${local.data_dir}\n" file_permission = "0644" } locals { # Everything under services/dev except the two files above: those are # tracked via their own resource content (reading them back with fileset()/ # filesha1() before they exist would fail during `tofu plan`). dev_static_files = sort([ for f in fileset("${path.root}/../services/dev", "**") : f if f != ".env" && f != "Runners/docker-compose.yml" ]) dev_static_files_hash = sha1(join("", [ for f in local.dev_static_files : filesha1("${path.root}/../services/dev/${f}") ])) } # Copies services/dev to the server on every apply that changes it (a hand-edited # compose file, a new runner count, ...) or recreates the server - not just once # at first creation. Split from hcloud_server.this because it must run after the # generated files above, which in turn depend on the volume, which depends on # the server - so it can't be a provisioner on the server resource itself. resource "null_resource" "deploy_services" { triggers = { server_id = hcloud_server.this.id static_files = local.dev_static_files_hash env = local_file.env.content runners = local_file.runners_compose.content } connection { type = "ssh" host = hcloud_server.this.ipv4_address user = "root" private_key = file(var.ssh_private_key_path) } provisioner "remote-exec" { inline = ["mkdir -p /home/${var.deploy_user}/services"] } provisioner "file" { source = "${path.root}/../services/dev" destination = "/home/${var.deploy_user}/services" } provisioner "remote-exec" { inline = [ "chown -R ${var.deploy_user}:${var.deploy_user} /home/${var.deploy_user}/services", "chmod +x /home/${var.deploy_user}/services/dev/*.sh", ] } depends_on = [ hcloud_server.this, local_file.env, local_file.runners_compose, ] }