diff --git a/infra/live/production/ci-runner/terragrunt.hcl b/infra/live/production/ci-runner/terragrunt.hcl new file mode 100644 index 000000000..616b326f8 --- /dev/null +++ b/infra/live/production/ci-runner/terragrunt.hcl @@ -0,0 +1,15 @@ +include "root" { + path = find_in_parent_folders() +} + +terraform { + source = "../../../modules/ci-runner" +} + +inputs = { + instance_type = "GP1-S" # 8 vCPU, 32GB RAM, 300GB disk + + # Sensitive — pass at apply time: + # terragrunt apply -var 'tailscale_auth_key=tskey-auth-...' -var 'runner_token=...' + # Or set TF_VAR_tailscale_auth_key and TF_VAR_runner_token env vars. +} diff --git a/infra/modules/ci-runner/cloud-init.yaml.tpl b/infra/modules/ci-runner/cloud-init.yaml.tpl new file mode 100644 index 000000000..8e20a28e5 --- /dev/null +++ b/infra/modules/ci-runner/cloud-init.yaml.tpl @@ -0,0 +1,105 @@ +#cloud-config +# +# Foxhunt CI Runner (act_runner for Gitea Actions) +# Tailscale-only access, native Docker (no DinD). +# +package_update: true +package_upgrade: true + +packages: + - ca-certificates + - curl + - gnupg + +write_files: + - path: /opt/act_runner/config.yaml + permissions: '0644' + content: | + log: + level: info + + runner: + file: /opt/act_runner/.runner + capacity: ${runner_capacity} + timeout: 3h + insecure: false + fetch_timeout: 5s + fetch_interval: 2s + + cache: + enabled: true + dir: /opt/act_runner/cache + + container: + network: host + privileged: false + options: + valid_volumes: [] + + - path: /etc/systemd/system/act-runner.service + permissions: '0644' + content: | + [Unit] + Description=Gitea Actions Runner + After=docker.service tailscaled.service + Requires=docker.service + + [Service] + Type=simple + WorkingDirectory=/opt/act_runner + ExecStart=/usr/local/bin/act_runner daemon --config /opt/act_runner/config.yaml + Restart=always + RestartSec=5 + + [Install] + WantedBy=multi-user.target + +runcmd: + # --- Docker CE --- + - install -m 0755 -d /etc/apt/keyrings + - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + - chmod a+r /etc/apt/keyrings/docker.gpg + - | + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list + - apt-get update -qq + - apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin + - systemctl enable docker + - systemctl start docker + + # --- Tailscale --- + - curl -fsSL https://tailscale.com/install.sh | sh + - tailscale up --authkey=${tailscale_auth_key} --hostname=${instance_name} --ssh + + # Wait for Tailscale connectivity + - | + for i in $(seq 1 30); do + TS_IP=$(tailscale ip -4 2>/dev/null) + if [ -n "$TS_IP" ]; then + echo "Tailscale IP: $TS_IP" + break + fi + sleep 2 + done + + # --- act_runner --- + - | + ACT_RUNNER_VERSION="0.3.0" + curl -fsSL "https://dl.gitea.com/act_runner/$${ACT_RUNNER_VERSION}/act_runner-$${ACT_RUNNER_VERSION}-linux-amd64" \ + -o /usr/local/bin/act_runner + chmod +x /usr/local/bin/act_runner + + # Register runner with Gitea (via Tailscale) + - | + cd /opt/act_runner + act_runner register --no-interactive \ + --instance "${gitea_url}" \ + --token "${runner_token}" \ + --name "${instance_name}" \ + --labels "${runner_labels}" + + # Enable and start runner service + - systemctl daemon-reload + - systemctl enable act-runner + - systemctl start act-runner + + - echo "PROVISIONING_COMPLETE" > /var/log/cloud-init-done diff --git a/infra/modules/ci-runner/main.tf b/infra/modules/ci-runner/main.tf new file mode 100644 index 000000000..ab6ef48bd --- /dev/null +++ b/infra/modules/ci-runner/main.tf @@ -0,0 +1,38 @@ +# Security group: drop all inbound (Tailscale-only access) +resource "scaleway_instance_security_group" "ci_runner" { + name = "${var.instance_name}-sg" + zone = var.zone + inbound_default_policy = "drop" + outbound_default_policy = "accept" +} + +resource "scaleway_instance_ip" "ci_runner" { + zone = var.zone +} + +resource "scaleway_instance_server" "ci_runner" { + name = var.instance_name + type = var.instance_type + image = var.image + zone = var.zone + + ip_id = scaleway_instance_ip.ci_runner.id + security_group_id = scaleway_instance_security_group.ci_runner.id + + root_volume { + size_in_gb = var.root_volume_size_gb + } + + user_data = { + cloud-init = templatefile("${path.module}/cloud-init.yaml.tpl", { + tailscale_auth_key = var.tailscale_auth_key + instance_name = var.instance_name + gitea_url = var.gitea_url + runner_token = var.runner_token + runner_labels = var.runner_labels + runner_capacity = var.runner_capacity + }) + } + + tags = ["ci", "foxhunt"] +} diff --git a/infra/modules/ci-runner/outputs.tf b/infra/modules/ci-runner/outputs.tf new file mode 100644 index 000000000..0e75013c8 --- /dev/null +++ b/infra/modules/ci-runner/outputs.tf @@ -0,0 +1,14 @@ +output "instance_id" { + description = "ID of the CI runner instance" + value = scaleway_instance_server.ci_runner.id +} + +output "public_ip" { + description = "Public IP (needed for Tailscale outbound tunnel)" + value = scaleway_instance_ip.ci_runner.address +} + +output "tailscale_hostname" { + description = "Tailscale hostname of the CI runner" + value = var.instance_name +} diff --git a/infra/modules/ci-runner/variables.tf b/infra/modules/ci-runner/variables.tf new file mode 100644 index 000000000..4d720f6bc --- /dev/null +++ b/infra/modules/ci-runner/variables.tf @@ -0,0 +1,63 @@ +variable "zone" { + description = "Scaleway zone for the CI runner instance" + type = string +} + +variable "region" { + description = "Scaleway region" + type = string +} + +variable "instance_type" { + description = "Instance type for the CI runner" + type = string + default = "GP1-S" +} + +variable "instance_name" { + description = "Name of the CI runner instance" + type = string + default = "vm-fxhnt-ci" +} + +variable "image" { + description = "OS image for the instance" + type = string + default = "ubuntu_noble" +} + +variable "root_volume_size_gb" { + description = "Root volume size in GB (Docker images + build cache)" + type = number + default = 100 +} + +variable "tailscale_auth_key" { + description = "Tailscale pre-auth key for joining the tailnet" + type = string + sensitive = true +} + +variable "gitea_url" { + description = "Gitea instance URL" + type = string + default = "https://git.fxhnt.ai" +} + +variable "runner_token" { + description = "Gitea Actions runner registration token" + type = string + sensitive = true +} + +variable "runner_labels" { + description = "Runner labels for act_runner" + type = string + default = "ubuntu-latest:docker://catthehacker/ubuntu:act-latest" +} + +variable "runner_capacity" { + description = "Number of concurrent jobs" + type = number + default = 1 +} diff --git a/infra/modules/secrets/main.tf b/infra/modules/secrets/main.tf index 915634f75..520e47bf2 100644 --- a/infra/modules/secrets/main.tf +++ b/infra/modules/secrets/main.tf @@ -31,3 +31,15 @@ resource "scaleway_secret_version" "db_password" { data = base64encode(random_password.db_password.result) region = var.region } + +resource "scaleway_secret" "tailscale_api_key" { + name = "foxhunt-tailscale-api-key" + project_id = var.project_id + region = var.region +} + +resource "scaleway_secret_version" "tailscale_api_key" { + secret_id = scaleway_secret.tailscale_api_key.id + data = base64encode(var.tailscale_api_key) + region = var.region +} diff --git a/infra/modules/secrets/variables.tf b/infra/modules/secrets/variables.tf index 2b3388782..6d3150254 100644 --- a/infra/modules/secrets/variables.tf +++ b/infra/modules/secrets/variables.tf @@ -7,3 +7,9 @@ variable "project_id" { description = "Scaleway project ID" type = string } + +variable "tailscale_api_key" { + description = "Tailscale API key for generating pre-auth keys" + type = string + sensitive = true +}